@flamingo-stack/openframe-frontend-core 0.0.219 → 0.0.220-snapshot.20260602172647

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.
Files changed (146) hide show
  1. package/dist/{chunk-F3FO2ZZZ.cjs → chunk-4PBV66HQ.cjs} +7 -7
  2. package/dist/{chunk-F3FO2ZZZ.cjs.map → chunk-4PBV66HQ.cjs.map} +1 -1
  3. package/dist/{chunk-EDW2NVRV.js → chunk-4WZOFD46.js} +37 -37
  4. package/dist/{chunk-EDW2NVRV.js.map → chunk-4WZOFD46.js.map} +1 -1
  5. package/dist/{chunk-YX3YQNC4.cjs → chunk-73YDB6AT.cjs} +13 -13
  6. package/dist/{chunk-YX3YQNC4.cjs.map → chunk-73YDB6AT.cjs.map} +1 -1
  7. package/dist/{chunk-MPHDM2VZ.cjs → chunk-7TQNW2AM.cjs} +30 -30
  8. package/dist/{chunk-MPHDM2VZ.cjs.map → chunk-7TQNW2AM.cjs.map} +1 -1
  9. package/dist/{chunk-65CPJ4SX.cjs → chunk-C6ASEPZL.cjs} +30 -30
  10. package/dist/{chunk-65CPJ4SX.cjs.map → chunk-C6ASEPZL.cjs.map} +1 -1
  11. package/dist/{chunk-DRPECAXO.js → chunk-CPIX5AAR.js} +2 -2
  12. package/dist/{chunk-SRA2QYK6.js → chunk-E2AWBQEU.js} +4 -4
  13. package/dist/{chunk-SZXKKEUH.cjs → chunk-E6B4B7GM.cjs} +46 -30
  14. package/dist/chunk-E6B4B7GM.cjs.map +1 -0
  15. package/dist/{chunk-XG7DFRJL.js → chunk-FOOQFRJR.js} +3 -3
  16. package/dist/{chunk-A3PL6ZCF.js → chunk-IS4IZC7N.js} +6388 -5128
  17. package/dist/chunk-IS4IZC7N.js.map +1 -0
  18. package/dist/{chunk-ZGBXHK26.cjs → chunk-JMGSJHFP.cjs} +12 -12
  19. package/dist/{chunk-ZGBXHK26.cjs.map → chunk-JMGSJHFP.cjs.map} +1 -1
  20. package/dist/{chunk-SL3RGBPX.cjs → chunk-QNYH3WUU.cjs} +9 -9
  21. package/dist/{chunk-SL3RGBPX.cjs.map → chunk-QNYH3WUU.cjs.map} +1 -1
  22. package/dist/{chunk-24Q2WLIU.js → chunk-QYRV6MKX.js} +2 -2
  23. package/dist/{chunk-ZII7TNVA.js → chunk-SRCEVQYA.js} +3 -3
  24. package/dist/{chunk-Y3MXGCOW.js → chunk-YZDUOUMB.js} +46 -30
  25. package/dist/chunk-YZDUOUMB.js.map +1 -0
  26. package/dist/{chunk-7UZLRI7W.cjs → chunk-ZAGQXSAP.cjs} +3292 -2032
  27. package/dist/chunk-ZAGQXSAP.cjs.map +1 -0
  28. package/dist/components/chat/chat-archive-page.d.ts +25 -0
  29. package/dist/components/chat/chat-archive-page.d.ts.map +1 -0
  30. package/dist/components/chat/chat-composer.d.ts +29 -0
  31. package/dist/components/chat/chat-composer.d.ts.map +1 -0
  32. package/dist/components/chat/chat-header-icon-button.d.ts +14 -0
  33. package/dist/components/chat/chat-header-icon-button.d.ts.map +1 -0
  34. package/dist/components/chat/chat-panel-header.d.ts +32 -0
  35. package/dist/components/chat/chat-panel-header.d.ts.map +1 -0
  36. package/dist/components/chat/embeddable-chat.d.ts +18 -0
  37. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  38. package/dist/components/chat/guide-mode-banner.d.ts +16 -0
  39. package/dist/components/chat/guide-mode-banner.d.ts.map +1 -0
  40. package/dist/components/chat/guide-welcome.d.ts +42 -0
  41. package/dist/components/chat/guide-welcome.d.ts.map +1 -0
  42. package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts +58 -0
  43. package/dist/components/chat/hooks/use-chat-dialog-manager.d.ts.map +1 -0
  44. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +26 -1
  45. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  46. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  47. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
  48. package/dist/components/chat/index.cjs +29 -5
  49. package/dist/components/chat/index.cjs.map +1 -1
  50. package/dist/components/chat/index.d.ts +9 -0
  51. package/dist/components/chat/index.d.ts.map +1 -1
  52. package/dist/components/chat/index.js +28 -4
  53. package/dist/components/chat/mingo-chat-history.d.ts +37 -0
  54. package/dist/components/chat/mingo-chat-history.d.ts.map +1 -0
  55. package/dist/components/chat/mingo-chat-modals.d.ts +50 -0
  56. package/dist/components/chat/mingo-chat-modals.d.ts.map +1 -0
  57. package/dist/components/chat/mingo-onboarding-card.d.ts.map +1 -1
  58. package/dist/components/chat/mingo-welcome.d.ts +78 -0
  59. package/dist/components/chat/mingo-welcome.d.ts.map +1 -0
  60. package/dist/components/chat/types/unified-chat-state.types.d.ts +6 -0
  61. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  62. package/dist/components/contact/index.cjs +6 -6
  63. package/dist/components/contact/index.js +5 -5
  64. package/dist/components/features/index.cjs +5 -5
  65. package/dist/components/features/index.js +4 -4
  66. package/dist/components/icons-v2-generated/brand-logos/fleet-mdm-logo-grey-icon.d.ts.map +1 -1
  67. package/dist/components/icons-v2-generated/brand-logos/fleet-mdm-logo-icon.d.ts.map +1 -1
  68. package/dist/components/icons-v2-generated/index.cjs +2 -2
  69. package/dist/components/icons-v2-generated/index.js +1 -1
  70. package/dist/components/index.cjs +128 -104
  71. package/dist/components/index.cjs.map +1 -1
  72. package/dist/components/index.js +31 -7
  73. package/dist/components/index.js.map +1 -1
  74. package/dist/components/layout/page-heading.d.ts +7 -6
  75. package/dist/components/layout/page-heading.d.ts.map +1 -1
  76. package/dist/components/navigation/app-layout-drawer.d.ts.map +1 -1
  77. package/dist/components/navigation/header-mingo-button.d.ts +4 -2
  78. package/dist/components/navigation/header-mingo-button.d.ts.map +1 -1
  79. package/dist/components/navigation/index.cjs +5 -5
  80. package/dist/components/navigation/index.js +4 -4
  81. package/dist/components/onboarding-guides/index.cjs +28 -28
  82. package/dist/components/onboarding-guides/index.js +6 -6
  83. package/dist/components/tickets/index.cjs +88 -88
  84. package/dist/components/tickets/index.js +7 -7
  85. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  86. package/dist/components/ui/file-manager/index.cjs +50 -50
  87. package/dist/components/ui/file-manager/index.js +1 -1
  88. package/dist/components/ui/index.cjs +29 -5
  89. package/dist/components/ui/index.cjs.map +1 -1
  90. package/dist/components/ui/index.js +28 -4
  91. package/dist/components/ui/modal-v2.d.ts.map +1 -1
  92. package/dist/components/ui/more-actions-menu.d.ts +8 -1
  93. package/dist/components/ui/more-actions-menu.d.ts.map +1 -1
  94. package/dist/components/ui/portal-container.d.ts +21 -0
  95. package/dist/components/ui/portal-container.d.ts.map +1 -0
  96. package/dist/components/ui/tooltip.d.ts.map +1 -1
  97. package/dist/hooks/index.cjs +3 -3
  98. package/dist/hooks/index.js +2 -2
  99. package/dist/index.cjs +29 -5
  100. package/dist/index.cjs.map +1 -1
  101. package/dist/index.js +28 -4
  102. package/package.json +1 -1
  103. package/src/components/chat/chat-archive-page.tsx +93 -0
  104. package/src/components/chat/chat-composer.tsx +99 -0
  105. package/src/components/chat/chat-header-icon-button.tsx +36 -0
  106. package/src/components/chat/chat-panel-header.tsx +114 -0
  107. package/src/components/chat/embeddable-chat.tsx +388 -311
  108. package/src/components/chat/guide-mode-banner.tsx +75 -0
  109. package/src/components/chat/guide-welcome.tsx +199 -0
  110. package/src/components/chat/hooks/use-chat-dialog-manager.ts +227 -0
  111. package/src/components/chat/hooks/use-nats-chat-adapter.ts +85 -0
  112. package/src/components/chat/hooks/use-sse-chat-adapter.ts +8 -0
  113. package/src/components/chat/hooks/use-unified-chat.ts +12 -0
  114. package/src/components/chat/index.ts +9 -0
  115. package/src/components/chat/mingo-chat-history.tsx +308 -0
  116. package/src/components/chat/mingo-chat-modals.tsx +223 -0
  117. package/src/components/chat/mingo-onboarding-card.tsx +5 -8
  118. package/src/components/chat/mingo-welcome.tsx +396 -0
  119. package/src/components/chat/types/unified-chat-state.types.ts +8 -0
  120. package/src/components/icons-v2/brand-logos/fleet-mdm-logo-grey.svg +6 -6
  121. package/src/components/icons-v2/brand-logos/fleet-mdm-logo.svg +6 -6
  122. package/src/components/icons-v2-generated/brand-logos/fleet-mdm-logo-grey-icon.tsx +2 -22
  123. package/src/components/icons-v2-generated/brand-logos/fleet-mdm-logo-icon.tsx +22 -2
  124. package/src/components/layout/page-heading.tsx +13 -7
  125. package/src/components/navigation/app-header.tsx +12 -12
  126. package/src/components/navigation/app-layout-drawer.tsx +25 -15
  127. package/src/components/navigation/header-mingo-button.tsx +22 -7
  128. package/src/components/ui/dropdown-menu.tsx +9 -3
  129. package/src/components/ui/modal-v2.tsx +33 -3
  130. package/src/components/ui/more-actions-menu.tsx +15 -2
  131. package/src/components/ui/portal-container.tsx +28 -0
  132. package/src/components/ui/tooltip.tsx +9 -3
  133. package/src/stories/AppLayoutSidebar.stories.tsx +184 -0
  134. package/src/stories/EmbeddableChat.stories.tsx +114 -0
  135. package/src/stories/GuideWelcome.stories.tsx +124 -0
  136. package/src/stories/MingoChatModals.stories.tsx +82 -0
  137. package/src/stories/MingoWelcome.stories.tsx +119 -0
  138. package/dist/chunk-7UZLRI7W.cjs.map +0 -1
  139. package/dist/chunk-A3PL6ZCF.js.map +0 -1
  140. package/dist/chunk-SZXKKEUH.cjs.map +0 -1
  141. package/dist/chunk-Y3MXGCOW.js.map +0 -1
  142. /package/dist/{chunk-DRPECAXO.js.map → chunk-CPIX5AAR.js.map} +0 -0
  143. /package/dist/{chunk-SRA2QYK6.js.map → chunk-E2AWBQEU.js.map} +0 -0
  144. /package/dist/{chunk-XG7DFRJL.js.map → chunk-FOOQFRJR.js.map} +0 -0
  145. /package/dist/{chunk-24Q2WLIU.js.map → chunk-QYRV6MKX.js.map} +0 -0
  146. /package/dist/{chunk-ZII7TNVA.js.map → chunk-SRCEVQYA.js.map} +0 -0
