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

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-EDW2NVRV.js → chunk-4WZOFD46.js} +37 -37
  2. package/dist/{chunk-EDW2NVRV.js.map → chunk-4WZOFD46.js.map} +1 -1
  3. package/dist/{chunk-ZGBXHK26.cjs → chunk-5GN7TXHY.cjs} +12 -12
  4. package/dist/{chunk-ZGBXHK26.cjs.map → chunk-5GN7TXHY.cjs.map} +1 -1
  5. package/dist/{chunk-F3FO2ZZZ.cjs → chunk-BAKZF4GU.cjs} +7 -7
  6. package/dist/{chunk-F3FO2ZZZ.cjs.map → chunk-BAKZF4GU.cjs.map} +1 -1
  7. package/dist/{chunk-MPHDM2VZ.cjs → chunk-BCL24DFU.cjs} +30 -30
  8. package/dist/{chunk-MPHDM2VZ.cjs.map → chunk-BCL24DFU.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-SZXKKEUH.cjs → chunk-E6B4B7GM.cjs} +46 -30
  12. package/dist/chunk-E6B4B7GM.cjs.map +1 -0
  13. package/dist/{chunk-SRA2QYK6.js → chunk-HUA4XG4S.js} +4 -4
  14. package/dist/{chunk-A3PL6ZCF.js → chunk-KXF3WCPH.js} +6397 -5128
  15. package/dist/chunk-KXF3WCPH.js.map +1 -0
  16. package/dist/{chunk-SL3RGBPX.cjs → chunk-QNYH3WUU.cjs} +9 -9
  17. package/dist/{chunk-SL3RGBPX.cjs.map → chunk-QNYH3WUU.cjs.map} +1 -1
  18. package/dist/{chunk-24Q2WLIU.js → chunk-QYRV6MKX.js} +2 -2
  19. package/dist/{chunk-XG7DFRJL.js → chunk-RCECOGMI.js} +3 -3
  20. package/dist/{chunk-7UZLRI7W.cjs → chunk-SEECETJY.cjs} +3301 -2032
  21. package/dist/chunk-SEECETJY.cjs.map +1 -0
  22. package/dist/{chunk-ZII7TNVA.js → chunk-T5MEXJD5.js} +3 -3
  23. package/dist/{chunk-YX3YQNC4.cjs → chunk-W23DRJAA.cjs} +13 -13
  24. package/dist/{chunk-YX3YQNC4.cjs.map → chunk-W23DRJAA.cjs.map} +1 -1
  25. package/dist/{chunk-DRPECAXO.js → chunk-WR32ZE63.js} +2 -2
  26. package/dist/{chunk-Y3MXGCOW.js → chunk-YZDUOUMB.js} +46 -30
  27. package/dist/chunk-YZDUOUMB.js.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 +40 -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 +386 -311
  108. package/src/components/chat/guide-mode-banner.tsx +75 -0
  109. package/src/components/chat/guide-welcome.tsx +207 -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 +102 -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-SRA2QYK6.js.map → chunk-HUA4XG4S.js.map} +0 -0
  143. /package/dist/{chunk-24Q2WLIU.js.map → chunk-QYRV6MKX.js.map} +0 -0
  144. /package/dist/{chunk-XG7DFRJL.js.map → chunk-RCECOGMI.js.map} +0 -0
  145. /package/dist/{chunk-ZII7TNVA.js.map → chunk-T5MEXJD5.js.map} +0 -0
  146. /package/dist/{chunk-DRPECAXO.js.map → chunk-WR32ZE63.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,207 @@
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. The first `maxVisibleQuickActions` render inline; the
30
+ * rest collapse under a trailing "⋯" overflow menu. */
31
+ quickActions?: ReadonlyArray<GuideQuickAction>
32
+ /** How many chips to show inline before overflowing to the menu (default 3). */
33
+ maxVisibleQuickActions?: number
34
+ /** Fired when a quick-action chip (inline or menu) is activated. */
35
+ onQuickAction?: (action: GuideQuickAction) => void
36
+ /** Slash-command onboarding list — rendered inside the shared scroll region
37
+ * below the greeting (so greeting + list scroll together, with edge fades). */
38
+ children?: React.ReactNode
39
+ className?: string
40
+ }
41
+
42
+ // =============================================================================
43
+ // Defaults (OpenFrame copy — overridable; the kit stays platform-agnostic)
44
+ // =============================================================================
45
+
46
+ const DEFAULT_TITLE = 'Guide Mode Chat'
47
+
48
+ const DEFAULT_SUBTITLE =
49
+ 'This chat is temporary and will not be saved. Ask about OpenFrame docs, ' +
50
+ 'known issues, or manage your support tickets right here.'
51
+
52
+ const DEFAULT_QUICK_ACTIONS: ReadonlyArray<GuideQuickAction> = [
53
+ { id: 'how-to-start', label: 'How to start' },
54
+ { id: 'connect-device', label: 'Connect device' },
55
+ { id: 'find-device', label: 'Find device' },
56
+ { id: 'remote-connection', label: 'Remote connection' },
57
+ { id: 'run-scripts', label: 'Run scripts' },
58
+ { id: 'device-software', label: 'Device software' },
59
+ { id: 'bulk-update', label: 'Bulk update' },
60
+ ]
61
+
62
+ // =============================================================================
63
+ // Component
64
+ // =============================================================================
65
+
66
+ /**
67
+ * GuideWelcome — Figma node `7532:328214`.
68
+ *
69
+ * Guide-mode chat empty state: a centred greeting and the slash-command
70
+ * onboarding list share one scroll region (with top/bottom fade affordances),
71
+ * and a pinned quick-action chip row sits above the composer. The first few
72
+ * quick actions render as chips; the remainder collapse under a "⋯" menu.
73
+ *
74
+ * Mirrors `MingoWelcome` (the default-mode empty state) — content is
75
+ * configurable with OpenFrame defaults.
76
+ */
77
+ export function GuideWelcome({
78
+ title = DEFAULT_TITLE,
79
+ subtitle = DEFAULT_SUBTITLE,
80
+ quickActions = DEFAULT_QUICK_ACTIONS,
81
+ maxVisibleQuickActions = 3,
82
+ onQuickAction,
83
+ children,
84
+ className,
85
+ }: GuideWelcomeProps) {
86
+ // Scroll-fade affordances: a 48px gradient at the top/bottom edge of the
87
+ // scroll region, shown only while content is actually hidden in that
88
+ // direction. (Same behaviour as MingoWelcome.)
89
+ const scrollRef = React.useRef<HTMLDivElement>(null)
90
+ const [scrollFade, setScrollFade] = React.useState({ top: false, bottom: false })
91
+ const updateScrollFade = React.useCallback(() => {
92
+ const el = scrollRef.current
93
+ if (!el) return
94
+ const top = el.scrollTop > 1
95
+ const bottom = el.scrollTop + el.clientHeight < el.scrollHeight - 1
96
+ setScrollFade((prev) =>
97
+ prev.top === top && prev.bottom === bottom ? prev : { top, bottom },
98
+ )
99
+ }, [])
100
+ React.useEffect(() => {
101
+ updateScrollFade()
102
+ const el = scrollRef.current
103
+ if (!el || typeof ResizeObserver === 'undefined') return
104
+ const ro = new ResizeObserver(updateScrollFade)
105
+ ro.observe(el)
106
+ return () => ro.disconnect()
107
+ }, [updateScrollFade])
108
+
109
+ const visibleActions = quickActions.slice(0, maxVisibleQuickActions)
110
+ const overflowActions = quickActions.slice(maxVisibleQuickActions)
111
+
112
+ return (
113
+ <div
114
+ className={cn(
115
+ 'flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)]',
116
+ className,
117
+ )}
118
+ >
119
+ {/* Greeting + slash-command list share one scroll region; `relative` so
120
+ the edge fades can overlay it. */}
121
+ <div className="relative flex flex-1 min-h-0 flex-col">
122
+ <div
123
+ ref={scrollRef}
124
+ onScroll={updateScrollFade}
125
+ className="flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto"
126
+ >
127
+ {/* Greeting grows to fill (`flex-1`) so it centres vertically while
128
+ the list stays anchored below it. */}
129
+ <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">
130
+ <MingoIcon
131
+ className="h-12 w-12"
132
+ color="white"
133
+ eyesColor="var(--ods-flamingo-cyan-base)"
134
+ cornerColor="var(--ods-flamingo-cyan-base)"
135
+ />
136
+ <div className="flex w-full flex-col gap-1">
137
+ <p className="text-h4 text-ods-text-primary">{title}</p>
138
+ <p className="text-h6 text-ods-text-secondary">{subtitle}</p>
139
+ </div>
140
+ </div>
141
+
142
+ {children}
143
+ </div>
144
+
145
+ {/* Top scroll-fade — visible only when content is hidden above. */}
146
+ <div
147
+ aria-hidden
148
+ className={cn(
149
+ 'pointer-events-none absolute inset-x-0 top-0 h-12 transition-opacity duration-150',
150
+ scrollFade.top ? 'opacity-100' : 'opacity-0',
151
+ )}
152
+ style={{
153
+ background:
154
+ 'linear-gradient(0deg, transparent 0%, var(--color-bg) 100%)',
155
+ }}
156
+ />
157
+ {/* Bottom scroll-fade — visible only when content is hidden below. */}
158
+ <div
159
+ aria-hidden
160
+ className={cn(
161
+ 'pointer-events-none absolute inset-x-0 bottom-0 h-12 transition-opacity duration-150',
162
+ scrollFade.bottom ? 'opacity-100' : 'opacity-0',
163
+ )}
164
+ style={{
165
+ background:
166
+ 'linear-gradient(180deg, transparent 0%, var(--color-bg) 100%)',
167
+ }}
168
+ />
169
+ </div>
170
+
171
+ {/* Pinned quick-action chips — always visible above the composer. */}
172
+ {quickActions.length > 0 && (
173
+ <div className="flex shrink-0 flex-wrap items-center gap-1">
174
+ {visibleActions.map((action) => (
175
+ <button
176
+ key={action.id}
177
+ type="button"
178
+ onClick={() => onQuickAction?.(action)}
179
+ className="rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
180
+ >
181
+ <Tag variant="outline" label={action.label} />
182
+ </button>
183
+ ))}
184
+ {overflowActions.length > 0 && (
185
+ <MoreActionsMenu
186
+ ariaLabel="More quick actions"
187
+ onCloseAutoFocus={(e) => e.preventDefault()}
188
+ items={overflowActions.map((action) => ({
189
+ label: action.label,
190
+ onClick: () => onQuickAction?.(action),
191
+ }))}
192
+ trigger={
193
+ <button
194
+ type="button"
195
+ aria-label="More quick actions"
196
+ className="rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent"
197
+ >
198
+ <Tag variant="outline" label={<Ellipsis01Icon size={16} />} />
199
+ </button>
200
+ }
201
+ />
202
+ )}
203
+ </div>
204
+ )}
205
+ </div>
206
+ )
207
+ }
@@ -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
  }