@flamingo-stack/openframe-frontend-core 0.0.275 → 0.0.276

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 (68) hide show
  1. package/dist/{chunk-5LOLBUZG.cjs → chunk-2GLHJ7ZY.cjs} +12 -12
  2. package/dist/{chunk-5LOLBUZG.cjs.map → chunk-2GLHJ7ZY.cjs.map} +1 -1
  3. package/dist/{chunk-EINXNG5D.js → chunk-62U6DFIT.js} +3 -3
  4. package/dist/{chunk-OKDCUJOZ.cjs → chunk-6IJWWLC3.cjs} +10 -10
  5. package/dist/{chunk-OKDCUJOZ.cjs.map → chunk-6IJWWLC3.cjs.map} +1 -1
  6. package/dist/{chunk-DBJZF2OV.cjs → chunk-DPMW5E6Q.cjs} +15 -15
  7. package/dist/{chunk-DBJZF2OV.cjs.map → chunk-DPMW5E6Q.cjs.map} +1 -1
  8. package/dist/{chunk-DEAWHNYW.cjs → chunk-IAB7QACX.cjs} +677 -640
  9. package/dist/chunk-IAB7QACX.cjs.map +1 -0
  10. package/dist/{chunk-XCPBD2PG.cjs → chunk-LH7R56RR.cjs} +2 -2
  11. package/dist/chunk-LH7R56RR.cjs.map +1 -0
  12. package/dist/{chunk-E6RCVOQG.cjs → chunk-MYJKC56A.cjs} +38 -38
  13. package/dist/{chunk-E6RCVOQG.cjs.map → chunk-MYJKC56A.cjs.map} +1 -1
  14. package/dist/{chunk-T4XXYAYY.js → chunk-NITJSVUM.js} +3 -3
  15. package/dist/{chunk-CTBL56EC.js → chunk-NPKMH446.js} +3 -3
  16. package/dist/{chunk-NY7OG2OJ.js → chunk-QNWFTSAG.js} +3 -3
  17. package/dist/{chunk-SA4ZTD3G.cjs → chunk-SITWWJWS.cjs} +27 -27
  18. package/dist/{chunk-SA4ZTD3G.cjs.map → chunk-SITWWJWS.cjs.map} +1 -1
  19. package/dist/{chunk-FDHKJJPK.js → chunk-UCX75VEM.js} +2 -2
  20. package/dist/{chunk-EKWPIEHH.js → chunk-VAIPAZPJ.js} +43 -6
  21. package/dist/{chunk-EKWPIEHH.js.map → chunk-VAIPAZPJ.js.map} +1 -1
  22. package/dist/{chunk-GHWOQUB4.js → chunk-ZS4XNIZK.js} +2 -2
  23. package/dist/chunk-ZS4XNIZK.js.map +1 -0
  24. package/dist/components/chat/index.cjs +3 -3
  25. package/dist/components/chat/index.js +2 -2
  26. package/dist/components/chat/utils/history-merge.d.ts.map +1 -1
  27. package/dist/components/contact/index.cjs +4 -4
  28. package/dist/components/contact/index.js +3 -3
  29. package/dist/components/faq/index.cjs +4 -4
  30. package/dist/components/faq/index.js +3 -3
  31. package/dist/components/features/board/board.d.ts.map +1 -1
  32. package/dist/components/features/board/ticket-card.d.ts +2 -1
  33. package/dist/components/features/board/ticket-card.d.ts.map +1 -1
  34. package/dist/components/features/board/types.d.ts +1 -0
  35. package/dist/components/features/board/types.d.ts.map +1 -1
  36. package/dist/components/features/index.cjs +3 -3
  37. package/dist/components/features/index.js +2 -2
  38. package/dist/components/index.cjs +85 -85
  39. package/dist/components/index.js +6 -6
  40. package/dist/components/navigation/index.cjs +3 -3
  41. package/dist/components/navigation/index.js +2 -2
  42. package/dist/components/onboarding-guides/index.cjs +22 -22
  43. package/dist/components/onboarding-guides/index.js +2 -2
  44. package/dist/components/related-content/index.cjs +4 -4
  45. package/dist/components/related-content/index.js +3 -3
  46. package/dist/components/tickets/index.cjs +61 -61
  47. package/dist/components/tickets/index.js +4 -4
  48. package/dist/components/ui/actions-menu.d.ts +6 -0
  49. package/dist/components/ui/actions-menu.d.ts.map +1 -1
  50. package/dist/components/ui/index.cjs +3 -3
  51. package/dist/components/ui/index.js +2 -2
  52. package/dist/index.cjs +3 -3
  53. package/dist/index.js +2 -2
  54. package/package.json +1 -1
  55. package/src/components/chat/utils/history-merge.ts +27 -0
  56. package/src/components/features/board/board-column.tsx +1 -0
  57. package/src/components/features/board/board.tsx +6 -1
  58. package/src/components/features/board/ticket-card.tsx +23 -6
  59. package/src/components/features/board/types.ts +1 -0
  60. package/src/components/ui/actions-menu.tsx +11 -1
  61. package/dist/chunk-DEAWHNYW.cjs.map +0 -1
  62. package/dist/chunk-GHWOQUB4.js.map +0 -1
  63. package/dist/chunk-XCPBD2PG.cjs.map +0 -1
  64. /package/dist/{chunk-EINXNG5D.js.map → chunk-62U6DFIT.js.map} +0 -0
  65. /package/dist/{chunk-T4XXYAYY.js.map → chunk-NITJSVUM.js.map} +0 -0
  66. /package/dist/{chunk-CTBL56EC.js.map → chunk-NPKMH446.js.map} +0 -0
  67. /package/dist/{chunk-NY7OG2OJ.js.map → chunk-QNWFTSAG.js.map} +0 -0
  68. /package/dist/{chunk-FDHKJJPK.js.map → chunk-UCX75VEM.js.map} +0 -0