@@ -0,0 +1,75 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../utils/cn'
5
+ import { CompassIcon } from '../icons-v2-generated/map-and-travel/compass-icon'
6
+ import { QuestionCircleIcon } from '../icons-v2-generated/signs-and-symbols/question-circle-icon'
7
+ import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipProvider,
11
+ TooltipTrigger,
12
+ } from '../ui/tooltip'
13
+
14
+ const DEFAULT_LABEL = 'Guide Mode Chat'
15
+
16
+ const DEFAULT_TOOLTIP =
17
+ 'A quick session chat for exploring OpenFrame. Mingo answers how-to ' +
18
+ 'questions and guides you through configuration, but won’t touch devices, ' +
19
+ 'run scripts, or perform any actions.'
20
+
21
+ export interface GuideModeBannerProps {
22
+ /** Banner label (uppercased by the `text-h5` style). */
23
+ label?: React.ReactNode
24
+ /** Help-icon tooltip copy. `null` hides the help icon entirely. */
25
+ tooltip?: React.ReactNode | null
26
+ className?: string
27
+ }
28
+
29
+ /**
30
+ * Guide-mode indicator banner — Figma node `7532:328222`. A full-bleed accent
31
+ * (yellow) strip below the chat header: leading compass icon, the "Guide Mode
32
+ * Chat" label, and a trailing help icon whose tooltip explains the temporary,
33
+ * read-only nature of Guide mode.
34
+ */
35
+ export function GuideModeBanner({
36
+ label = DEFAULT_LABEL,
37
+ tooltip = DEFAULT_TOOLTIP,
38
+ className,
39
+ }: GuideModeBannerProps) {
40
+ return (
41
+ <div
42
+ className={cn(
43
+ 'flex h-8 w-full shrink-0 items-center gap-[var(--spacing-system-xs)] bg-ods-accent px-[var(--spacing-system-m)] py-[var(--spacing-system-xsf)]',
44
+ className,
45
+ )}
46
+ >
47
+ <CompassIcon size={16} className="shrink-0 text-ods-text-on-accent" />
48
+ <p className="flex-1 truncate text-h5 uppercase text-ods-text-on-accent">
49
+ {label}
50
+ </p>
51
+ {tooltip != null && (
52
+ <TooltipProvider delayDuration={0}>
53
+ <Tooltip>
54
+ <TooltipTrigger asChild>
55
+ <button
56
+ type="button"
57
+ aria-label="About Guide Mode"
58
+ className="shrink-0 rounded-sm text-ods-text-on-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-text-on-accent"
59
+ >
60
+ <QuestionCircleIcon size={16} />
61
+ </button>
62
+ </TooltipTrigger>
63
+ <TooltipContent
64
+ side="bottom"
65
+ align="end"
66
+ className="max-w-[280px] text-h6 normal-case"
67
+ >
68
+ {tooltip}
69
+ </TooltipContent>
70
+ </Tooltip>
71
+ </TooltipProvider>
72
+ )}
73
+ </div>
74
+ )
75
+ }
@@ -0,0 +1,199 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { cn } from '../../utils/cn'
5
+ import { MingoIcon } from '../icons'
6
+ import { Tag } from '../ui/tag'
7
+ import { MoreActionsMenu } from '../ui/more-actions-menu'
8
+ import { Ellipsis01Icon } from '../icons-v2-generated'
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ /** A guide quick-action chip (e.g. "How to start"). */
15
+ export interface GuideQuickAction {
16
+ /** Stable React key. */
17
+ id: string
18
+ /** Chip label. */
19
+ label: string
20
+ /** Prompt text seeded into the composer on click. Defaults to `label`. */
21
+ prompt?: string
22
+ }
23
+
24
+ export interface GuideWelcomeProps {
25
+ /** Greeting heading. Defaults to "Guide Mode Chat". */
26
+ title?: React.ReactNode
27
+ /** Greeting sub-line. Defaults to the OpenFrame temporary-session copy. */
28
+ subtitle?: React.ReactNode
29
+ /** Quick-action chips — caller-provided; there are no built-in defaults, so
30
+ * the chip row is omitted entirely unless the host supplies actions. The
31
+ * first `maxVisibleQuickActions` render inline; the rest collapse under a
32
+ * trailing "⋯" overflow menu. */
33
+ quickActions?: ReadonlyArray<GuideQuickAction>
34
+ /** How many chips to show inline before overflowing to the menu (default 3). */
35
+ maxVisibleQuickActions?: number
36
+ /** Fired when a quick-action chip (inline or menu) is activated. */
37
+ onQuickAction?: (action: GuideQuickAction) => void
38
+ /** Slash-command onboarding list — rendered inside the shared scroll region
39
+ * below the greeting (so greeting + list scroll together, with edge fades). */
40
+ children?: React.ReactNode
41
+ className?: string
42
+ }
43
+
44
+ // =============================================================================
45
+ // Defaults (OpenFrame copy — overridable; the kit stays platform-agnostic)
46
+ // =============================================================================
47
+
48
+ const DEFAULT_TITLE = 'Guide Mode Chat'
49
+
50
+ const DEFAULT_SUBTITLE =
51
+ 'This chat is temporary and will not be saved. Ask about OpenFrame docs, ' +
52
+ 'known issues, or manage your support tickets right here.'
53
+
54
+ // =============================================================================
55
+ // Component
56
+ // =============================================================================
57
+
58
+ /**
59
+ * GuideWelcome — Figma node `7532:328214`.
60
+ *
61
+ * Guide-mode chat empty state: a centred greeting and the slash-command
62
+ * onboarding list share one scroll region (with top/bottom fade affordances),
63
+ * and a pinned quick-action chip row sits above the composer. The first few
64
+ * quick actions render as chips; the remainder collapse under a "⋯" menu.
65
+ *
66
+ * Mirrors `MingoWelcome` (the default-mode empty state) — content is
67
+ * configurable with OpenFrame defaults.
68
+ */
69
+ export function GuideWelcome({
70
+ title = DEFAULT_TITLE,
71
+ subtitle = DEFAULT_SUBTITLE,
72
+ quickActions = [],
73
+ maxVisibleQuickActions = 3,
74
+ onQuickAction,
75
+ children,
76
+ className,
77
+ }: GuideWelcomeProps) {
78
+ // Scroll-fade affordances: a 48px gradient at the top/bottom edge of the
79
+ // scroll region, shown only while content is actually hidden in that
80
+ // direction. (Same behaviour as MingoWelcome.)
81
+ const scrollRef = React.useRef<HTMLDivElement>(null)
82
+ const [scrollFade, setScrollFade] = React.useState({ top: false, bottom: false })
83
+ const updateScrollFade = React.useCallback(() => {
84
+ const el = scrollRef.current
85
+ if (!el) return
86
+ const top = el.scrollTop > 1
87
+ const bottom = el.scrollTop + el.clientHeight < el.scrollHeight - 1
88
+ setScrollFade((prev) =>
89
+ prev.top === top && prev.bottom === bottom ? prev : { top, bottom },
90
+ )
91
+ }, [])
92
+ React.useEffect(() => {
93
+ updateScrollFade()
94
+ const el = scrollRef.current
95
+ if (!el || typeof ResizeObserver === 'undefined') return
96
+ const ro = new ResizeObserver(updateScrollFade)
97
+ ro.observe(el)
98
+ return () => ro.disconnect()
99
+ }, [updateScrollFade])
100
+
101
+ const visibleActions = quickActions.slice(0, maxVisibleQuickActions)
102
+ const overflowActions = quickActions.slice(maxVisibleQuickActions)
103
+
104
+ return (
105
+ <div
106
+ className={cn(
107
+ 'flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)]',
108
+ className,
109
+ )}
110
+ >
111
+ {/* Greeting + slash-command list share one scroll region; `relative` so
112
+ the edge fades can overlay it. */}
113
+ <div className="relative flex flex-1 min-h-0 flex-col">
114
+ <div
115
+ ref={scrollRef}
116
+ onScroll={updateScrollFade}
117
+ className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto"
118
+ >
119
+ {/* Greeting grows to fill (`flex-1`) so it centres vertically while
120
+ the list stays anchored below it. */}
121
+ <div className="flex flex-1 flex-col items-center justify-center gap-[var(--spacing-system-l)] px-[var(--spacing-system-l)] py-[var(--spacing-system-xxl)] text-center">
122
+ <MingoIcon
123
+ className="h-12 w-12"
124
+ color="white"
125
+ eyesColor="var(--ods-flamingo-cyan-base)"
126
+ cornerColor="var(--ods-flamingo-cyan-base)"
127
+ />
128
+ <div className="flex w-full flex-col gap-1">
129
+ <p className="text-h4 text-ods-text-primary">{title}</p>
130
+ <p className="text-h6 text-ods-text-secondary">{subtitle}</p>
131
+ </div>
132
+ </div>
133
+
134
+ {children}
135
+ </div>
136
+
137
+ {/* Top scroll-fade — visible only when content is hidden above. */}
138
+ <div
139
+ aria-hidden
140
+ className={cn(
141
+ 'pointer-events-none absolute inset-x-0 top-0 h-12 transition-opacity duration-150',
142
+ scrollFade.top ? 'opacity-100' : 'opacity-0',
143
+ )}
144
+ style={{
145
+ background:
146
+ 'linear-gradient(0deg, transparent 0%, var(--color-bg) 100%)',
147
+ }}
148
+ />
149
+ {/* Bottom scroll-fade — visible only when content is hidden below. */}
150
+ <div
151
+ aria-hidden
152
+ className={cn(
153
+ 'pointer-events-none absolute inset-x-0 bottom-0 h-12 transition-opacity duration-150',
154
+ scrollFade.bottom ? 'opacity-100' : 'opacity-0',
155
+ )}
156
+ style={{
157
+ background:
158
+ 'linear-gradient(180deg, transparent 0%, var(--color-bg) 100%)',
159
+ }}
160
+ />
161
+ </div>
162
+
163
+ {/* Pinned quick-action chips — always visible above the composer. */}
164
+ {quickActions.length > 0 && (
165
+ <div className="flex shrink-0 flex-wrap items-center gap-1">
166
+ {visibleActions.map((action) => (
167
+ <button
168
+ key={action.id}
169
+ type="button"
170
+ onClick={() => onQuickAction?.(action)}
171
+ className="rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
172
+ >
173
+ <Tag variant="outline" label={action.label} />
174
+ </button>
175
+ ))}
176
+ {overflowActions.length > 0 && (
177
+ <MoreActionsMenu
178
+ ariaLabel="More quick actions"
179
+ onCloseAutoFocus={(e) => e.preventDefault()}
180
+ items={overflowActions.map((action) => ({
181
+ label: action.label,
182
+ onClick: () => onQuickAction?.(action),
183
+ }))}
184
+ trigger={
185
+ <button
186
+ type="button"
187
+ aria-label="More quick actions"
188
+ className="rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
189
+ >
190
+ <Tag variant="outline" label={<Ellipsis01Icon size={16} />} />
191
+ </button>
192
+ }
193
+ />
194
+ )}
195
+ </div>
196
+ )}
197
+ </div>
198
+ )
199
+ }
@@ -0,0 +1,227 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useMemo, useState } from 'react'
4
+ import type { DialogItem } from '../types/component.types'
5
+ import type {
6
+ FetchDialogsParams,
7
+ FetchDialogsResult,
8
+ } from './use-nats-chat-adapter'
9
+
10
+ export interface UseChatDialogManagerArgs {
11
+ /** Active (non-archived) dialogs from the unified chat state. */
12
+ dialogs: ReadonlyArray<DialogItem>
13
+ /** Currently-open dialog id. */
14
+ activeDialogId: string | null | undefined
15
+ /** Open a dialog by id. */
16
+ selectDialog: (id: string | null) => void
17
+ /** Reset the open conversation (drops back to the list / empty state). */
18
+ clearMessages: () => void
19
+ /** Rename a dialog (wired through the active adapter). */
20
+ renameDialog: (id: string, title: string) => Promise<void>
21
+ /** Archive a dialog (wired through the active adapter). */
22
+ archiveDialog: (id: string) => Promise<void>
23
+ /** Host callback that pages archived dialogs — gates the archive UI. */
24
+ fetchArchivedDialogs?: (
25
+ params: FetchDialogsParams,
26
+ ) => Promise<FetchDialogsResult>
27
+ /** Host callback that restores an archived dialog — gates the restore UI. */
28
+ unarchiveDialog?: (id: string) => Promise<void>
29
+ }
30
+
31
+ /**
32
+ * Owns the chat panel's dialog-history concerns so `EmbeddableChat` stays an
33
+ * orchestrator rather than a state dump:
34
+ * - the Chat Archive page (open / paginate the archived list),
35
+ * - read-only "archived conversation" mode (open from the archive, restore,
36
+ * back-to-archive navigation),
37
+ * - the Rename / Archive / Unarchive confirmation-modal targets + handlers.
38
+ *
39
+ * Returns flat fields (intentionally the same names the JSX already uses) so
40
+ * the consuming component reads as a thin wiring layer.
41
+ */
42
+ export function useChatDialogManager({
43
+ dialogs,
44
+ activeDialogId,
45
+ selectDialog,
46
+ clearMessages,
47
+ renameDialog,
48
+ archiveDialog,
49
+ fetchArchivedDialogs,
50
+ unarchiveDialog,
51
+ }: UseChatDialogManagerArgs) {
52
+ // ─── Rename / Archive confirmation modals ───
53
+ const [renameTarget, setRenameTarget] = useState<DialogItem | null>(null)
54
+ const [archiveTarget, setArchiveTarget] = useState<DialogItem | null>(null)
55
+ const handleConfirmRename = useCallback(
56
+ (name: string) => {
57
+ if (renameTarget) void renameDialog(renameTarget.id, name)
58
+ setRenameTarget(null)
59
+ },
60
+ [renameTarget, renameDialog],
61
+ )
62
+ const handleConfirmArchive = useCallback(async () => {
63
+ if (archiveTarget) {
64
+ try {
65
+ await archiveDialog(archiveTarget.id)
66
+ } catch (err) {
67
+ // Keep the modal open (don't clear the target) so the user can retry;
68
+ // don't tear down the open conversation for an archive that failed.
69
+ console.error('[useChatDialogManager] archive failed:', err)
70
+ return
71
+ }
72
+ // Archiving the open conversation drops back to the chat list.
73
+ if (archiveTarget.id === activeDialogId) {
74
+ setOpeningDialogId(null)
75
+ clearMessages()
76
+ }
77
+ }
78
+ setArchiveTarget(null)
79
+ }, [archiveTarget, archiveDialog, activeDialogId, clearMessages])
80
+
81
+ // ─── Chat Archive page ───
82
+ // The archived list is paged locally (it's a secondary, on-demand view).
83
+ const [archiveOpen, setArchiveOpen] = useState(false)
84
+ const [archivedDialogs, setArchivedDialogs] = useState<DialogItem[]>([])
85
+ const [archivedCursor, setArchivedCursor] = useState<string | null>(null)
86
+ const [archivedLoading, setArchivedLoading] = useState(false)
87
+ const loadArchivedPage = useCallback(
88
+ async (cursor?: string): Promise<void> => {
89
+ if (!fetchArchivedDialogs) return
90
+ setArchivedLoading(true)
91
+ try {
92
+ const result = await fetchArchivedDialogs({ cursor, limit: 20 })
93
+ setArchivedCursor(result.nextCursor)
94
+ setArchivedDialogs((prev) =>
95
+ cursor ? [...prev, ...result.dialogs] : result.dialogs,
96
+ )
97
+ } catch (err) {
98
+ console.error('[useChatDialogManager] fetchArchivedDialogs failed:', err)
99
+ } finally {
100
+ setArchivedLoading(false)
101
+ }
102
+ },
103
+ [fetchArchivedDialogs],
104
+ )
105
+ const openArchive = useCallback(() => {
106
+ setArchiveOpen(true)
107
+ setArchivedDialogs([])
108
+ setArchivedCursor(null)
109
+ void loadArchivedPage()
110
+ }, [loadArchivedPage])
111
+ const closeArchive = useCallback(() => setArchiveOpen(false), [])
112
+
113
+ // ─── Read-only "archived conversation" mode ───
114
+ // Set when a chat is opened FROM the archive page — flips the open
115
+ // conversation into read-only mode (restore button + disabled composer).
116
+ const [viewingArchivedId, setViewingArchivedId] = useState<string | null>(
117
+ null,
118
+ )
119
+
120
+ // ─── Open-conversation tracking ───
121
+ // `openingDialogId` is set synchronously the moment a dialog is selected from
122
+ // the current-chats list, so the panel can flip to the conversation surface
123
+ // immediately — matching the archived-chat open (`viewingArchivedId` above)
124
+ // instead of waiting for `messages` to finish loading. Without it the normal
125
+ // open lagged behind the archived one (grey → load → flip vs. instant flip).
126
+ const [openingDialogId, setOpeningDialogId] = useState<string | null>(null)
127
+ const handleSelectDialog = useCallback(
128
+ (id: string) => {
129
+ setViewingArchivedId(null)
130
+ setOpeningDialogId(id)
131
+ selectDialog(id)
132
+ },
133
+ [selectDialog],
134
+ )
135
+ const isOpeningDialog =
136
+ openingDialogId != null && openingDialogId === activeDialogId
137
+
138
+ const handleArchivedSelect = useCallback(
139
+ (id: string) => {
140
+ setArchiveOpen(false)
141
+ setOpeningDialogId(null)
142
+ setViewingArchivedId(id)
143
+ selectDialog(id)
144
+ },
145
+ [selectDialog],
146
+ )
147
+ const [restoreTarget, setRestoreTarget] = useState<DialogItem | null>(null)
148
+ const handleConfirmRestore = useCallback(async () => {
149
+ if (restoreTarget) {
150
+ try {
151
+ await unarchiveDialog?.(restoreTarget.id)
152
+ } catch (err) {
153
+ // Backend restore failed — leave the local archived cache untouched so
154
+ // the dialog doesn't vanish from the archive only to resurface on the
155
+ // next fetch. Keep the modal open for a retry.
156
+ console.error('[useChatDialogManager] unarchive failed:', err)
157
+ return
158
+ }
159
+ // Drop it from the locally-cached archived list and exit read-only mode
160
+ // so the composer returns and the user can keep chatting.
161
+ setArchivedDialogs((prev) => prev.filter((d) => d.id !== restoreTarget.id))
162
+ setViewingArchivedId((cur) => (cur === restoreTarget.id ? null : cur))
163
+ }
164
+ setRestoreTarget(null)
165
+ }, [restoreTarget, unarchiveDialog])
166
+
167
+ const isViewingArchived =
168
+ viewingArchivedId != null && viewingArchivedId === activeDialogId
169
+
170
+ // Header back-chevron: from an archived chat, return to the Chat Archive
171
+ // page (not the current-chats list); otherwise reset to the list.
172
+ const handleBack = useCallback(() => {
173
+ const wasArchived = isViewingArchived
174
+ setViewingArchivedId(null)
175
+ setOpeningDialogId(null)
176
+ // Reset the adapter's active dialog id (not just the message buffer):
177
+ // `clearMessages` empties `messages` but leaves `activeDialogId` pointing at
178
+ // the just-closed dialog, so re-selecting that same dialog from the list is
179
+ // a no-op the history-load effect skips → a blank conversation. Selecting
180
+ // `null` lets the next selection register as a real change and reload.
181
+ selectDialog(null)
182
+ clearMessages()
183
+ if (wasArchived) setArchiveOpen(true)
184
+ }, [isViewingArchived, selectDialog, clearMessages])
185
+
186
+ // Active-conversation dialog — resolved from the active list, or the
187
+ // archived list when an archived chat is open (archived dialogs aren't in
188
+ // `dialogs`, so the header title and ⋯ / restore actions need this).
189
+ const activeDialog = useMemo(
190
+ () =>
191
+ dialogs.find((d) => d.id === activeDialogId) ??
192
+ archivedDialogs.find((d) => d.id === activeDialogId),
193
+ [dialogs, archivedDialogs, activeDialogId],
194
+ )
195
+
196
+ return {
197
+ // capability passthroughs (gate the archive / restore affordances)
198
+ fetchArchivedDialogs,
199
+ unarchiveDialog,
200
+ // archive page
201
+ archiveOpen,
202
+ archivedDialogs,
203
+ archivedCursor,
204
+ archivedLoading,
205
+ openArchive,
206
+ closeArchive,
207
+ loadArchivedPage,
208
+ handleArchivedSelect,
209
+ // open conversation (current-chats list)
210
+ handleSelectDialog,
211
+ isOpeningDialog,
212
+ // archived conversation
213
+ isViewingArchived,
214
+ handleBack,
215
+ activeDialog,
216
+ // action modals
217
+ renameTarget,
218
+ setRenameTarget,
219
+ archiveTarget,
220
+ setArchiveTarget,
221
+ restoreTarget,
222
+ setRestoreTarget,
223
+ handleConfirmRename,
224
+ handleConfirmArchive,
225
+ handleConfirmRestore,
226
+ }
227
+ }
@@ -191,6 +191,19 @@ export interface UseNatsChatAdapterConfig {
191
191
  */
192
192
  fetchChunks?: FetchChunksFunction
193
193
 
194
+ /**
195
+ * NATS topics to live-tail for the active dialog. Each maps to the
196
+ * subject suffix `chat.{dialogId}.{topic}` (see
197
+ * `useNatsDialogSubscription`). Defaults to `['message']` — the
198
+ * client-chat subject the Tauri Fae Chat consumer relies on. Admin /
199
+ * Mingo chat publishes its agent replies on `'admin-message'`, so the
200
+ * openframe EmbeddableChat host MUST set `topics: ['admin-message']`
201
+ * here — otherwise the subscription tails the wrong subject, no reply
202
+ * chunks ever arrive, and the assistant placeholder hangs forever in
203
+ * the `thinking` phase.
204
+ */
205
+ topics?: NatsMessageType[]
206
+
194
207
  /**
195
208
  * Whether THINKING chunks are surfaced as segments. Default `false`
196
209
  * (parity with the existing `useRealtimeChunkProcessor` default).
@@ -241,6 +254,25 @@ export interface UseNatsChatAdapterConfig {
241
254
  * delete affordance is hidden. */
242
255
  deleteDialog?: (dialogId: string) => Promise<void>
243
256
 
257
+ /** Rename a dialog on the backend. When omitted, the "Rename" affordance
258
+ * in the chat-history row menu is hidden. */
259
+ renameDialog?: (dialogId: string, title: string) => Promise<void>
260
+
261
+ /** Archive a dialog on the backend (removes it from the active list).
262
+ * When omitted, the "Archive" affordance in the row menu is hidden. */
263
+ archiveDialog?: (dialogId: string) => Promise<void>
264
+
265
+ /** Fetch a paginated page of ARCHIVED dialogs for the Chat Archive page.
266
+ * When omitted, the archive (clock-history) button in the header is
267
+ * hidden. Same page/cursor contract as `fetchDialogs`. */
268
+ fetchArchivedDialogs?: (
269
+ params: FetchDialogsParams,
270
+ ) => Promise<FetchDialogsResult>
271
+
272
+ /** Restore an archived dialog back to the active list. When omitted, the
273
+ * restore (refresh) button in an archived chat's header is hidden. */
274
+ unarchiveDialog?: (dialogId: string) => Promise<void>
275
+
244
276
  /**
245
277
  * Approve a pending tool-call request. Wired into the segment
246
278
  * accumulator's `onApprove` so approval-card buttons fire this
@@ -383,12 +415,15 @@ export function useNatsChatAdapter(
383
415
  clientConfig,
384
416
  publishUserMessage,
385
417
  fetchChunks,
418
+ topics,
386
419
  enableThinking,
387
420
  batchApprovalsEnabled,
388
421
  fetchDialogs,
389
422
  fetchDialogMessages,
390
423
  createDialog: createDialogCallback,
391
424
  deleteDialog: deleteDialogCallback,
425
+ renameDialog: renameDialogCallback,
426
+ archiveDialog: archiveDialogCallback,
392
427
  approveRequest: approveRequestCallback,
393
428
  rejectRequest: rejectRequestCallback,
394
429
  stopGeneration: stopGenerationCallback,
@@ -676,6 +711,10 @@ export function useNatsChatAdapter(
676
711
  useNatsDialogSubscription({
677
712
  enabled: active && dialogId != null,
678
713
  dialogId,
714
+ // Subject suffix to tail. Omitted → the hook defaults to `['message']`
715
+ // (client chat). Admin/Mingo hosts pass `['admin-message']` so the
716
+ // live tail lands on the subject the agent actually publishes to.
717
+ ...(topics ? { topics } : {}),
679
718
  getNatsWsUrl,
680
719
  clientConfig,
681
720
  onEvent: (payload: unknown, messageType: NatsMessageType) => {
@@ -844,6 +883,48 @@ export function useNatsChatAdapter(
844
883
  [deleteDialogCallback, internalDialogId, isManagedMode],
845
884
  )
846
885
 
886
+ const renameDialog = useCallback(
887
+ async (id: string, title: string): Promise<void> => {
888
+ if (!renameDialogCallback) return
889
+ // Optimistic — update the local title immediately, roll back on error.
890
+ let previous: string | undefined
891
+ setDialogs((prev) =>
892
+ prev.map((d) => {
893
+ if (d.id !== id) return d
894
+ previous = d.title
895
+ return { ...d, title }
896
+ }),
897
+ )
898
+ try {
899
+ await renameDialogCallback(id, title)
900
+ } catch (err) {
901
+ console.error('[useNatsChatAdapter] renameDialog failed:', err)
902
+ if (previous !== undefined) {
903
+ setDialogs((prev) =>
904
+ prev.map((d) => (d.id === id ? { ...d, title: previous! } : d)),
905
+ )
906
+ }
907
+ }
908
+ },
909
+ [renameDialogCallback],
910
+ )
911
+
912
+ const archiveDialog = useCallback(
913
+ async (id: string): Promise<void> => {
914
+ if (!archiveDialogCallback) return
915
+ try {
916
+ await archiveDialogCallback(id)
917
+ setDialogs((prev) => prev.filter((d) => d.id !== id))
918
+ if (isManagedMode && internalDialogId === id) {
919
+ setInternalDialogId(null)
920
+ }
921
+ } catch (err) {
922
+ console.error('[useNatsChatAdapter] archiveDialog failed:', err)
923
+ }
924
+ },
925
+ [archiveDialogCallback, internalDialogId, isManagedMode],
926
+ )
927
+
847
928
  const loadMoreDialogs = useCallback(async (): Promise<void> => {
848
929
  if (!dialogsNextCursor) return
849
930
  await loadDialogsPage(dialogsNextCursor)
@@ -906,6 +987,8 @@ export function useNatsChatAdapter(
906
987
  selectDialog,
907
988
  startNewDialog,
908
989
  deleteDialog,
990
+ renameDialog,
991
+ archiveDialog,
909
992
  isDialogsLoading: isDialogsLoading || isCreatingDialog,
910
993
  isMessagesLoading,
911
994
  hasMoreDialogs,
@@ -934,6 +1017,8 @@ export function useNatsChatAdapter(
934
1017
  selectDialog,
935
1018
  startNewDialog,
936
1019
  deleteDialog,
1020
+ renameDialog,
1021
+ archiveDialog,
937
1022
  isDialogsLoading,
938
1023
  isCreatingDialog,
939
1024
  isMessagesLoading,
@@ -1071,6 +1071,8 @@ export function useSseChatAdapter(
1071
1071
  selectDialog: noopSelectDialog,
1072
1072
  startNewDialog: noopStartNewDialog,
1073
1073
  deleteDialog: noopDeleteDialog,
1074
+ renameDialog: noopRenameDialog,
1075
+ archiveDialog: noopArchiveDialog,
1074
1076
  isDialogsLoading: false,
1075
1077
  isMessagesLoading: false,
1076
1078
  hasMoreDialogs: false,
@@ -1096,6 +1098,12 @@ const noopStartNewDialog = async (): Promise<string | null> => null
1096
1098
  const noopDeleteDialog = async (_id: string): Promise<void> => {
1097
1099
  /* no-op until Guide localStorage history is exposed */
1098
1100
  }
