@flamingo-stack/openframe-frontend-core 0.0.510 → 0.0.511-snapshot.20260804140004
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/dist/chat-protocol/decode.d.ts.map +1 -1
- package/dist/chat-protocol/frames.d.ts +26 -1
- package/dist/chat-protocol/frames.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +24 -0
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.js +24 -0
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/{chunk-GE3JUTW2.js → chunk-27OKD7ZV.js} +44 -14
- package/dist/chunk-27OKD7ZV.js.map +1 -0
- package/dist/{chunk-RHQ7BJKK.cjs → chunk-2WWDZR4D.cjs} +73 -73
- package/dist/{chunk-RHQ7BJKK.cjs.map → chunk-2WWDZR4D.cjs.map} +1 -1
- package/dist/{chunk-J2JUAS4C.cjs → chunk-2XL7UK5Y.cjs} +11 -11
- package/dist/{chunk-J2JUAS4C.cjs.map → chunk-2XL7UK5Y.cjs.map} +1 -1
- package/dist/{chunk-Q4K3TGNP.cjs → chunk-3NECEXDF.cjs} +74 -44
- package/dist/chunk-3NECEXDF.cjs.map +1 -0
- package/dist/{chunk-JOCFPAS6.js → chunk-4NFDSMB5.js} +6 -6
- package/dist/{chunk-SYZFFRVD.cjs → chunk-5BAGE7AI.cjs} +13 -13
- package/dist/{chunk-SYZFFRVD.cjs.map → chunk-5BAGE7AI.cjs.map} +1 -1
- package/dist/{chunk-53JKCFFE.cjs → chunk-5JBDT2MQ.cjs} +12 -12
- package/dist/{chunk-53JKCFFE.cjs.map → chunk-5JBDT2MQ.cjs.map} +1 -1
- package/dist/{chunk-XJVRAKEX.cjs → chunk-67YDSTH7.cjs} +56 -33
- package/dist/chunk-67YDSTH7.cjs.map +1 -0
- package/dist/{chunk-WNOGR5AA.js → chunk-6LMTBNLN.js} +3 -3
- package/dist/{chunk-6FIDTXYC.js → chunk-7AQSXQEL.js} +3 -3
- package/dist/{chunk-JMBIGH4H.cjs → chunk-7VBEOGCR.cjs} +83 -83
- package/dist/{chunk-JMBIGH4H.cjs.map → chunk-7VBEOGCR.cjs.map} +1 -1
- package/dist/{chunk-5AZZQQYS.cjs → chunk-AWLFJMOJ.cjs} +40 -40
- package/dist/{chunk-5AZZQQYS.cjs.map → chunk-AWLFJMOJ.cjs.map} +1 -1
- package/dist/{chunk-RJ3D3PE2.cjs → chunk-DQDPDUI3.cjs} +9 -9
- package/dist/{chunk-RJ3D3PE2.cjs.map → chunk-DQDPDUI3.cjs.map} +1 -1
- package/dist/{chunk-2RLI5XVI.cjs → chunk-F2YI5N7E.cjs} +16 -16
- package/dist/{chunk-2RLI5XVI.cjs.map → chunk-F2YI5N7E.cjs.map} +1 -1
- package/dist/{chunk-LBOWSKGM.js → chunk-FI35LJ3L.js} +5 -5
- package/dist/{chunk-BTUHMCYW.js → chunk-FS3YSNSU.js} +193 -6
- package/dist/chunk-FS3YSNSU.js.map +1 -0
- package/dist/{chunk-RB2EW6IZ.cjs → chunk-GOFTC2HV.cjs} +3690 -3472
- package/dist/chunk-GOFTC2HV.cjs.map +1 -0
- package/dist/{chunk-CTMLY2GN.js → chunk-HGZPH7S5.js} +5 -5
- package/dist/{chunk-7VUKIWF6.cjs → chunk-HIV2GIFP.cjs} +3 -3
- package/dist/{chunk-7VUKIWF6.cjs.map → chunk-HIV2GIFP.cjs.map} +1 -1
- package/dist/{chunk-DBR5JLX3.js → chunk-IHNUMRMU.js} +46 -23
- package/dist/chunk-IHNUMRMU.js.map +1 -0
- package/dist/{chunk-R4HKB3YD.cjs → chunk-IIPBTWQR.cjs} +22 -22
- package/dist/{chunk-R4HKB3YD.cjs.map → chunk-IIPBTWQR.cjs.map} +1 -1
- package/dist/{chunk-TBGSQTJK.js → chunk-IK2WRYVA.js} +6 -6
- package/dist/{chunk-WSAPKKDU.cjs → chunk-JIW5JI3E.cjs} +9 -9
- package/dist/{chunk-WSAPKKDU.cjs.map → chunk-JIW5JI3E.cjs.map} +1 -1
- package/dist/{chunk-MLGGF24T.js → chunk-JKAX5K7C.js} +8 -8
- package/dist/{chunk-UWPT23SA.js → chunk-JO3NPEK6.js} +3 -3
- package/dist/{chunk-HCZJZMU7.js → chunk-JOFGJYIT.js} +3 -3
- package/dist/{chunk-A62C4C4V.cjs → chunk-KI4EGKNV.cjs} +3 -2
- package/dist/chunk-KI4EGKNV.cjs.map +1 -0
- package/dist/{chunk-OB7YDOTK.cjs → chunk-NJADHL2G.cjs} +8 -8
- package/dist/{chunk-OB7YDOTK.cjs.map → chunk-NJADHL2G.cjs.map} +1 -1
- package/dist/{chunk-EYTXD62B.cjs → chunk-OGVIGAMZ.cjs} +232 -45
- package/dist/chunk-OGVIGAMZ.cjs.map +1 -0
- package/dist/{chunk-HX4CJYXL.js → chunk-OT4RV6WY.js} +10 -10
- package/dist/{chunk-Z35QWWJH.cjs → chunk-RJXI25TJ.cjs} +34 -34
- package/dist/{chunk-Z35QWWJH.cjs.map → chunk-RJXI25TJ.cjs.map} +1 -1
- package/dist/{chunk-6TL5XIAW.cjs → chunk-SG25J6LV.cjs} +119 -119
- package/dist/{chunk-6TL5XIAW.cjs.map → chunk-SG25J6LV.cjs.map} +1 -1
- package/dist/{chunk-M4MKQKFF.js → chunk-UBTOOE55.js} +2 -2
- package/dist/{chunk-FEGEC6FY.js → chunk-V5WLIFKN.js} +5660 -5442
- package/dist/chunk-V5WLIFKN.js.map +1 -0
- package/dist/{chunk-EJ3EBYNV.cjs → chunk-VVXUUBML.cjs} +32 -32
- package/dist/{chunk-EJ3EBYNV.cjs.map → chunk-VVXUUBML.cjs.map} +1 -1
- package/dist/{chunk-SRYV4H3M.js → chunk-VZX3SSFL.js} +3 -3
- package/dist/{chunk-5CDPRCPS.cjs → chunk-WVZHCDB5.cjs} +23 -23
- package/dist/{chunk-5CDPRCPS.cjs.map → chunk-WVZHCDB5.cjs.map} +1 -1
- package/dist/{chunk-VDJF4ZAU.js → chunk-XG7FLAKQ.js} +3 -3
- package/dist/{chunk-SZMA65OW.js → chunk-XURRAFIB.js} +2 -2
- package/dist/{chunk-RG2WRBPT.js → chunk-Z3ADCYC5.js} +2 -2
- package/dist/{chunk-2AASLY6D.js → chunk-ZIQMVAGA.js} +3 -3
- package/dist/{chunk-4GPWLGKT.js → chunk-ZRILYK5U.js} +2 -1
- package/dist/components/calendar.cjs +4 -4
- package/dist/components/calendar.js +3 -3
- package/dist/components/case-studies/index.cjs +17 -17
- package/dist/components/case-studies/index.js +7 -7
- package/dist/components/chat/approval-batch-message.d.ts +2 -2
- package/dist/components/chat/approval-batch-message.d.ts.map +1 -1
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/deleted-data-card.d.ts +27 -0
- package/dist/components/chat/entity-cards/deleted-data-card.d.ts.map +1 -0
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-card-item.d.ts +3 -0
- package/dist/components/chat/hooks/use-chat-card-item.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +13 -7
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +12 -6
- package/dist/components/chat/remark-mention-chips.d.ts +5 -2
- package/dist/components/chat/remark-mention-chips.d.ts.map +1 -1
- package/dist/components/chat/remark-strip-citations.d.ts +22 -0
- package/dist/components/chat/remark-strip-citations.d.ts.map +1 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/stream/message-mutations.d.ts +49 -5
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
- package/dist/components/chat/types/component.types.d.ts +7 -7
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +11 -4
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +4 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/discuss-ref-prompt.d.ts +48 -0
- package/dist/components/chat/utils/discuss-ref-prompt.d.ts.map +1 -0
- package/dist/components/chat/utils/index.d.ts +2 -0
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +5 -3
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +63 -0
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts.map +1 -0
- package/dist/components/chat/utils/source-row-cta.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/contact/index.js +7 -7
- package/dist/components/docs/index.cjs +10 -10
- package/dist/components/docs/index.js +9 -9
- package/dist/components/embeds/index.cjs +8 -8
- package/dist/components/embeds/index.js +7 -7
- package/dist/components/faq/index.cjs +9 -9
- package/dist/components/faq/index.js +8 -8
- package/dist/components/features/index.cjs +7 -7
- package/dist/components/features/index.js +6 -6
- package/dist/components/help-center-pages/index.cjs +28 -28
- package/dist/components/help-center-pages/index.js +19 -19
- package/dist/components/icons/index.cjs +3 -3
- package/dist/components/icons/index.js +2 -2
- package/dist/components/index.cjs +253 -247
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +23 -17
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +92 -92
- package/dist/components/meeting-scheduler/index.js +9 -9
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +11 -11
- package/dist/components/onboarding-guides/index.js +10 -10
- package/dist/components/related-content/index.cjs +9 -9
- package/dist/components/related-content/index.js +8 -8
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts +11 -3
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts.map +1 -1
- package/dist/components/shared/doc-search/use-doc-search.d.ts +7 -0
- package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +11 -11
- package/dist/components/tickets/index.js +10 -10
- package/dist/components/ui/actions-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +70 -70
- package/dist/components/ui/file-manager/index.js +3 -3
- package/dist/components/ui/index.cjs +13 -7
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +12 -6
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
- package/dist/components/ui/overlay-open-registry.d.ts +50 -0
- package/dist/components/ui/overlay-open-registry.d.ts.map +1 -0
- package/dist/components/ui/portal-container.d.ts +24 -0
- package/dist/components/ui/portal-container.d.ts.map +1 -1
- package/dist/hooks/index.cjs +4 -4
- package/dist/hooks/index.js +3 -3
- package/dist/index.cjs +20 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -14
- package/dist/utils/anchor-id.d.ts +32 -0
- package/dist/utils/anchor-id.d.ts.map +1 -0
- package/dist/utils/content-href.d.ts +27 -6
- package/dist/utils/content-href.d.ts.map +1 -1
- package/dist/utils/index.cjs +213 -152
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +212 -153
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/same-page-hash-nav.d.ts +17 -6
- package/dist/utils/same-page-hash-nav.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/chat-protocol/decode.ts +43 -0
- package/src/chat-protocol/frames.ts +27 -0
- package/src/components/chat/__tests__/remark-mention-chips.test.ts +67 -0
- package/src/components/chat/approval-batch-message.tsx +2 -2
- package/src/components/chat/chat-message-enhanced.tsx +5 -3
- package/src/components/chat/chat-message-list.tsx +49 -5
- package/src/components/chat/embeddable-chat.tsx +19 -2
- package/src/components/chat/entity-cards/deleted-data-card.tsx +61 -0
- package/src/components/chat/entity-cards/dispatch.tsx +205 -102
- package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +1 -1
- package/src/components/chat/hooks/use-chat-card-item.ts +15 -1
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +28 -4
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +57 -32
- package/src/components/chat/remark-mention-chips.ts +6 -3
- package/src/components/chat/remark-strip-citations.ts +39 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer-approval-batch.test.ts +196 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +67 -2
- package/src/components/chat/stream/message-mutations.ts +170 -40
- package/src/components/chat/types/component.types.ts +9 -7
- package/src/components/chat/types/message.types.ts +14 -4
- package/src/components/chat/types/unified-chat-state.types.ts +4 -1
- package/src/components/chat/utils/__tests__/discuss-ref-prompt.test.ts +67 -0
- package/src/components/chat/utils/__tests__/resolve-fetched-card-href.test.ts +156 -0
- package/src/components/chat/utils/discuss-ref-prompt.ts +63 -0
- package/src/components/chat/utils/index.ts +16 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +24 -29
- package/src/components/chat/utils/resolve-fetched-card-href.ts +104 -0
- package/src/components/chat/utils/source-row-cta.ts +5 -1
- package/src/components/shared/doc-search/__tests__/resolve-search-result-action.test.ts +145 -0
- package/src/components/shared/doc-search/resolve-search-result-action.ts +46 -10
- package/src/components/shared/doc-search/use-doc-search.ts +50 -14
- package/src/components/ui/__tests__/overlay-open-registry.test.tsx +87 -0
- package/src/components/ui/actions-menu.tsx +65 -2
- package/src/components/ui/dropdown-menu.tsx +62 -12
- package/src/components/ui/markdown/base-components.tsx +26 -0
- package/src/components/ui/overlay-open-registry.tsx +99 -0
- package/src/components/ui/portal-container.tsx +31 -0
- package/src/utils/__tests__/anchor-id.test.ts +139 -0
- package/src/utils/__tests__/content-href.test.ts +74 -0
- package/src/utils/__tests__/list-url.test.ts +11 -1
- package/src/utils/anchor-id.ts +106 -0
- package/src/utils/content-href.ts +113 -13
- package/src/utils/index.ts +8 -0
- package/src/utils/list-url.ts +6 -1
- package/src/utils/same-page-hash-nav.ts +28 -9
- package/dist/chunk-A62C4C4V.cjs.map +0 -1
- package/dist/chunk-BTUHMCYW.js.map +0 -1
- package/dist/chunk-DBR5JLX3.js.map +0 -1
- package/dist/chunk-EYTXD62B.cjs.map +0 -1
- package/dist/chunk-FEGEC6FY.js.map +0 -1
- package/dist/chunk-GE3JUTW2.js.map +0 -1
- package/dist/chunk-Q4K3TGNP.cjs.map +0 -1
- package/dist/chunk-RB2EW6IZ.cjs.map +0 -1
- package/dist/chunk-XJVRAKEX.cjs.map +0 -1
- /package/dist/{chunk-JOCFPAS6.js.map → chunk-4NFDSMB5.js.map} +0 -0
- /package/dist/{chunk-WNOGR5AA.js.map → chunk-6LMTBNLN.js.map} +0 -0
- /package/dist/{chunk-6FIDTXYC.js.map → chunk-7AQSXQEL.js.map} +0 -0
- /package/dist/{chunk-LBOWSKGM.js.map → chunk-FI35LJ3L.js.map} +0 -0
- /package/dist/{chunk-CTMLY2GN.js.map → chunk-HGZPH7S5.js.map} +0 -0
- /package/dist/{chunk-TBGSQTJK.js.map → chunk-IK2WRYVA.js.map} +0 -0
- /package/dist/{chunk-MLGGF24T.js.map → chunk-JKAX5K7C.js.map} +0 -0
- /package/dist/{chunk-UWPT23SA.js.map → chunk-JO3NPEK6.js.map} +0 -0
- /package/dist/{chunk-HCZJZMU7.js.map → chunk-JOFGJYIT.js.map} +0 -0
- /package/dist/{chunk-HX4CJYXL.js.map → chunk-OT4RV6WY.js.map} +0 -0
- /package/dist/{chunk-M4MKQKFF.js.map → chunk-UBTOOE55.js.map} +0 -0
- /package/dist/{chunk-SRYV4H3M.js.map → chunk-VZX3SSFL.js.map} +0 -0
- /package/dist/{chunk-VDJF4ZAU.js.map → chunk-XG7FLAKQ.js.map} +0 -0
- /package/dist/{chunk-SZMA65OW.js.map → chunk-XURRAFIB.js.map} +0 -0
- /package/dist/{chunk-RG2WRBPT.js.map → chunk-Z3ADCYC5.js.map} +0 -0
- /package/dist/{chunk-2AASLY6D.js.map → chunk-ZIQMVAGA.js.map} +0 -0
- /package/dist/{chunk-4GPWLGKT.js.map → chunk-ZRILYK5U.js.map} +0 -0
|
@@ -159,6 +159,16 @@ export interface ApprovalBatchData {
|
|
|
159
159
|
executions?: Record<string, ApprovalBatchExecutionState>
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/** Approve/reject handler stamped onto approval segments. MAY resolve a
|
|
163
|
+
* boolean success flag — `false` means the confirm FAILED (expired
|
|
164
|
+
* proposal, network error); batch approve-all loops use it to tick the
|
|
165
|
+
* row's failure cross. `void` (legacy transports) is treated as
|
|
166
|
+
* success. Single source of truth for this signature — component
|
|
167
|
+
* props, accumulator callbacks, and adapters all reference it. */
|
|
168
|
+
export type ApprovalResolutionHandler = (
|
|
169
|
+
requestId?: string,
|
|
170
|
+
) => void | boolean | Promise<void | boolean>
|
|
171
|
+
|
|
162
172
|
// ========== Message Segment Types ==========
|
|
163
173
|
|
|
164
174
|
export type TextSegment = {
|
|
@@ -192,8 +202,8 @@ export type ApprovalRequestSegment = {
|
|
|
192
202
|
/** Display name of the user who resolved the request; baked into the client
|
|
193
203
|
* variant's full-text status pill ("Approved by {name}"). */
|
|
194
204
|
resolvedByName?: string | null
|
|
195
|
-
onApprove?:
|
|
196
|
-
onReject?:
|
|
205
|
+
onApprove?: ApprovalResolutionHandler
|
|
206
|
+
onReject?: ApprovalResolutionHandler
|
|
197
207
|
}
|
|
198
208
|
|
|
199
209
|
export type ApprovalBatchSegment = {
|
|
@@ -202,8 +212,8 @@ export type ApprovalBatchSegment = {
|
|
|
202
212
|
status?: ChatApprovalStatus
|
|
203
213
|
/** Display name of the user who resolved the request; set when the batch is resolved (null/absent for system actions). */
|
|
204
214
|
resolvedByName?: string | null
|
|
205
|
-
onApprove?:
|
|
206
|
-
onReject?:
|
|
215
|
+
onApprove?: ApprovalResolutionHandler
|
|
216
|
+
onReject?: ApprovalResolutionHandler
|
|
207
217
|
}
|
|
208
218
|
|
|
209
219
|
export type ErrorSegment = {
|
|
@@ -274,10 +274,13 @@ export interface UnifiedChatState {
|
|
|
274
274
|
isHydratingHistory?: boolean
|
|
275
275
|
|
|
276
276
|
// ─── Actions ──────────────────────────────────────────────────────────────
|
|
277
|
+
/** Resolution MAY carry a boolean success flag (`false` = the request
|
|
278
|
+
* failed) — consumed by batch approve-all loops to mark failed rows.
|
|
279
|
+
* Callers that only await it are unaffected (`void` = success). */
|
|
277
280
|
sendMessage: (
|
|
278
281
|
text: string,
|
|
279
282
|
options?: UnifiedSendMessageOptions,
|
|
280
|
-
) => Promise<void>
|
|
283
|
+
) => Promise<void | boolean>
|
|
281
284
|
|
|
282
285
|
/** Abort the in-flight stream. No-op when idle. */
|
|
283
286
|
stopMessage: () => void
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildDiscussPrompt } from '../discuss-ref-prompt'
|
|
3
|
+
import { readFetchedCardTitle } from '../resolve-fetched-card-href'
|
|
4
|
+
|
|
5
|
+
const ROADMAP_REF = {
|
|
6
|
+
type: 'roadmap_item',
|
|
7
|
+
id: '86ad3qvv5',
|
|
8
|
+
title: 'Deep Google Workspace tenant management',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('buildDiscussPrompt', () => {
|
|
12
|
+
it('guide/SSE shape — prose only, retrieval rides entityIdFilter', () => {
|
|
13
|
+
expect(buildDiscussPrompt(ROADMAP_REF)).toBe(
|
|
14
|
+
'Tell me more about Deep Google Workspace tenant management',
|
|
15
|
+
)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('Mingo shape — appends the reference the agent has no filter for', () => {
|
|
19
|
+
expect(buildDiscussPrompt(ROADMAP_REF, { includeReference: true })).toBe(
|
|
20
|
+
'Tell me more about Deep Google Workspace tenant management (roadmap_item 86ad3qvv5)',
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('a synthetic ref (title === id) never leaks the id as the subject', () => {
|
|
25
|
+
const synthetic = { type: 'roadmap_item', id: '86ad3qvv5', title: '86ad3qvv5' }
|
|
26
|
+
expect(buildDiscussPrompt(synthetic, { includeReference: true })).toBe(
|
|
27
|
+
'Tell me more about this item (roadmap_item 86ad3qvv5)',
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('missing / blank title falls back to "this item"', () => {
|
|
32
|
+
expect(buildDiscussPrompt({ type: 'blog_post', id: 'b1' })).toBe('Tell me more about this item')
|
|
33
|
+
expect(buildDiscussPrompt({ type: 'blog_post', id: 'b1', title: ' ' })).toBe(
|
|
34
|
+
'Tell me more about this item',
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('no id → no reference suffix', () => {
|
|
39
|
+
expect(buildDiscussPrompt({ type: 'blog_post', id: '', title: 'Hello' }, { includeReference: true })).toBe(
|
|
40
|
+
'Tell me more about Hello',
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('readFetchedCardTitle — what upgrades a synthetic ref before the prompt', () => {
|
|
46
|
+
it('prefers title, then name', () => {
|
|
47
|
+
expect(readFetchedCardTitle({ title: 'Roadmap row' })).toBe('Roadmap row')
|
|
48
|
+
expect(readFetchedCardTitle({ name: 'Named row' })).toBe('Named row')
|
|
49
|
+
expect(readFetchedCardTitle({ title: ' ', name: 'Named row' })).toBe('Named row')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('null for rows with neither (and for non-objects)', () => {
|
|
53
|
+
expect(readFetchedCardTitle({ headline: 'nope' })).toBeNull()
|
|
54
|
+
expect(readFetchedCardTitle(null)).toBeNull()
|
|
55
|
+
expect(readFetchedCardTitle('string')).toBeNull()
|
|
56
|
+
expect(readFetchedCardTitle({ title: 42 })).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('end to end: fetched row title reaches the prompt', () => {
|
|
60
|
+
const synthetic = { type: 'roadmap_item', id: '86ad3qvv5', title: '86ad3qvv5' }
|
|
61
|
+
const row = { id: 'db-9', title: 'Deep Google Workspace tenant management' }
|
|
62
|
+
const enriched = { ...synthetic, title: readFetchedCardTitle(row) ?? synthetic.title }
|
|
63
|
+
expect(buildDiscussPrompt(enriched, { includeReference: true })).toBe(
|
|
64
|
+
'Tell me more about Deep Google Workspace tenant management (roadmap_item 86ad3qvv5)',
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { makeComposeContentUrl } from '../../../../utils/content-href'
|
|
3
|
+
import { resolveFetchedCardHref } from '../resolve-fetched-card-href'
|
|
4
|
+
|
|
5
|
+
const HUB = 'https://www.flamingo.run'
|
|
6
|
+
|
|
7
|
+
/** The OpenFrame Help Center seam, verbatim in shape from
|
|
8
|
+
* `openframe-oss-frontend/src/app/(app)/help-center/help-center-content-href.ts`. */
|
|
9
|
+
const compose = makeComposeContentUrl({
|
|
10
|
+
hostedTypes: new Set(['onboarding_guide', 'product_release']),
|
|
11
|
+
contentOrigin: HUB,
|
|
12
|
+
suffixes: {
|
|
13
|
+
onboarding_guide: 'help-center/onboarding-guides',
|
|
14
|
+
product_release: 'help-center/releases',
|
|
15
|
+
blog_post: 'blog',
|
|
16
|
+
case_study: 'case-studies',
|
|
17
|
+
},
|
|
18
|
+
overrides: {
|
|
19
|
+
roadmap_item: (id) => ({
|
|
20
|
+
href: `/help-center/roadmap?search=${encodeURIComponent(id)}`,
|
|
21
|
+
targetPlatform: null,
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('resolveFetchedCardHref — Mingo cards with no ref metadata', () => {
|
|
27
|
+
it('override type deep-links on the MARKER id, not the fetched row id', () => {
|
|
28
|
+
// The `[card://roadmap_item:86ad3qvv5]` id is the ClickUp task id the
|
|
29
|
+
// roadmap list filters by; the fetched row carries its own db id.
|
|
30
|
+
expect(
|
|
31
|
+
resolveFetchedCardHref({
|
|
32
|
+
contentRefType: 'roadmap_item',
|
|
33
|
+
id: '86ad3qvv5',
|
|
34
|
+
item: { id: 'db-uuid-9', title: 'Deep Google Workspace tenant management' },
|
|
35
|
+
composeContentUrl: compose,
|
|
36
|
+
}),
|
|
37
|
+
).toEqual({ href: '/help-center/roadmap?search=86ad3qvv5', targetPlatform: null })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('hosted type uses the fetched row slug for its in-app detail route', () => {
|
|
41
|
+
expect(
|
|
42
|
+
resolveFetchedCardHref({
|
|
43
|
+
contentRefType: 'product_release',
|
|
44
|
+
id: 'row-uuid-1',
|
|
45
|
+
item: { id: 'row-uuid-1', slug: 'v1-2-0' },
|
|
46
|
+
composeContentUrl: compose,
|
|
47
|
+
}),
|
|
48
|
+
).toEqual({ href: '/help-center/releases/v1-2-0', targetPlatform: null })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('hosted type with no slug falls back to the marker id', () => {
|
|
52
|
+
expect(
|
|
53
|
+
resolveFetchedCardHref({
|
|
54
|
+
contentRefType: 'onboarding_guide',
|
|
55
|
+
id: 'guide-7',
|
|
56
|
+
item: { id: 'guide-7' },
|
|
57
|
+
composeContentUrl: compose,
|
|
58
|
+
}),
|
|
59
|
+
).toEqual({ href: '/help-center/onboarding-guides/guide-7', targetPlatform: null })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('non-hosted type resolves to the content hub', () => {
|
|
63
|
+
expect(
|
|
64
|
+
resolveFetchedCardHref({
|
|
65
|
+
contentRefType: 'case_study',
|
|
66
|
+
id: 'cs-1',
|
|
67
|
+
item: { id: 'cs-1', slug: 'acme-migration' },
|
|
68
|
+
composeContentUrl: compose,
|
|
69
|
+
}),
|
|
70
|
+
).toEqual({ href: `${HUB}/case-studies/acme-migration`, targetPlatform: null })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('legacy rail-vocab alias resolves to the SAME url as the canonical type', () => {
|
|
74
|
+
// The card registry keys blog entries by `blog_post_existing` (what
|
|
75
|
+
// `buildListUrl` wants). Both spellings must produce one url — the reported
|
|
76
|
+
// split was `/blog/what-is-mdr` vs `/blog_post_existing/bitwarden-…`.
|
|
77
|
+
const canonical = resolveFetchedCardHref({
|
|
78
|
+
contentRefType: 'blog_post',
|
|
79
|
+
id: 'p1',
|
|
80
|
+
item: { id: 'p1', slug: 'bitwarden-review-for-msps' },
|
|
81
|
+
composeContentUrl: compose,
|
|
82
|
+
})
|
|
83
|
+
const alias = resolveFetchedCardHref({
|
|
84
|
+
contentRefType: 'blog_post_existing',
|
|
85
|
+
id: 'p1',
|
|
86
|
+
item: { id: 'p1', slug: 'bitwarden-review-for-msps' },
|
|
87
|
+
composeContentUrl: compose,
|
|
88
|
+
})
|
|
89
|
+
expect(alias).toEqual(canonical)
|
|
90
|
+
expect(alias).toEqual({
|
|
91
|
+
href: `${HUB}/blog/bitwarden-review-for-msps`,
|
|
92
|
+
targetPlatform: null,
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('no seam wired → null (card stays unlinked, pre-existing behavior)', () => {
|
|
97
|
+
expect(
|
|
98
|
+
resolveFetchedCardHref({
|
|
99
|
+
contentRefType: 'roadmap_item',
|
|
100
|
+
id: '86ad3qvv5',
|
|
101
|
+
item: { id: '86ad3qvv5' },
|
|
102
|
+
}),
|
|
103
|
+
).toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('guards a missing type / id / malformed item', () => {
|
|
107
|
+
expect(
|
|
108
|
+
resolveFetchedCardHref({ contentRefType: '', id: 'x', item: {}, composeContentUrl: compose }),
|
|
109
|
+
).toBeNull()
|
|
110
|
+
expect(
|
|
111
|
+
resolveFetchedCardHref({
|
|
112
|
+
contentRefType: 'case_study',
|
|
113
|
+
id: '',
|
|
114
|
+
item: {},
|
|
115
|
+
composeContentUrl: compose,
|
|
116
|
+
}),
|
|
117
|
+
).toBeNull()
|
|
118
|
+
// A non-object item (or a non-string slug) must not throw — it just loses
|
|
119
|
+
// the slug hint and falls back to the marker id.
|
|
120
|
+
expect(
|
|
121
|
+
resolveFetchedCardHref({
|
|
122
|
+
contentRefType: 'case_study',
|
|
123
|
+
id: 'cs-2',
|
|
124
|
+
item: { slug: 42 },
|
|
125
|
+
composeContentUrl: compose,
|
|
126
|
+
}),
|
|
127
|
+
).toEqual({ href: `${HUB}/case-studies/cs-2`, targetPlatform: null })
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('makeComposeContentUrl — the new `slug` hint', () => {
|
|
132
|
+
it('externalUrl still wins over the hint (SSE path unchanged)', () => {
|
|
133
|
+
expect(
|
|
134
|
+
compose({
|
|
135
|
+
type: 'product_release',
|
|
136
|
+
identifier: 'row-uuid-1',
|
|
137
|
+
slug: 'from-the-row',
|
|
138
|
+
externalUrl: `${HUB}/releases/from-the-url`,
|
|
139
|
+
}),
|
|
140
|
+
).toEqual({ href: '/help-center/releases/from-the-url', targetPlatform: null })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('applies to non-hosted types too — hub detail routes are slug-based', () => {
|
|
144
|
+
expect(compose({ type: 'blog_post', identifier: 'id-1', slug: 'hello-world' })).toEqual({
|
|
145
|
+
href: `${HUB}/blog/hello-world`,
|
|
146
|
+
targetPlatform: null,
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('no hint → identifier, unchanged for page-view and SSE callers', () => {
|
|
151
|
+
expect(compose({ type: 'blog_post', identifier: 'hello-world' })).toEqual({
|
|
152
|
+
href: `${HUB}/blog/hello-world`,
|
|
153
|
+
targetPlatform: null,
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place the "Ask Mingo" (discuss) prompt for an inline entity card is
|
|
3
|
+
* composed — shared by every transport so the affordance reads identically
|
|
4
|
+
* wherever it is clicked.
|
|
5
|
+
*
|
|
6
|
+
* ## Why the two transports still differ underneath
|
|
7
|
+
*
|
|
8
|
+
* The prose is UX only. What narrows retrieval differs:
|
|
9
|
+
*
|
|
10
|
+
* - **SSE / guide** — `sendMessage(prompt, { commandOverride: { entityIdFilter:
|
|
11
|
+
* { tableId, id } } })`. Retrieval is strictly primary-key-driven; the
|
|
12
|
+
* sentence is decoration.
|
|
13
|
+
* - **Mingo / NATS** — the agent backend has no entity-id-filtered retrieval
|
|
14
|
+
* (`ContextItemType` covers DEVICE / SCRIPT / TICKET / ORGANIZATION / USER /
|
|
15
|
+
* KB_ARTICLE / POLICY / QUERY / SCHEDULED_SCRIPT — no content types), so the
|
|
16
|
+
* prompt is ALL the agent gets. Hence `includeReference`, which appends the
|
|
17
|
+
* type + id so the agent can still identify the exact row by text.
|
|
18
|
+
*
|
|
19
|
+
* When the backend grows an id-filter for the agent transport, the Mingo callers
|
|
20
|
+
* switch to it and drop `includeReference` — the prose stays put.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { sanitizeTitleForChat } from './slash-dispatch-utils'
|
|
24
|
+
|
|
25
|
+
export interface DiscussPromptRef {
|
|
26
|
+
/** documentType, e.g. `'roadmap_item'`. */
|
|
27
|
+
type: string
|
|
28
|
+
/** Primary key — the id from the `[card://type:id]` marker. */
|
|
29
|
+
id: string
|
|
30
|
+
/** Display title. May be the bare id on a synthetic ref (Mingo). */
|
|
31
|
+
title?: string | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BuildDiscussPromptOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Append `(<type> <id>)` so a transport WITHOUT structured entity filtering
|
|
37
|
+
* still points the agent at the exact row. Default `false` — the SSE path
|
|
38
|
+
* sends `entityIdFilter` and must keep the sentence clean.
|
|
39
|
+
*/
|
|
40
|
+
includeReference?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `Tell me more about <title>` — plus `(<type> <id>)` when
|
|
45
|
+
* `includeReference` is set and the title alone is ambiguous.
|
|
46
|
+
*
|
|
47
|
+
* Falls back to `this item` when the ref has no usable title, matching the
|
|
48
|
+
* long-standing SSE behavior. A title that is just the id (the synthetic ref a
|
|
49
|
+
* Mingo `[card://…]` marker produces before its row loads) is treated as
|
|
50
|
+
* missing, so the prompt never degrades to `Tell me more about 86ad3qvv5`.
|
|
51
|
+
*/
|
|
52
|
+
export function buildDiscussPrompt(
|
|
53
|
+
reference: DiscussPromptRef,
|
|
54
|
+
options: BuildDiscussPromptOptions = {},
|
|
55
|
+
): string {
|
|
56
|
+
const id = (reference.id ?? '').trim()
|
|
57
|
+
const rawTitle = (reference.title ?? '').trim()
|
|
58
|
+
const usableTitle = rawTitle && rawTitle !== id ? sanitizeTitleForChat(rawTitle) : ''
|
|
59
|
+
const prompt = `Tell me more about ${usableTitle || 'this item'}`
|
|
60
|
+
if (!options.includeReference || !id) return prompt
|
|
61
|
+
// Only worth the noise when the reference adds something the title doesn't.
|
|
62
|
+
return `${prompt} (${reference.type} ${id})`
|
|
63
|
+
}
|
|
@@ -196,4 +196,20 @@ export {
|
|
|
196
196
|
resolveSourceIcon,
|
|
197
197
|
} from './source-row-cta'
|
|
198
198
|
|
|
199
|
+
// Post-fetch enrichment for the synthetic ChatRef a `[card://type:id]` marker
|
|
200
|
+
// produces when the transport ships no refs metadata (Mingo/NATS).
|
|
201
|
+
export {
|
|
202
|
+
resolveFetchedCardHref,
|
|
203
|
+
readFetchedCardTitle,
|
|
204
|
+
type FetchedCardHrefInput,
|
|
205
|
+
} from './resolve-fetched-card-href'
|
|
206
|
+
|
|
207
|
+
// The ONE "Ask Mingo" prompt builder — shared by the SSE adapter, the NATS
|
|
208
|
+
// adapter, and hosts that inject their own Mingo state (openframe-frontend).
|
|
209
|
+
export {
|
|
210
|
+
buildDiscussPrompt,
|
|
211
|
+
type DiscussPromptRef,
|
|
212
|
+
type BuildDiscussPromptOptions,
|
|
213
|
+
} from './discuss-ref-prompt'
|
|
214
|
+
|
|
199
215
|
export * from './scripted-stream'
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
ApprovalRequestSegment,
|
|
15
15
|
ApprovalBatchSegment,
|
|
16
16
|
ApprovalBatchExecutionState,
|
|
17
|
+
ApprovalResolutionHandler,
|
|
17
18
|
ContextCompactionSegment,
|
|
18
19
|
ErrorSegment,
|
|
19
20
|
PendingApproval,
|
|
@@ -22,10 +23,17 @@ import type {
|
|
|
22
23
|
ChatApprovalStatus,
|
|
23
24
|
ExecutingToolState,
|
|
24
25
|
} from '../types'
|
|
26
|
+
import {
|
|
27
|
+
applyApprovalStatusToSegment,
|
|
28
|
+
nextBatchExecution,
|
|
29
|
+
withBatchExecution,
|
|
30
|
+
} from '../stream/message-mutations'
|
|
25
31
|
|
|
26
32
|
export interface AccumulatorCallbacks {
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
/** See `ApprovalResolutionHandler` (message.types) — the SSOT for the
|
|
34
|
+
* approve/reject signature incl. the boolean failure flag. */
|
|
35
|
+
onApprove?: ApprovalResolutionHandler
|
|
36
|
+
onReject?: ApprovalResolutionHandler
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/**
|
|
@@ -244,25 +252,13 @@ export class MessageSegmentAccumulator {
|
|
|
244
252
|
if (!hasCall) return seg
|
|
245
253
|
|
|
246
254
|
const prev: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return seg
|
|
252
|
-
}
|
|
253
|
-
const next: ApprovalBatchExecutionState =
|
|
254
|
-
toolData.type === 'EXECUTED_TOOL'
|
|
255
|
-
? { status: 'done', result: toolData.result, success: toolData.success }
|
|
256
|
-
: { status: 'executing', result: prev?.result, success: prev?.success }
|
|
257
|
-
|
|
255
|
+
// Shared rule (`nextBatchExecution` from message-mutations): the
|
|
256
|
+
// never-downgrade guard + EXECUTED/EXECUTING ternary — one
|
|
257
|
+
// implementation with the message-array projection.
|
|
258
|
+
const next = nextBatchExecution(prev, toolData)
|
|
258
259
|
matched = true
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
data: {
|
|
262
|
-
...seg.data,
|
|
263
|
-
executions: { ...(seg.data.executions ?? {}), [execId]: next },
|
|
264
|
-
},
|
|
265
|
-
}
|
|
260
|
+
if (next === null) return seg
|
|
261
|
+
return withBatchExecution(seg, execId, next)
|
|
266
262
|
})
|
|
267
263
|
return matched
|
|
268
264
|
}
|
|
@@ -429,15 +425,14 @@ export class MessageSegmentAccumulator {
|
|
|
429
425
|
* resolved card shows "by {name}"; omit it to leave any existing value untouched.
|
|
430
426
|
*/
|
|
431
427
|
updateApprovalStatus(requestId: string, status: ChatApprovalStatus, resolvedByName?: string | null): MessageSegment[] {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
})
|
|
428
|
+
// ONE rule, two containers: `applyApprovalStatusToSegment` is the
|
|
429
|
+
// same predicate the message-array projection uses
|
|
430
|
+
// (`projectApprovalResolutionToMessages`) — anchor status flip AND
|
|
431
|
+
// batch-row execution tick, so this flat-list path can never drift
|
|
432
|
+
// from the projection again (it previously missed the row case).
|
|
433
|
+
this.segments = this.segments.map((segment) =>
|
|
434
|
+
applyApprovalStatusToSegment(segment, requestId, status, resolvedByName),
|
|
435
|
+
)
|
|
441
436
|
return this.getSegments()
|
|
442
437
|
}
|
|
443
438
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-fetch href fallback for an inline chat card whose `ChatRef` carried no
|
|
3
|
+
* `externalUrl`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a fetch-mode card can arrive without a URL
|
|
6
|
+
*
|
|
7
|
+
* The two chat transports deliver entity references differently:
|
|
8
|
+
*
|
|
9
|
+
* - **SSE / guide transport** — the server ships a `refs` metadata frame, so
|
|
10
|
+
* each `[card://type:id]` marker expands into a FULL `ChatRef`
|
|
11
|
+
* (`title` / `url` / `targetPlatform` / `sourceRepo`). `resolveSourceRowCTA`
|
|
12
|
+
* then routes that `externalUrl` through the host's `composeContentUrl`
|
|
13
|
+
* seam and the card is clickable.
|
|
14
|
+
* - **Mingo / NATS transport** (`messageData: [{ type: 'GUIDE' | 'TEXT', text }]`)
|
|
15
|
+
* — the body carries bare `[card://type:id]` markers and NOTHING else.
|
|
16
|
+
* `chat-message-enhanced.tsx` synthesizes a minimal `{ type, id, title: id,
|
|
17
|
+
* url: null }` ref so fetch-mode cards can still self-fetch their row, but
|
|
18
|
+
* `resolveSourceRowCTA` has no `externalUrl`, no `path` and no
|
|
19
|
+
* `targetPlatform` to work with → `href: null` → the card renders with its
|
|
20
|
+
* real title and description but goes nowhere when clicked.
|
|
21
|
+
*
|
|
22
|
+
* This resolver closes that gap WITHOUT a second URL vocabulary: it calls the
|
|
23
|
+
* exact same `composeContentUrl` seam the page cards (`resolveContentHref`) and
|
|
24
|
+
* the SSE cards (`resolveSourceRowCTA`) use, so one host config —
|
|
25
|
+
* `hostedTypes` / `suffixes` / `overrides` — governs all three.
|
|
26
|
+
*
|
|
27
|
+
* `identifier` stays the marker's primary key (that is what `overrides` such as
|
|
28
|
+
* roadmap's `?search=<task id>` deep-link on); the row's `slug` rides the
|
|
29
|
+
* separate `slug` hint for hosted types whose detail route is slug-based.
|
|
30
|
+
*
|
|
31
|
+
* Pure — no React, no DOM. The caller owns `safeHref` validation.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { ComposeContentUrl } from '../../../utils/content-href'
|
|
35
|
+
import { canonicalContentRefType } from '../../../utils/list-url'
|
|
36
|
+
|
|
37
|
+
export interface FetchedCardHrefInput {
|
|
38
|
+
/** Canonical `documentType` for the seam (registry `contentRefType`). */
|
|
39
|
+
contentRefType: string
|
|
40
|
+
/** The marker's primary key — `ChatRef.id`, NOT the fetched row's id. */
|
|
41
|
+
id: string
|
|
42
|
+
/** The row the card fetched. Shape is per-type and opaque here; only
|
|
43
|
+
* `slug` and `platforms` are read, both defensively. */
|
|
44
|
+
item: unknown
|
|
45
|
+
/** Host seam. Absent (unwired runtime) → no fallback, same as today. */
|
|
46
|
+
composeContentUrl?: ComposeContentUrl
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Read `item.slug` when it is a non-empty string. */
|
|
50
|
+
function readSlug(item: unknown): string | null {
|
|
51
|
+
if (!item || typeof item !== 'object') return null
|
|
52
|
+
const slug = (item as { slug?: unknown }).slug
|
|
53
|
+
return typeof slug === 'string' && slug.trim() ? slug.trim() : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Human title of a fetched row — `title`, else `name`, else null.
|
|
58
|
+
*
|
|
59
|
+
* A Mingo synthetic ref carries `title: <the marker id>` because that is all
|
|
60
|
+
* the transport shipped. Everything downstream that prints or sends the title
|
|
61
|
+
* (the "Ask Mingo" prompt, host renderers falling back to `ref.title`) would
|
|
62
|
+
* otherwise say `86ad3qvv5`. Once the row loads, this is the real title.
|
|
63
|
+
*/
|
|
64
|
+
export function readFetchedCardTitle(item: unknown): string | null {
|
|
65
|
+
if (!item || typeof item !== 'object') return null
|
|
66
|
+
for (const key of ['title', 'name'] as const) {
|
|
67
|
+
const value = (item as Record<string, unknown>)[key]
|
|
68
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Read `item.platforms` when it is an array — the hub composer keys its
|
|
74
|
+
* cross-platform topology off it; the embedder default ignores it. */
|
|
75
|
+
function readPlatforms(item: unknown): Array<{ name?: string }> | undefined {
|
|
76
|
+
if (!item || typeof item !== 'object') return undefined
|
|
77
|
+
const platforms = (item as { platforms?: unknown }).platforms
|
|
78
|
+
return Array.isArray(platforms) ? (platforms as Array<{ name?: string }>) : undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve `{href, targetPlatform}` for a freshly fetched card row, or `null`
|
|
83
|
+
* when the host wired no seam (the caller then keeps the card unlinked, which
|
|
84
|
+
* is the pre-existing behavior).
|
|
85
|
+
*/
|
|
86
|
+
export function resolveFetchedCardHref({
|
|
87
|
+
contentRefType,
|
|
88
|
+
id,
|
|
89
|
+
item,
|
|
90
|
+
composeContentUrl,
|
|
91
|
+
}: FetchedCardHrefInput): { href: string; targetPlatform: string | null } | null {
|
|
92
|
+
if (!composeContentUrl || !contentRefType || !id) return null
|
|
93
|
+
return composeContentUrl({
|
|
94
|
+
// The card registry keys some entries by the legacy rail vocabulary
|
|
95
|
+
// (`blog_post_existing`) because that is what `buildListUrl` expects.
|
|
96
|
+
// The seam speaks the CANONICAL vocabulary — a host's `hostedTypes` /
|
|
97
|
+
// `overrides` are written against `blog_post`, so an un-aliased type
|
|
98
|
+
// misses them all and lands on `<origin>/blog_post_existing/<slug>`.
|
|
99
|
+
type: canonicalContentRefType(contentRefType),
|
|
100
|
+
identifier: id,
|
|
101
|
+
slug: readSlug(item),
|
|
102
|
+
platforms: readPlatforms(item),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
@@ -29,6 +29,7 @@ import { getBaseUrl } from '../../../utils/cn'
|
|
|
29
29
|
import { safeHref } from './compact-card-classes'
|
|
30
30
|
import type { ChatRef } from '../chat-ref.types'
|
|
31
31
|
import type { ComposeContentUrl } from '../../../utils/content-href'
|
|
32
|
+
import { canonicalContentRefType } from '../../../utils/list-url'
|
|
32
33
|
|
|
33
34
|
/** Path sanitization — keep alphanumerics, slash, dash, dot, underscore;
|
|
34
35
|
* strip everything else. Defends against a hostile mapper returning a
|
|
@@ -233,7 +234,10 @@ export function resolveSourceRowCTA(
|
|
|
233
234
|
// from the externalUrl) and everything else to the externalUrl verbatim —
|
|
234
235
|
// the SAME resolver the page-view cards use, so chat + page links match.
|
|
235
236
|
const composed = ctx.composeContentUrl({
|
|
236
|
-
|
|
237
|
+
// Canonical vocabulary only — a rail-vocab alias would miss the host's
|
|
238
|
+
// `hostedTypes` / `overrides` and fall through to the seam's default
|
|
239
|
+
// `<origin>/<type>/<id>` branch. Same canonicalizer `buildListUrl` uses.
|
|
240
|
+
type: canonicalContentRefType(row.documentType),
|
|
237
241
|
identifier: idValue,
|
|
238
242
|
externalUrl: row.externalUrl,
|
|
239
243
|
targetPlatform: row.targetPlatform ?? null,
|