@@ -41,6 +41,20 @@ function isSyntheticRealtimeId(id: string): boolean {
41
41
  return SYNTHETIC_REALTIME_ID_PREFIXES.some((prefix) => id.startsWith(prefix))
42
42
  }
43
43
 
44
+ /** Rendered answer text of an assistant message (TEXT segments, or a plain string). Used to
45
+ * recognise a replayed synthetic as the twin of an already-persisted turn when the stream-seq
46
+ * signal can't prove it (see the trailing-turn fallback below). Empty for tool/approval-only
47
+ * turns that carry no answer text — callers must treat "" as "no signal", never as a match. */
48
+ function assistantAnswerText(content: MessageContent): string {
49
+ if (typeof content === 'string') return content.trim()
50
+ if (!Array.isArray(content)) return ''
51
+ const parts: string[] = []
52
+ for (const seg of content) {
53
+ if (seg.type === 'text') parts.push(seg.text)
54
+ }
55
+ return parts.join('\n').trim()
56
+ }
57
+
44
58
  /** Flattens DESC-sorted message pages (newest page first, newest message
45
59
  * first within a page) into one chronological list. */
46
60
  export function flattenMessagePagesChronological<T>(pages: readonly { messages: readonly T[] }[] | undefined): T[] {
@@ -170,6 +184,14 @@ export function mergeHistoryWithRealtime<M extends MergeableChatMessage>(input:
170
184
  }
171
185
  }
172
186
 