1101
+ const noopRenameDialog = async (_id: string, _title: string): Promise<void> => {
1102
+ /* no-op until Guide localStorage history is exposed */
1103
+ }
1104
+ const noopArchiveDialog = async (_id: string): Promise<void> => {
1105
+ /* no-op until Guide localStorage history is exposed */
1106
+ }
1099
1107
  const noopAsync = async (): Promise<void> => {
1100
1108
  /* no-op pagination stub */
1101
1109
  }
@@ -157,6 +157,14 @@ export function useUnifiedChat(
157
157
  (id: string) => activeState.deleteDialog(id),
158
158
  [activeState],
159
159
  )
160
+ const renameDialog = useCallback(
161
+ (id: string, title: string) => activeState.renameDialog(id, title),
162
+ [activeState],
163
+ )
164
+ const archiveDialog = useCallback(
165
+ (id: string) => activeState.archiveDialog(id),
166
+ [activeState],
167
+ )
160
168
  const loadMoreDialogs = useCallback(
161
169
  () => activeState.loadMoreDialogs(),
162
170
  [activeState],
@@ -198,6 +206,8 @@ export function useUnifiedChat(
198
206
  selectDialog,
199
207
  startNewDialog,
200
208
  deleteDialog,
209
+ renameDialog,
210
+ archiveDialog,
201
211
  isDialogsLoading: activeState.isDialogsLoading,
202
212
  isMessagesLoading: activeState.isMessagesLoading,
203
213
  hasMoreDialogs: activeState.hasMoreDialogs,
@@ -221,6 +231,8 @@ export function useUnifiedChat(
221
231
  selectDialog,
222
232
  startNewDialog,
223
233
  deleteDialog,
234
+ renameDialog,
235
+ archiveDialog,
224
236
  loadMoreDialogs,
225
237
  loadMoreMessages,
226
238
  approveRequest,