@flamingo-stack/openframe-frontend-core 0.0.510 → 0.0.511
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/{chunk-GE3JUTW2.js → chunk-27OKD7ZV.js} +44 -14
- package/dist/chunk-27OKD7ZV.js.map +1 -0
- package/dist/{chunk-2RLI5XVI.cjs → chunk-2CE725NN.cjs} +16 -16
- package/dist/{chunk-2RLI5XVI.cjs.map → chunk-2CE725NN.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-RJ3D3PE2.cjs → chunk-4SG55XVA.cjs} +9 -9
- package/dist/{chunk-RJ3D3PE2.cjs.map → chunk-4SG55XVA.cjs.map} +1 -1
- 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-6FIDTXYC.js → chunk-7AQSXQEL.js} +3 -3
- package/dist/{chunk-6TL5XIAW.cjs → chunk-7YV67K3I.cjs} +119 -119
- package/dist/{chunk-6TL5XIAW.cjs.map → chunk-7YV67K3I.cjs.map} +1 -1
- package/dist/{chunk-5CDPRCPS.cjs → chunk-A5YZUV6B.cjs} +23 -23
- package/dist/{chunk-5CDPRCPS.cjs.map → chunk-A5YZUV6B.cjs.map} +1 -1
- package/dist/{chunk-WSAPKKDU.cjs → chunk-C4EUXZSA.cjs} +9 -9
- package/dist/{chunk-WSAPKKDU.cjs.map → chunk-C4EUXZSA.cjs.map} +1 -1
- package/dist/{chunk-5AZZQQYS.cjs → chunk-DPLXHG7H.cjs} +40 -40
- package/dist/{chunk-5AZZQQYS.cjs.map → chunk-DPLXHG7H.cjs.map} +1 -1
- package/dist/{chunk-BTUHMCYW.js → chunk-FS3YSNSU.js} +193 -6
- package/dist/chunk-FS3YSNSU.js.map +1 -0
- package/dist/{chunk-RB2EW6IZ.cjs → chunk-GHXTF35X.cjs} +3231 -3165
- package/dist/chunk-GHXTF35X.cjs.map +1 -0
- 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-R4HKB3YD.cjs → chunk-IIPBTWQR.cjs} +22 -22
- package/dist/{chunk-R4HKB3YD.cjs.map → chunk-IIPBTWQR.cjs.map} +1 -1
- package/dist/{chunk-JMBIGH4H.cjs → chunk-IUZMWMVX.cjs} +83 -83
- package/dist/{chunk-JMBIGH4H.cjs.map → chunk-IUZMWMVX.cjs.map} +1 -1
- package/dist/{chunk-XJVRAKEX.cjs → chunk-IXCBVJRU.cjs} +56 -33
- package/dist/chunk-IXCBVJRU.cjs.map +1 -0
- package/dist/{chunk-TBGSQTJK.js → chunk-J4VB4EHW.js} +6 -6
- package/dist/{chunk-UWPT23SA.js → chunk-JXMNLH7F.js} +3 -3
- package/dist/{chunk-A62C4C4V.cjs → chunk-KI4EGKNV.cjs} +3 -2
- package/dist/chunk-KI4EGKNV.cjs.map +1 -0
- package/dist/{chunk-RHQ7BJKK.cjs → chunk-LEWQRAZ4.cjs} +73 -73
- package/dist/{chunk-RHQ7BJKK.cjs.map → chunk-LEWQRAZ4.cjs.map} +1 -1
- package/dist/{chunk-DBR5JLX3.js → chunk-LVAJK2U2.js} +46 -23
- package/dist/chunk-LVAJK2U2.js.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-HCZJZMU7.js → chunk-NKA3O4L6.js} +3 -3
- package/dist/{chunk-SRYV4H3M.js → chunk-NNWSGOUP.js} +3 -3
- package/dist/{chunk-EYTXD62B.cjs → chunk-OGVIGAMZ.cjs} +232 -45
- package/dist/chunk-OGVIGAMZ.cjs.map +1 -0
- package/dist/{chunk-EJ3EBYNV.cjs → chunk-OMN7A6RP.cjs} +32 -32
- package/dist/{chunk-EJ3EBYNV.cjs.map → chunk-OMN7A6RP.cjs.map} +1 -1
- package/dist/{chunk-WNOGR5AA.js → chunk-PRMR6WKC.js} +3 -3
- package/dist/{chunk-J2JUAS4C.cjs → chunk-PXQQFRUD.cjs} +11 -11
- package/dist/{chunk-J2JUAS4C.cjs.map → chunk-PXQQFRUD.cjs.map} +1 -1
- package/dist/{chunk-VDJF4ZAU.js → chunk-RU3SCECX.js} +3 -3
- package/dist/{chunk-53JKCFFE.cjs → chunk-RXFBW2EJ.cjs} +12 -12
- package/dist/{chunk-53JKCFFE.cjs.map → chunk-RXFBW2EJ.cjs.map} +1 -1
- package/dist/{chunk-JOCFPAS6.js → chunk-TNZOUHHN.js} +6 -6
- package/dist/{chunk-HX4CJYXL.js → chunk-TR7AMHGB.js} +10 -10
- package/dist/{chunk-MLGGF24T.js → chunk-TYVHM67X.js} +8 -8
- package/dist/{chunk-Z35QWWJH.cjs → chunk-U4WPFX5K.cjs} +34 -34
- package/dist/{chunk-Z35QWWJH.cjs.map → chunk-U4WPFX5K.cjs.map} +1 -1
- package/dist/{chunk-M4MKQKFF.js → chunk-UBTOOE55.js} +2 -2
- package/dist/{chunk-FEGEC6FY.js → chunk-X7AJ3J42.js} +5170 -5104
- package/dist/chunk-X7AJ3J42.js.map +1 -0
- package/dist/{chunk-SZMA65OW.js → chunk-XURRAFIB.js} +2 -2
- package/dist/{chunk-CTMLY2GN.js → chunk-YGS7BLSN.js} +5 -5
- package/dist/{chunk-RG2WRBPT.js → chunk-Z3ADCYC5.js} +2 -2
- package/dist/{chunk-2AASLY6D.js → chunk-ZBQV7WH7.js} +3 -3
- package/dist/{chunk-4GPWLGKT.js → chunk-ZRILYK5U.js} +2 -1
- package/dist/{chunk-LBOWSKGM.js → chunk-ZSIEM7CL.js} +5 -5
- 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/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/dispatch.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/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/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/components/chat/__tests__/remark-mention-chips.test.ts +67 -0
- package/src/components/chat/chat-message-enhanced.tsx +3 -2
- 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/dispatch.tsx +65 -4
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +28 -4
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +5 -3
- package/src/components/chat/remark-mention-chips.ts +6 -3
- 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/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-6FIDTXYC.js.map → chunk-7AQSXQEL.js.map} +0 -0
- /package/dist/{chunk-TBGSQTJK.js.map → chunk-J4VB4EHW.js.map} +0 -0
- /package/dist/{chunk-UWPT23SA.js.map → chunk-JXMNLH7F.js.map} +0 -0
- /package/dist/{chunk-HCZJZMU7.js.map → chunk-NKA3O4L6.js.map} +0 -0
- /package/dist/{chunk-SRYV4H3M.js.map → chunk-NNWSGOUP.js.map} +0 -0
- /package/dist/{chunk-WNOGR5AA.js.map → chunk-PRMR6WKC.js.map} +0 -0
- /package/dist/{chunk-VDJF4ZAU.js.map → chunk-RU3SCECX.js.map} +0 -0
- /package/dist/{chunk-JOCFPAS6.js.map → chunk-TNZOUHHN.js.map} +0 -0
- /package/dist/{chunk-HX4CJYXL.js.map → chunk-TR7AMHGB.js.map} +0 -0
- /package/dist/{chunk-MLGGF24T.js.map → chunk-TYVHM67X.js.map} +0 -0
- /package/dist/{chunk-M4MKQKFF.js.map → chunk-UBTOOE55.js.map} +0 -0
- /package/dist/{chunk-SZMA65OW.js.map → chunk-XURRAFIB.js.map} +0 -0
- /package/dist/{chunk-CTMLY2GN.js.map → chunk-YGS7BLSN.js.map} +0 -0
- /package/dist/{chunk-RG2WRBPT.js.map → chunk-Z3ADCYC5.js.map} +0 -0
- /package/dist/{chunk-2AASLY6D.js.map → chunk-ZBQV7WH7.js.map} +0 -0
- /package/dist/{chunk-4GPWLGKT.js.map → chunk-ZRILYK5U.js.map} +0 -0
- /package/dist/{chunk-LBOWSKGM.js.map → chunk-ZSIEM7CL.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-url.d.ts","sourceRoot":"","sources":["../../src/utils/list-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAWH;;;;iEAIiE;AACjE,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"list-url.d.ts","sourceRoot":"","sources":["../../src/utils/list-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAWH;;;;iEAIiE;AACjE,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAOtE;AA6BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,SAAK,GAAG,MAAM,GAAG,IAAI,CAa5F"}
|
|
@@ -15,12 +15,23 @@ export declare const HUB_HEADER_OFFSET_PX = 80;
|
|
|
15
15
|
*/
|
|
16
16
|
export declare function normalizeHashFragment(hash: string): string;
|
|
17
17
|
/**
|
|
18
|
-
* Resolve the DOM element a hash fragment id points at
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* Resolve the DOM element a hash fragment id points at — THE hash resolver;
|
|
19
|
+
* `navigateSamePageHash`, `useScrollToHash` and the markdown link renderer
|
|
20
|
+
* all go through this one, so a fragment that resolves for a deep link
|
|
21
|
+
* resolves for an in-body click too.
|
|
22
|
+
*
|
|
23
|
+
* Three passes, cheapest first:
|
|
24
|
+
* 1. the raw id (ordinary anchors — the overwhelming common case);
|
|
25
|
+
* 2. the percent-decoded form — href composers like `buildTicketOpenHref`
|
|
26
|
+
* encode the id into the fragment while rows render the raw id, so
|
|
27
|
+
* `#ticket-a%2Fb` must still find `id="ticket-a/b"`. A malformed escape
|
|
28
|
+
* sequence just falls through;
|
|
29
|
+
* 3. a NORMALIZED match against the document's headings — doc bodies are
|
|
30
|
+
* authored on GitHub, whose slugger keeps the hyphen left behind by a
|
|
31
|
+
* stripped emoji (`## 📚 Table of Contents` → `#-table-of-contents`)
|
|
32
|
+
* while our ids trim it. Without this every in-body TOC link in every
|
|
33
|
+
* GitHub-authored doc resolves to nothing and clicking one does nothing.
|
|
34
|
+
* See `utils/anchor-id`.
|
|
24
35
|
*/
|
|
25
36
|
export declare function getHashTargetElement(id: string): HTMLElement | null;
|
|
26
37
|
export interface NavigateSamePageHashOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"same-page-hash-nav.d.ts","sourceRoot":"","sources":["../../src/utils/same-page-hash-nav.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"same-page-hash-nav.d.ts","sourceRoot":"","sources":["../../src/utils/same-page-hash-nav.ts"],"names":[],"mappings":"AAGA;;sCAEsC;AACtC,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC;yCACyC;AACzC,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAEtC;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAkBnE;AAED,MAAM,WAAW,2BAA2B;IAC1C,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;gEAC4D;IAC5D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAkET"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import type { Root, Text, Link, Paragraph } from 'mdast'
|
|
3
|
+
import { remarkMentionChips } from '../remark-mention-chips'
|
|
4
|
+
|
|
5
|
+
/** One-paragraph tree holding a single text leaf — what the plugin walks. */
|
|
6
|
+
function treeOf(text: string): Root {
|
|
7
|
+
return { type: 'root', children: [{ type: 'paragraph', children: [{ type: 'text', value: text }] }] }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* unified's `Plugin` type declares a `this: Processor` parameter, so calling the
|
|
12
|
+
* attacher bare (outside a pipeline) is a TS2684 — cast the attacher itself, not
|
|
13
|
+
* just its return value, to drop that `this`.
|
|
14
|
+
*/
|
|
15
|
+
const attach = remarkMentionChips as unknown as () => (t: Root) => void
|
|
16
|
+
|
|
17
|
+
function run(text: string): Array<Text | Link> {
|
|
18
|
+
const tree = treeOf(text)
|
|
19
|
+
attach()(tree)
|
|
20
|
+
return (tree.children[0] as Paragraph).children as Array<Text | Link>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `mention://marker:id` urls the plugin emitted, in order. */
|
|
24
|
+
function mentions(text: string): string[] {
|
|
25
|
+
return run(text).flatMap((n) => (n.type === 'link' ? [n.url] : []))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('remarkMentionChips markers', () => {
|
|
29
|
+
it('parses the lowercase markers', () => {
|
|
30
|
+
expect(mentions('see @device:64f0a1 and @kb:5')).toEqual([
|
|
31
|
+
'mention://device:64f0a1',
|
|
32
|
+
'mention://kb:5',
|
|
33
|
+
])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('parses a camelCase marker', () => {
|
|
37
|
+
// Regression: `ContextItemType.SCHEDULED_SCRIPT` ships as `scheduledScript`,
|
|
38
|
+
// and a lowercase-only grammar matched NOTHING in the token — the mention
|
|
39
|
+
// silently stayed raw text in the bubble instead of becoming a chip.
|
|
40
|
+
expect(mentions('run @scheduledScript:6a6c9342f8e10b2477900c08 now')).toEqual([
|
|
41
|
+
'mention://scheduledScript:6a6c9342f8e10b2477900c08',
|
|
42
|
+
])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('keeps the surrounding text intact around a camelCase mention', () => {
|
|
46
|
+
const parts = run('run @scheduledScript:6a6c9342f8e10b2477900c08 now')
|
|
47
|
+
expect(parts.map((n) => (n.type === 'text' ? n.value : `[${(n as Link).url}]`))).toEqual([
|
|
48
|
+
'run ',
|
|
49
|
+
'[mention://scheduledScript:6a6c9342f8e10b2477900c08]',
|
|
50
|
+
' now',
|
|
51
|
+
])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('fires inside punctuation but never mid-word', () => {
|
|
55
|
+
expect(mentions('(@scheduledScript:6a6c9342f8e10b2477900c08)')).toEqual([
|
|
56
|
+
'mention://scheduledScript:6a6c9342f8e10b2477900c08',
|
|
57
|
+
])
|
|
58
|
+
// An email-shaped host:port is not a mention — the `@` is preceded by a word char.
|
|
59
|
+
expect(mentions('mail user@Host:1234 stays text')).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('leaves a trailing sentence period out of the id', () => {
|
|
63
|
+
expect(mentions('ran @scheduledScript:6a6c9342f8e10b2477900c08.')).toEqual([
|
|
64
|
+
'mention://scheduledScript:6a6c9342f8e10b2477900c08',
|
|
65
|
+
])
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -27,8 +27,9 @@ import type { MessageSegment, MessageContent, ChatMessageEnhancedProps } from ".
|
|
|
27
27
|
* (e.g. it matches `x@device:1` mid-word, which the plugin skips), so a context
|
|
28
28
|
* item would be stripped from the chip strip yet never rendered inline — lost
|
|
29
29
|
* from display entirely. The id is capture group 2 (group 1 is the boundary).
|
|
30
|
-
* Marker lowercase
|
|
31
|
-
|
|
30
|
+
* Marker `[a-zA-Z]+` (markers are mostly lowercase but not all — e.g.
|
|
31
|
+
* `scheduledScript`); id is the mention-token charset. */
|
|
32
|
+
const MENTION_MARKER_REGEX = /(^|[^\w@])@[a-zA-Z]+:([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Same regex shape as `remarkCardLinks` — kept in lockstep so the
|
|
@@ -6,6 +6,7 @@ import { cn } from "../../utils/cn"
|
|
|
6
6
|
import { ChatMessageEnhanced } from "./chat-message-enhanced"
|
|
7
7
|
import { ChatMessageListSkeleton } from "./chat-message-skeleton"
|
|
8
8
|
import { DotsLoaderIcon, Arrow02DownIcon } from "../icons-v2-generated"
|
|
9
|
+
import { OverlayOpenRegistryProvider } from "../ui/overlay-open-registry"
|
|
9
10
|
import { CyclingPhrase } from "./cycling-phrase"
|
|
10
11
|
import type { ChatMessageListProps } from "./types"
|
|
11
12
|
import { SCROLL_ANCHOR } from "./types/message.types"
|
|
@@ -141,10 +142,44 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
141
142
|
// effect below owns first-paint positioning so re-opening a chat
|
|
142
143
|
// with prior history lands at the bottom without a smooth-scroll
|
|
143
144
|
// animation playing over the cold-paint.
|
|
144
|
-
const { scrollRef, contentRef, scrollToBottom, stopScroll } =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
const { scrollRef, contentRef, scrollToBottom, stopScroll, isAtBottom } =
|
|
146
|
+
useStickToBottom({
|
|
147
|
+
resize: 'smooth',
|
|
148
|
+
initial: false,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// ---- Overlay guard: don't move the ground under an open menu -------
|
|
152
|
+
// A ⋯ menu inside a bubble stays anchored to its trigger. While a reply
|
|
153
|
+
// streams in, the follow-the-bottom scroll carries that trigger upward and
|
|
154
|
+
// drags the menu with it, until the menu sits at the panel edge detached
|
|
155
|
+
// from its card. So: while any overlay in this thread is open, stop
|
|
156
|
+
// chasing the bottom (the same thing ChatGPT/Claude do once you interact
|
|
157
|
+
// with the transcript), then catch up on close if we were at the bottom
|
|
158
|
+
// when it opened. New content still renders — we simply stop scrolling to
|
|
159
|
+
// it, and the existing "scroll to bottom" affordance stays available.
|
|
160
|
+
const overlayOpenRef = useRef(false)
|
|
161
|
+
const wasAtBottomOnOpenRef = useRef(false)
|
|
162
|
+
const isAtBottomRef = useRef(isAtBottom)
|
|
163
|
+
isAtBottomRef.current = isAtBottom
|
|
164
|
+
const handleOverlayOpenChange = useCallback(
|
|
165
|
+
(hasOpenOverlay: boolean) => {
|
|
166
|
+
overlayOpenRef.current = hasOpenOverlay
|
|
167
|
+
if (hasOpenOverlay) {
|
|
168
|
+
wasAtBottomOnOpenRef.current = isAtBottomRef.current
|
|
169
|
+
// Cancel any in-flight spring so the thread settles immediately
|
|
170
|
+
// instead of drifting for another few hundred ms under the menu.
|
|
171
|
+
stopScroll()
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
// Closed again: resume only if the reader was pinned to the bottom
|
|
175
|
+
// before opening it. Someone who had scrolled up to read history keeps
|
|
176
|
+
// their position.
|
|
177
|
+
if (wasAtBottomOnOpenRef.current) {
|
|
178
|
+
void scrollToBottom({ animation: 'smooth' })
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
[scrollToBottom, stopScroll],
|
|
182
|
+
)
|
|
148
183
|
|
|
149
184
|
// ---- Prepend / load-more state (NOT owned by the library) --------
|
|
150
185
|
const sentinelRef = useRef<HTMLDivElement>(null)
|
|
@@ -263,7 +298,12 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
263
298
|
const prevLastIdx = prevLastId ? messages.findIndex((m) => m.id === prevLastId) : -1
|
|
264
299
|
const newSlice = prevLastIdx >= 0 ? messages.slice(prevLastIdx + 1) : messages.slice(prevCount)
|
|
265
300
|
const hasNewUser = newSlice.some((m) => m.role === 'user')
|
|
266
|
-
|
|
301
|
+
// An open overlay suppresses even this one: `ignoreEscapes` would yank
|
|
302
|
+
// the thread out from under it — and with it the follow intent, which
|
|
303
|
+
// would re-assert the jump on every later resize. A user who just sent
|
|
304
|
+
// a message with a menu open is rare; a menu jumping away mid-click is
|
|
305
|
+
// not worth it.
|
|
306
|
+
if (hasNewUser && !overlayOpenRef.current) {
|
|
267
307
|
// Arms the follow intent for the WHOLE turn: the answer's
|
|
268
308
|
// async growth (attachment images, streamed tokens, entity
|
|
269
309
|
// cards resolving out of their skeletons long after the
|
|
@@ -657,6 +697,8 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
657
697
|
|
|
658
698
|
useLayoutEffect(() => {
|
|
659
699
|
if (!autoScroll) return
|
|
700
|
+
// Top-anchored display turns also move the ground — same guard.
|
|
701
|
+
if (overlayOpenRef.current) return
|
|
660
702
|
const last = messages[messages.length - 1]
|
|
661
703
|
if (!last || last.role !== 'assistant' || last.scrollAnchor !== SCROLL_ANCHOR.TOP) return
|
|
662
704
|
// Metadata frame arrives BEFORE body. Wait for content so we don't
|
|
@@ -847,6 +889,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
847
889
|
!(pendingApprovals && pendingApprovals.length > 0)
|
|
848
890
|
|
|
849
891
|
return (
|
|
892
|
+
<OverlayOpenRegistryProvider onOpenChange={handleOverlayOpenChange}>
|
|
850
893
|
<div className="flex-1 min-h-0 flex flex-col">
|
|
851
894
|
{/* Positioning box for the jump-to-bottom button: it wraps ONLY the
|
|
852
895
|
scroller, so `absolute bottom-…` really is the scroller's lower
|
|
@@ -1029,6 +1072,7 @@ const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
|
|
1029
1072
|
</div>
|
|
1030
1073
|
)}
|
|
1031
1074
|
</div>
|
|
1075
|
+
</OverlayOpenRegistryProvider>
|
|
1032
1076
|
)
|
|
1033
1077
|
},
|
|
1034
1078
|
)
|
|
@@ -52,7 +52,7 @@ import { MingoHistoryRail } from './mingo-history-rail'
|
|
|
52
52
|
import { GuideWelcome, type GuideWelcomeProps } from './guide-welcome'
|
|
53
53
|
import { accentFromIdentityIcon, getAgentAccent } from './quick-action-chip'
|
|
54
54
|
import { GuideModeBanner } from './guide-mode-banner'
|
|
55
|
-
import { PortalContainerContext } from '../ui/portal-container'
|
|
55
|
+
import { CollisionBoundaryContext, PortalContainerContext } from '../ui/portal-container'
|
|
56
56
|
import { ChatPanelHeader, type ChatPanelHeaderProps } from './chat-panel-header'
|
|
57
57
|
import { SquareAvatar } from '../ui/square-avatar'
|
|
58
58
|
import { ChatHeaderIconButton } from './chat-header-icon-button'
|
|
@@ -1795,6 +1795,13 @@ function EmbeddableChatInner({
|
|
|
1795
1795
|
|
|
1796
1796
|
// Host node for in-panel Radix portals (see the body wrapper below).
|
|
1797
1797
|
const [portalHost, setPortalHost] = useState<HTMLDivElement | null>(null)
|
|
1798
|
+
// The panel element itself, published as the COLLISION boundary for overlays
|
|
1799
|
+
// opened inside it. Radix otherwise collides against the viewport, so a ⋯ menu
|
|
1800
|
+
// near the top of the thread flips upward past the panel and renders over the
|
|
1801
|
+
// app header (reported: an entity card's menu landing in the top-right corner,
|
|
1802
|
+
// detached from its card). `portalHost` can't serve here — it is
|
|
1803
|
+
// `display: contents` and has no box to measure.
|
|
1804
|
+
const [panelBoundary, setPanelBoundary] = useState<HTMLDivElement | null>(null)
|
|
1798
1805
|
|
|
1799
1806
|
// ── Split (wide) Mingo layout ─────────────────────────────────────────────
|
|
1800
1807
|
// The panel measures its own width and, in Mingo mode, promotes the inline
|
|
@@ -1817,6 +1824,14 @@ function EmbeddableChatInner({
|
|
|
1817
1824
|
return () => ro.disconnect()
|
|
1818
1825
|
}, [])
|
|
1819
1826
|
|
|
1827
|
+
// One ref for two consumers: the width measurement above (a plain ref, read
|
|
1828
|
+
// once on mount) and the collision-boundary context (state, so provider
|
|
1829
|
+
// consumers re-render once the node exists).
|
|
1830
|
+
const setPanelNode = useCallback((node: HTMLDivElement | null) => {
|
|
1831
|
+
panelMeasureRef.current = node
|
|
1832
|
+
setPanelBoundary(node)
|
|
1833
|
+
}, [])
|
|
1834
|
+
|
|
1820
1835
|
// Rail collapse toggle (Figma ⟶| control), persisted so it survives the
|
|
1821
1836
|
// drawer remounting on close.
|
|
1822
1837
|
const [railCollapsed, setRailCollapsed] = useState(false)
|
|
@@ -2003,6 +2018,7 @@ function EmbeddableChatInner({
|
|
|
2003
2018
|
// document root. See `PortalContainerContext`.
|
|
2004
2019
|
const body = (
|
|
2005
2020
|
<PortalContainerContext.Provider value={portalHost}>
|
|
2021
|
+
<CollisionBoundaryContext.Provider value={panelBoundary}>
|
|
2006
2022
|
{/* Panel surface depends on state (Figma):
|
|
2007
2023
|
• Narrow "Current Chats" LIST + FULL-PANEL archive page → grey
|
|
2008
2024
|
`ods-card` (#212121) — matching the grey rail those lists live
|
|
@@ -2017,7 +2033,7 @@ function EmbeddableChatInner({
|
|
|
2017
2033
|
grey), so the chat block on the right must keep the conversation's
|
|
2018
2034
|
dark surface instead of following the archive. */}
|
|
2019
2035
|
<div
|
|
2020
|
-
ref={
|
|
2036
|
+
ref={setPanelNode}
|
|
2021
2037
|
className={`flex h-full flex-col overflow-hidden transition-colors duration-200 ${
|
|
2022
2038
|
(archiveOpen && !splitActive) || stackedListView
|
|
2023
2039
|
? 'bg-ods-card'
|
|
@@ -2587,6 +2603,7 @@ function EmbeddableChatInner({
|
|
|
2587
2603
|
{/* Portal target for in-panel Radix overlays — `display: contents`
|
|
2588
2604
|
so it adds no box; content is positioned `fixed` by Radix. */}
|
|
2589
2605
|
<div ref={setPortalHost} style={{ display: 'contents' }} />
|
|
2606
|
+
</CollisionBoundaryContext.Provider>
|
|
2590
2607
|
</PortalContainerContext.Provider>
|
|
2591
2608
|
)
|
|
2592
2609
|
|
|
@@ -36,6 +36,7 @@ import type { ChatRef } from '../chat-ref.types'
|
|
|
36
36
|
import { useChatCardItem } from '../hooks/use-chat-card-item'
|
|
37
37
|
import { handleChatNavClick } from '../utils/nav-click-handler'
|
|
38
38
|
import { resolveSourceRowCTA, resolveSourceIcon, sourceRowCtxFromRuntime } from '../utils/source-row-cta'
|
|
39
|
+
import { resolveFetchedCardHref, readFetchedCardTitle } from '../utils/resolve-fetched-card-href'
|
|
39
40
|
import { resolveHrefForRuntime } from '../utils/chat-nav-resolution'
|
|
40
41
|
import {
|
|
41
42
|
computeIsNewTab,
|
|
@@ -1000,8 +1001,15 @@ type ChatCardRegistryEntry =
|
|
|
1000
1001
|
* resolved ref — so the wrapper sees a non-null `href`, the
|
|
1001
1002
|
* pre-computed `isNewTab` reflects the actual destination, and
|
|
1002
1003
|
* the click goes through `handleChatNavClick` (no silent
|
|
1003
|
-
* bypass of embed-mode / close-on-nav).
|
|
1004
|
+
* bypass of embed-mode / close-on-nav). Takes precedence over the
|
|
1005
|
+
* `composeContentUrl` fallback below. */
|
|
1004
1006
|
fallbackHref?: (item: any) => string | null
|
|
1007
|
+
/** Opt OUT of the post-fetch `composeContentUrl` fallback (see
|
|
1008
|
+
* `resolveFetchedCardHref`). Set for types with NO public destination
|
|
1009
|
+
* — the seam would otherwise synthesize `<contentOrigin>/<type>/<id>`
|
|
1010
|
+
* from its default branch and hand the user a 404. Types with a real
|
|
1011
|
+
* route (hosted, or covered by a host `override`) leave it unset. */
|
|
1012
|
+
noComposedHref?: boolean
|
|
1005
1013
|
}
|
|
1006
1014
|
|
|
1007
1015
|
interface FinancialCardConfig {
|
|
@@ -1106,6 +1114,8 @@ interface RoadmapEntryConfig {
|
|
|
1106
1114
|
label: string
|
|
1107
1115
|
cardType: RoadmapCardType
|
|
1108
1116
|
contentRefType: string
|
|
1117
|
+
/** See `ChatCardRegistryEntry.noComposedHref`. */
|
|
1118
|
+
noComposedHref?: boolean
|
|
1109
1119
|
}
|
|
1110
1120
|
const ROADMAP_CARD_CONFIGS: Record<string, RoadmapEntryConfig> = {
|
|
1111
1121
|
roadmap_item: { label: 'Roadmap item', cardType: 'roadmap_item', contentRefType: 'roadmap_item' },
|
|
@@ -1118,6 +1128,9 @@ const ROADMAP_CARD_CONFIGS: Record<string, RoadmapEntryConfig> = {
|
|
|
1118
1128
|
label: 'Internal task',
|
|
1119
1129
|
cardType: 'internal_task',
|
|
1120
1130
|
contentRefType: 'internal_task',
|
|
1131
|
+
// Internal ClickUp work has no public destination on any platform — the
|
|
1132
|
+
// seam's default branch would synthesize `<hub>/internal_task/<id>`.
|
|
1133
|
+
noComposedHref: true,
|
|
1121
1134
|
},
|
|
1122
1135
|
}
|
|
1123
1136
|
function roadmapRegistryEntries(): Record<string, ChatCardRegistryEntry> {
|
|
@@ -1128,6 +1141,7 @@ function roadmapRegistryEntries(): Record<string, ChatCardRegistryEntry> {
|
|
|
1128
1141
|
label: cfg.label,
|
|
1129
1142
|
contentRefType: cfg.contentRefType,
|
|
1130
1143
|
bareInline: true,
|
|
1144
|
+
...(cfg.noComposedHref ? { noComposedHref: true } : {}),
|
|
1131
1145
|
skeleton: () => <RoadmapCardSkeleton size="sm" />,
|
|
1132
1146
|
render: (item, chatRef, opts) => (
|
|
1133
1147
|
<RoadmapChatCard
|
|
@@ -1420,6 +1434,17 @@ function ChatCardNavWrap({
|
|
|
1420
1434
|
const onClickCapture = (e: React.MouseEvent<HTMLElement>) => {
|
|
1421
1435
|
if (!href) return
|
|
1422
1436
|
const targetEl = e.target as HTMLElement
|
|
1437
|
+
// Only claim clicks that physically happened INSIDE this card.
|
|
1438
|
+
//
|
|
1439
|
+
// The card's "⋯" menu renders through a React PORTAL: its DOM lives in the
|
|
1440
|
+
// panel's portal host, but React still bubbles its events through this
|
|
1441
|
+
// subtree — so this capture handler sees them first and, being a capture
|
|
1442
|
+
// phase, runs BEFORE the menu row's own `stopPropagation`. That swallowed
|
|
1443
|
+
// the menu's trailing "↗ Open in new tab" button: `executeNavigation`
|
|
1444
|
+
// called `preventDefault()` and routed the card's href in the SAME tab,
|
|
1445
|
+
// so the anchor's `target="_blank"` never applied.
|
|
1446
|
+
const host = e.currentTarget as HTMLElement | null
|
|
1447
|
+
if (!host?.contains(targetEl)) return
|
|
1423
1448
|
// Buttons rendered INSIDE the card's outer `<a>` (e.g. RoadmapCard
|
|
1424
1449
|
// vote buttons, ImageGallery thumbnails) bubble up with
|
|
1425
1450
|
// `closest('a')` truthy — without this guard, clicking them would
|
|
@@ -1522,18 +1547,54 @@ export function ChatCardLoader({
|
|
|
1522
1547
|
)
|
|
1523
1548
|
if (!entry) return null
|
|
1524
1549
|
|
|
1525
|
-
// Apply
|
|
1550
|
+
// Apply the post-fetch URL fallback (the ref carried no `externalUrl`).
|
|
1526
1551
|
// We mutate `resolvedChatRef.url` BEFORE computing isNewTab so the
|
|
1527
1552
|
// wrapper's interceptor sees the destination the user will actually
|
|
1528
1553
|
// visit. `safeHref` blocks `javascript:` / `data:` payloads even
|
|
1529
1554
|
// though the registry callers compose hub-internal strings today.
|
|
1530
|
-
|
|
1555
|
+
//
|
|
1556
|
+
// Two sources, in order:
|
|
1557
|
+
// 1. the registry's per-type `fallbackHref` — an explicit non-content
|
|
1558
|
+
// destination (marketing campaign → `/admin/...`);
|
|
1559
|
+
// 2. the host's `composeContentUrl` seam via `resolveFetchedCardHref` —
|
|
1560
|
+
// the SAME resolver page cards and SSE chat cards go through. This is
|
|
1561
|
+
// what makes Mingo/NATS cards clickable: that transport ships bare
|
|
1562
|
+
// `[card://type:id]` markers with no refs metadata, so the ref reaches
|
|
1563
|
+
// us with `url: null` and `resolveSourceRowCTA` has nothing to route.
|
|
1564
|
+
const composedHref =
|
|
1565
|
+
fetchEntry && !resolvedChatRef.url && item && !fetchEntry.fallbackHref && !fetchEntry.noComposedHref
|
|
1566
|
+
? resolveFetchedCardHref({
|
|
1567
|
+
contentRefType: fetchEntry.contentRefType,
|
|
1568
|
+
id: resolvedChatRef.id,
|
|
1569
|
+
item,
|
|
1570
|
+
composeContentUrl: runtime.composeContentUrl,
|
|
1571
|
+
})
|
|
1572
|
+
: null
|
|
1573
|
+
const hrefResolvedChatRef: ChatRef =
|
|
1531
1574
|
fetchEntry && !resolvedChatRef.url && item && fetchEntry.fallbackHref
|
|
1532
1575
|
? {
|
|
1533
1576
|
...resolvedChatRef,
|
|
1534
1577
|
url: safeHref(fetchEntry.fallbackHref(item)),
|
|
1535
1578
|
}
|
|
1536
|
-
:
|
|
1579
|
+
: composedHref
|
|
1580
|
+
? {
|
|
1581
|
+
...resolvedChatRef,
|
|
1582
|
+
url: safeHref(composedHref.href),
|
|
1583
|
+
targetPlatform: composedHref.targetPlatform ?? resolvedChatRef.targetPlatform ?? null,
|
|
1584
|
+
}
|
|
1585
|
+
: resolvedChatRef
|
|
1586
|
+
|
|
1587
|
+
// Title enrichment, same synthetic-ref gap as the href above: a Mingo
|
|
1588
|
+
// `[card://type:id]` marker produces `title: <id>` because the transport
|
|
1589
|
+
// ships no ref metadata. Downstream consumers that read `ref.title` — most
|
|
1590
|
+
// visibly the "Ask Mingo" prompt, which would otherwise send
|
|
1591
|
+
// "Tell me more about 86ad3qvv5" — get the row's real title once it loads.
|
|
1592
|
+
// A ref that already carries a distinct title (the SSE path) is untouched.
|
|
1593
|
+
const fetchedTitle = fetchEntry && item ? readFetchedCardTitle(item) : null
|
|
1594
|
+
const finalChatRef: ChatRef =
|
|
1595
|
+
fetchedTitle && (!hrefResolvedChatRef.title || hrefResolvedChatRef.title === hrefResolvedChatRef.id)
|
|
1596
|
+
? { ...hrefResolvedChatRef, title: fetchedTitle }
|
|
1597
|
+
: hrefResolvedChatRef
|
|
1537
1598
|
|
|
1538
1599
|
// Pre-compute new-tab decision ONCE here (the same rule chips use).
|
|
1539
1600
|
// Render branches that pass `target` / `rel` to their card pull this
|
|
@@ -56,6 +56,7 @@ import { createChatDialogStore, DEFAULT_DIALOG_SIDE } from '../stream/chat-dialo
|
|
|
56
56
|
import { useChatStreamReducer } from '../stream/use-chat-stream-reducer'
|
|
57
57
|
import { processHistoricalMessagesWithErrors } from '../utils/process-historical-messages'
|
|
58
58
|
import { extractIncompleteTailState } from '../utils/extract-incomplete-message-state'
|
|
59
|
+
import { buildDiscussPrompt } from '../utils/discuss-ref-prompt'
|
|
59
60
|
import type {
|
|
60
61
|
ChunkData,
|
|
61
62
|
FetchChunksFunction,
|
|
@@ -1076,10 +1077,33 @@ export function useNatsChatAdapter(
|
|
|
1076
1077
|
[],
|
|
1077
1078
|
)
|
|
1078
1079
|
|
|
1079
|
-
//
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1080
|
+
// "Ask Mingo" on an inline entity card. The agent transport has NO
|
|
1081
|
+
// entity-id-filtered retrieval (its `ContextItemType` enum covers DEVICE /
|
|
1082
|
+
// SCRIPT / TICKET / ORGANIZATION / USER / KB_ARTICLE / POLICY / QUERY /
|
|
1083
|
+
// SCHEDULED_SCRIPT — no content types), so unlike the SSE path there is no
|
|
1084
|
+
// `commandOverride.entityIdFilter` to send: the prompt carries the type + id
|
|
1085
|
+
// as text and the agent resolves the row itself. Same sentence as guide mode
|
|
1086
|
+
// (shared builder) so the affordance reads identically in both.
|
|
1087
|
+
//
|
|
1088
|
+
// This used to be a no-op, which made the card's "Ask Mingo" menu item dead
|
|
1089
|
+
// in Mingo mode. Swap `includeReference` for a structured filter once the
|
|
1090
|
+
// agent backend accepts one.
|
|
1091
|
+
const discussRef = useCallback(
|
|
1092
|
+
(reference: ChatRef) => {
|
|
1093
|
+
const prompt = buildDiscussPrompt(reference, { includeReference: true })
|
|
1094
|
+
// Menu-item click, so there is no composer promise for the rejection to
|
|
1095
|
+
// surface through — a bare `void` would leave it unhandled. Logged in the
|
|
1096
|
+
// same shape as `stopMessage`'s. NOTE: recovering the optimistic rows and
|
|
1097
|
+
// the stuck `thinking` phase after a failed publish is a `sendMessage`
|
|
1098
|
+
// concern (it affects composer sends identically) and is not done here.
|
|
1099
|
+
void sendMessage(prompt).catch((err) => {
|
|
1100
|
+
console.error('[useNatsChatAdapter] discussRef send failed:', err)
|
|
1101
|
+
})
|
|
1102
|
+
},
|
|
1103
|
+
[sendMessage],
|
|
1104
|
+
)
|
|
1105
|
+
// Display stays a no-op: it dispatches a `/… display "<x>"` slash command,
|
|
1106
|
+
// and the agent transport has no slash-command registry.
|
|
1083
1107
|
const displayRef = useCallback((_ref: ChatRef) => {
|
|
1084
1108
|
/* no-op in Mingo mode */
|
|
1085
1109
|
}, [])
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
} from '../utils/chat-conversation-storage'
|
|
68
68
|
import { useChatHistoryHydration } from './use-chat-history-hydration'
|
|
69
69
|
import { sanitizeTitleForChat } from '../utils/slash-dispatch-utils'
|
|
70
|
+
import { buildDiscussPrompt } from '../utils/discuss-ref-prompt'
|
|
70
71
|
import { defaultTableIdForDocumentType } from '../utils/source-icons'
|
|
71
72
|
import type { WireCommandOverride } from '../utils/slash-dispatch-utils'
|
|
72
73
|
import type { ChatAttachment } from '../utils/chat-attachment-markdown'
|
|
@@ -657,9 +658,10 @@ export function useSseChatAdapter(
|
|
|
657
658
|
}
|
|
658
659
|
// RETRIEVAL IS STRICTLY PRIMARY-KEY-DRIVEN. The visible prose
|
|
659
660
|
// ("Tell me more about <title>") is UX-only; retrieval narrows
|
|
660
|
-
// via `entityIdFilter` before the LLM is invoked.
|
|
661
|
-
|
|
662
|
-
|
|
661
|
+
// via `entityIdFilter` before the LLM is invoked. The sentence itself
|
|
662
|
+
// comes from the shared builder so the Mingo transport (which has no
|
|
663
|
+
// id filter and leans on the prose) phrases it identically.
|
|
664
|
+
const prompt = buildDiscussPrompt(reference)
|
|
663
665
|
sendMessage(prompt, {
|
|
664
666
|
commandOverride: { entityIdFilter: { tableId, id: refId } },
|
|
665
667
|
})
|
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
* intact, and the marker disappears before any rehype-raw HTML pass.
|
|
16
16
|
*
|
|
17
17
|
* Grammar:
|
|
18
|
-
* - marker: `[a-
|
|
18
|
+
* - marker: `[a-zA-Z]+` — the backend `ContextItemType.marker()` short form
|
|
19
19
|
* (`device`, `script`, `ticket`, `organization`, `user`, `kb`, `policy`,
|
|
20
|
-
* `query`, …).
|
|
20
|
+
* `query`, `scheduledScript`, …). Mostly lowercase, but NOT guaranteed to be:
|
|
21
|
+
* the marker is matched verbatim by the backend's `MentionParser`, and
|
|
22
|
+
* `SCHEDULED_SCRIPT` ships as camelCase `scheduledScript` — a lowercase-only
|
|
23
|
+
* grammar silently dropped that whole token (no chip, raw text in the bubble).
|
|
21
24
|
* - id: charset `[A-Za-z0-9_.+/=-]` (UUIDs with hyphens, fleet numeric ids,
|
|
22
25
|
* base64 global ids, etc.; matches the composer's `MENTION_TOKEN`) — but the
|
|
23
26
|
* id may NOT END in `.` or `-`, so a trailing SENTENCE period/dash isn't
|
|
@@ -35,7 +38,7 @@ import type { Plugin } from 'unified'
|
|
|
35
38
|
import type { Root, Text, Link } from 'mdast'
|
|
36
39
|
import { visit, SKIP } from 'unist-util-visit'
|
|
37
40
|
|
|
38
|
-
const MENTION_REGEX = /(^|[^\w@])@([a-
|
|
41
|
+
const MENTION_REGEX = /(^|[^\w@])@([a-zA-Z]+):([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
|
|
39
42
|
|
|
40
43
|
export const remarkMentionChips: Plugin<[], Root> = () => {
|
|
41
44
|
return (tree: Root) => {
|
|
@@ -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
|
+
})
|