187
+ // Content-equality fallback for the trailing turn. The backend stamps `lastChunkStreamSeq`
188
+ // asynchronously, so a just-finished assistant can land in history with a seq BELOW the replay's
189
+ // terminal chunk seq; seq coverage then reads "not covered" and the replayed synthetic survives
190
+ // next to its persisted twin. Recognise the twin by its rendered answer text.
191
+ const lastProcessed = processedToUse[processedToUse.length - 1]
192
+ const trailingAssistantText =
193
+ lastProcessed && lastProcessed.role === 'assistant' ? assistantAnswerText(lastProcessed.content) : ''
194
+
173
195
  const realtimeMessages = existingMessages.filter((m) => {
174
196
  // Pin wins over everything: the twin may carry a persisted Mongo id (the
175
197
  // chunk processors ADOPT an in-progress trailing assistant after a prior
@@ -196,6 +218,11 @@ export function mergeHistoryWithRealtime<M extends MergeableChatMessage>(input:
196
218
  // (JetStream resumes after the highest seq this client has consumed).
197
219
  // Decided by seq coverage when known, wall-clock otherwise.
198
220
  if (isSyntheticRealtimeId(m.id) && m.id !== streamingMessageId) {
221
+ // A non-streaming synthetic that re-renders the trailing persisted assistant verbatim is its
222
+ // twin no matter what the seq signal says — drop it (covers the persistence-lag gap).
223
+ if (m.role === 'assistant' && trailingAssistantText && assistantAnswerText(m.content) === trailingAssistantText) {
224
+ return false
225
+ }
199
226
  const covered =
200
227
  historyCoversRealtime !== null ? historyCoversRealtime : (m.timestamp?.getTime() ?? 0) <= historyFetchedAt
201
228
  if (covered) return false
@@ -147,6 +147,7 @@ function ColumnBody({ column, getTicketHref, renderAssignSlot, onLoadMore, loadM
147
147
  key={t.id}
148
148
  ticket={t}
149
149
  columnId={column.id}
150
+ columnColor={column.color}
150
151
  href={getTicketHref?.(t.id)}
151
152
  renderAssignSlot={renderAssignSlot}
152
153
  dragDisabled={column.dragDisabled}
@@ -302,7 +302,12 @@ export function Board({
302
302
  </div>
303
303
  <DragOverlay dropAnimation={null}>
304
304
  {activeTicket ? (
305
- <TicketCard ticket={activeTicket.ticket} columnId={activeTicket.columnId} isOverlay />
305
+ <TicketCard
306
+ ticket={activeTicket.ticket}
307
+ columnId={activeTicket.columnId}
308
+ columnColor={items.find(c => c.id === activeTicket.columnId)?.color}
309
+ isOverlay
310
+ />
306
311
  ) : null}
307
312
  </DragOverlay>
308
313
  </DndContext>
@@ -4,11 +4,12 @@ import { useSortable } from '@dnd-kit/sortable'
4
4
  import { CSS } from '@dnd-kit/utilities'
5
5
  import Link from '../../../embed-shims/next-link'
6
6
  import * as React from 'react'
7
- import { LaptopIcon, Flag02Icon } from '../../icons-v2-generated'
7
+ import { LaptopIcon, Flag02Icon, MessagesIcon } from '../../icons-v2-generated'
8
8
  import { SquareAvatar } from '../../ui/square-avatar'
9
9
  import { Tag } from '../../ui/tag'
10
10
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/tooltip'
11
11
  import { cn } from '../../../utils/cn'
12
+ import { getReadableTextColor } from '../../../utils/ods-color-utils'
12
13
  import { formatTicketRelativeTime, formatTicketFullTimestamp } from '../../../utils/date-utils'
13
14
  import type { BoardPriority, BoardTicket } from './types'
14
15
 
@@ -25,6 +26,7 @@ const MAX_VISIBLE_ASSIGNEES = 3
25
26
  export interface TicketCardProps {
26
27
  ticket: BoardTicket
27
28
  columnId: string
29
+ columnColor?: string
28
30
  href?: string
29
31
  isOverlay?: boolean
30
32
  dragDisabled?: boolean
@@ -34,6 +36,7 @@ export interface TicketCardProps {
34
36
  export function TicketCard({
35
37
  ticket,
36
38
  columnId,
39
+ columnColor,
37
40
  href,
38
41
  isOverlay = false,
39
42
  dragDisabled,
@@ -49,10 +52,16 @@ export function TicketCard({
49
52
  disabled: dragDisabled,
50
53
  })
51
54
 
52
- const style: React.CSSProperties = {
53
- transform: CSS.Transform.toString(sortable.transform),
54
- transition: sortable.transition,
55
- }
55
+ const showNewMessage = !!ticket.hasNewMessage && !!columnColor
56
+ const newMessageTextColor = columnColor ? getReadableTextColor(columnColor) : undefined
57
+
58
+ const style: React.CSSProperties = isOverlay
59
+ ? {}
60
+ : {
61
+ transform: CSS.Transform.toString(sortable.transform),
62
+ transition: sortable.transition,
63
+ }
64
+ if (showNewMessage) style.borderColor = columnColor
56
65
 
57
66
  const showDeviceRow = !!(ticket.deviceHostnames?.length || ticket.organizationName)
58
67
  const deviceText = [
@@ -126,6 +135,14 @@ export function TicketCard({
126
135
  </Tooltip>
127
136
  </TooltipProvider>
128
137
  )}
138
+ {showNewMessage && (
139
+ <Tag
140
+ label="New Message"
141
+ icon={<MessagesIcon size={16} color={newMessageTextColor} />}
142
+ className="w-fit shrink-0"
143
+ style={{ backgroundColor: columnColor, color: newMessageTextColor }}
144
+ />
145
+ )}
129
146
  </>
130
147
  )
131
148
 
@@ -138,7 +155,7 @@ export function TicketCard({
138
155
 
139
156
  const outerProps = {
140
157
  ref: isOverlay ? undefined : sortable.setNodeRef,
141
- style: isOverlay ? undefined : style,
158
+ style,
142
159
  className: cardClasses,
143
160
  ...(isOverlay ? {} : sortable.attributes),
144
161
  ...(isOverlay ? {} : sortable.listeners),
@@ -21,6 +21,7 @@ export interface BoardTicket {
21
21
  assignees?: BoardTicketAssignee[]
22
22
  tags?: string[]
23
23
  createdAt?: string
24
+ hasNewMessage?: boolean
24
25
  }
25
26
 
26
27
  export interface BoardColumnDef {
@@ -31,6 +31,12 @@ export interface ActionsMenuItem {
31
31
  disabled?: boolean;
32
32
  type?: "item" | "checkbox" | "submenu" | "separator";
33
33
  checked?: boolean;
34
+ /**
35
+ * Keep the dropdown open after this item is clicked instead of closing it
36
+ * (e.g. multi-select add). Only affects ActionsMenuDropdown; `checkbox` and
37
+ * `submenu` items always keep the menu open regardless. Defaults to closing.
38
+ */
39
+ closeOnSelect?: boolean;
34
40
  submenu?: ActionsMenuItem[];
35
41
  /** Render the row in the error/destructive color (label + icon). */
36
42
  danger?: boolean;
@@ -373,7 +379,11 @@ export const ActionsMenuDropdown: React.FC<ActionsMenuDropdownProps> = ({
373
379
  const handleItemClick = useCallback(
374
380
  (item: ActionsMenuItem) => {
375
381
  onItemClick?.(item);
376
- if (item.type !== "checkbox" && item.type !== "submenu") {
382
+ if (
383
+ item.type !== "checkbox" &&
384
+ item.type !== "submenu" &&
385
+ item.closeOnSelect !== false
386
+ ) {
377
387
  setOpen(false);
378
388
  }
379
389
  },