@flamingo-stack/openframe-frontend-core 0.0.488 → 0.0.489-snapshot.20260728173516
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-GQX6LFFA.cjs → chunk-2MKUHJ3O.cjs} +42 -42
- package/dist/{chunk-GQX6LFFA.cjs.map → chunk-2MKUHJ3O.cjs.map} +1 -1
- package/dist/{chunk-DX6AOHNU.cjs → chunk-3IEVA5AO.cjs} +77 -77
- package/dist/{chunk-DX6AOHNU.cjs.map → chunk-3IEVA5AO.cjs.map} +1 -1
- package/dist/{chunk-PNXNWGBY.js → chunk-6CFFISUC.js} +2 -2
- package/dist/{chunk-F6BYUO5U.js → chunk-6CJ2K7BD.js} +3 -3
- package/dist/{chunk-KFQXO7QB.js → chunk-6GT557KN.js} +1 -1
- package/dist/chunk-6GT557KN.js.map +1 -0
- package/dist/{chunk-AJNDDXKC.js → chunk-6QK3ONDD.js} +3 -3
- package/dist/{chunk-H7JX2BG7.cjs → chunk-7I42V2OD.cjs} +75 -75
- package/dist/{chunk-H7JX2BG7.cjs.map → chunk-7I42V2OD.cjs.map} +1 -1
- package/dist/{chunk-AHDJMEEN.js → chunk-AIRNG27G.js} +7 -7
- package/dist/{chunk-RZSA6MZL.cjs → chunk-AW63ET2A.cjs} +15 -15
- package/dist/{chunk-RZSA6MZL.cjs.map → chunk-AW63ET2A.cjs.map} +1 -1
- package/dist/{chunk-A7IPON2V.cjs → chunk-BIKOJ4FH.cjs} +5 -5
- package/dist/{chunk-A7IPON2V.cjs.map → chunk-BIKOJ4FH.cjs.map} +1 -1
- package/dist/{chunk-S52LJNMU.js → chunk-BZMOEWWS.js} +7 -7
- package/dist/{chunk-SIKCXKGB.cjs → chunk-D5LQMRPU.cjs} +7 -7
- package/dist/{chunk-SIKCXKGB.cjs.map → chunk-D5LQMRPU.cjs.map} +1 -1
- package/dist/{chunk-HQYJINFV.js → chunk-EGXCWZYP.js} +4 -4
- package/dist/{chunk-EUYIFALO.cjs → chunk-EJQUYZXF.cjs} +14 -14
- package/dist/{chunk-EUYIFALO.cjs.map → chunk-EJQUYZXF.cjs.map} +1 -1
- package/dist/{chunk-V3CNNMY3.cjs → chunk-FY5XAW3H.cjs} +18 -18
- package/dist/{chunk-V3CNNMY3.cjs.map → chunk-FY5XAW3H.cjs.map} +1 -1
- package/dist/{chunk-FOQV6RCO.cjs → chunk-GMPYR3MX.cjs} +29 -29
- package/dist/{chunk-FOQV6RCO.cjs.map → chunk-GMPYR3MX.cjs.map} +1 -1
- package/dist/{chunk-EYEIVYYF.cjs → chunk-GUTYUMGV.cjs} +7 -7
- package/dist/{chunk-EYEIVYYF.cjs.map → chunk-GUTYUMGV.cjs.map} +1 -1
- package/dist/{chunk-FZOBQG6M.cjs → chunk-IEZ3YBAP.cjs} +35 -35
- package/dist/{chunk-FZOBQG6M.cjs.map → chunk-IEZ3YBAP.cjs.map} +1 -1
- package/dist/{chunk-PECGZWQR.cjs → chunk-KP5KRRD6.cjs} +3 -3
- package/dist/{chunk-PECGZWQR.cjs.map → chunk-KP5KRRD6.cjs.map} +1 -1
- package/dist/{chunk-2B6773QM.cjs → chunk-LILQJ3UZ.cjs} +1 -1
- package/dist/chunk-LILQJ3UZ.cjs.map +1 -0
- package/dist/{chunk-SCCTJ2UI.js → chunk-MPYDX5WX.js} +4 -4
- package/dist/{chunk-YJSKQ2ZV.js → chunk-NGEFYPY7.js} +3 -3
- package/dist/{chunk-77DFPD35.js → chunk-OQIMH2WQ.js} +5515 -5377
- package/dist/chunk-OQIMH2WQ.js.map +1 -0
- package/dist/{chunk-KLYHZRAD.js → chunk-PB3BTZDU.js} +2 -2
- package/dist/{chunk-CWQACI4Z.js → chunk-RW2LPL72.js} +2 -2
- package/dist/{chunk-YEEHB4T6.cjs → chunk-UZZT65PQ.cjs} +40 -40
- package/dist/{chunk-YEEHB4T6.cjs.map → chunk-UZZT65PQ.cjs.map} +1 -1
- package/dist/{chunk-LLWBPPZ4.js → chunk-VDZO6IDR.js} +6 -6
- package/dist/{chunk-3BFX6HJM.js → chunk-VL2YMDPH.js} +6 -6
- package/dist/{chunk-5RXKOGH6.cjs → chunk-VUXNXWOQ.cjs} +496 -358
- package/dist/chunk-VUXNXWOQ.cjs.map +1 -0
- package/dist/{chunk-PXF5J24T.js → chunk-WLM3F3WU.js} +2 -2
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/guide-display.d.ts +14 -0
- package/dist/components/chat/guide-display.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-chat-history-hydration.d.ts +47 -0
- package/dist/components/chat/hooks/use-chat-history-hydration.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-chat.d.ts +1 -0
- package/dist/components/chat/hooks/use-chat.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-realtime-chunk-processor.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +4 -0
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +6 -4
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +5 -3
- package/dist/components/chat/types/component.types.d.ts +6 -0
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +15 -2
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +3 -0
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +5 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chat-conversation-storage.d.ts +34 -0
- package/dist/components/chat/utils/chat-conversation-storage.d.ts.map +1 -0
- package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +5 -0
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +142 -140
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +14 -12
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/index.cjs +6 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +5 -3
- package/dist/contexts/chat-runtime-context.d.ts +8 -0
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +6 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -3
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/chat-message-enhanced.tsx +63 -52
- package/src/components/chat/guide-display.tsx +44 -0
- package/src/components/chat/hooks/use-chat-history-hydration.ts +142 -0
- package/src/components/chat/hooks/use-chat.ts +12 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +2 -0
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +10 -0
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +109 -162
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/types/component.types.ts +9 -0
- package/src/components/chat/types/message.types.ts +17 -1
- package/src/components/chat/types/processing.types.ts +1 -0
- package/src/components/chat/types/unified-chat-state.types.ts +6 -0
- package/src/components/chat/utils/chat-conversation-storage.ts +83 -0
- package/src/components/chat/utils/chunk-parser.ts +6 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +19 -0
- package/src/components/chat/utils/process-historical-messages.ts +6 -0
- package/src/contexts/chat-runtime-context.tsx +8 -0
- package/src/stories/ChatMessageEnhanced.stories.tsx +48 -1
- package/dist/chunk-2B6773QM.cjs.map +0 -1
- package/dist/chunk-5RXKOGH6.cjs.map +0 -1
- package/dist/chunk-77DFPD35.js.map +0 -1
- package/dist/chunk-KFQXO7QB.js.map +0 -1
- /package/dist/{chunk-PNXNWGBY.js.map → chunk-6CFFISUC.js.map} +0 -0
- /package/dist/{chunk-F6BYUO5U.js.map → chunk-6CJ2K7BD.js.map} +0 -0
- /package/dist/{chunk-AJNDDXKC.js.map → chunk-6QK3ONDD.js.map} +0 -0
- /package/dist/{chunk-AHDJMEEN.js.map → chunk-AIRNG27G.js.map} +0 -0
- /package/dist/{chunk-S52LJNMU.js.map → chunk-BZMOEWWS.js.map} +0 -0
- /package/dist/{chunk-HQYJINFV.js.map → chunk-EGXCWZYP.js.map} +0 -0
- /package/dist/{chunk-SCCTJ2UI.js.map → chunk-MPYDX5WX.js.map} +0 -0
- /package/dist/{chunk-YJSKQ2ZV.js.map → chunk-NGEFYPY7.js.map} +0 -0
- /package/dist/{chunk-KLYHZRAD.js.map → chunk-PB3BTZDU.js.map} +0 -0
- /package/dist/{chunk-CWQACI4Z.js.map → chunk-RW2LPL72.js.map} +0 -0
- /package/dist/{chunk-LLWBPPZ4.js.map → chunk-VDZO6IDR.js.map} +0 -0
- /package/dist/{chunk-3BFX6HJM.js.map → chunk-VL2YMDPH.js.map} +0 -0
- /package/dist/{chunk-PXF5J24T.js.map → chunk-WLM3F3WU.js.map} +0 -0
|
@@ -40,17 +40,21 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { useState, useEffect, useCallback, useMemo, useRef, type MutableRefObject } from 'react'
|
|
43
|
-
import { useChat, type
|
|
43
|
+
import { useChat, type StreamFnExtraOptions } from './use-chat'
|
|
44
44
|
import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
|
|
45
45
|
import type { ChatRef } from '../chat-ref.types'
|
|
46
46
|
import { buildChatRefKey } from '../types/chat.types'
|
|
47
47
|
import type { MessageSegment } from '../types/message.types'
|
|
48
48
|
import { useSlashCommandRegistry, type SlashCommandSummary } from './use-slash-commands'
|
|
49
|
-
import { getChatProxyAuth } from '../utils/chat-proxy-auth-storage'
|
|
50
49
|
import { chatAuthedFetch } from '../utils/chat-authed-fetch'
|
|
50
|
+
import {
|
|
51
|
+
createChatConversationStorage,
|
|
52
|
+
pruneStaleChatConversationStorage,
|
|
53
|
+
type PersistedChatConversation,
|
|
54
|
+
} from '../utils/chat-conversation-storage'
|
|
55
|
+
import type { LocalStorageAdapter } from '../../../utils/local-storage-adapter'
|
|
56
|
+
import { useChatHistoryHydration } from './use-chat-history-hydration'
|
|
51
57
|
import { parseScrollAnchor, type ScrollAnchor } from '../utils/scroll-anchor'
|
|
52
|
-
import { AUTO_CONTINUATION_DIRECTIVE_PREFIX } from '../utils/auto-continuation-directive'
|
|
53
|
-
import { flattenAssistantContent } from '../utils/flatten-assistant-content'
|
|
54
58
|
import { sanitizeTitleForChat } from '../utils/slash-dispatch-utils'
|
|
55
59
|
import { defaultTableIdForDocumentType } from '../utils/source-icons'
|
|
56
60
|
import type {
|
|
@@ -146,6 +150,10 @@ export interface ChatTurnMeta {
|
|
|
146
150
|
/** Routing decision from the server's `decideRoute`. */
|
|
147
151
|
routedComplexity: string | null
|
|
148
152
|
routedThinkingBudget: number | null
|
|
153
|
+
/** Server-computed conversation trace id (`chat_conversations.id` for this
|
|
154
|
+
* thread), echoed in the leading metadata frame. Stable across turns of
|
|
155
|
+
* one thread — the correlation handle for transcripts/analytics. */
|
|
156
|
+
conversationId: string | null
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
/**
|
|
@@ -199,6 +207,7 @@ function createEmptyTurnMeta(): ChatTurnMeta {
|
|
|
199
207
|
scrollAnchor: null,
|
|
200
208
|
routedComplexity: null,
|
|
201
209
|
routedThinkingBudget: null,
|
|
210
|
+
conversationId: null,
|
|
202
211
|
}
|
|
203
212
|
}
|
|
204
213
|
|
|
@@ -215,13 +224,14 @@ function escapeThinkingTags(text: string): string {
|
|
|
215
224
|
function createDocStreamFn(
|
|
216
225
|
source: DocSource,
|
|
217
226
|
endpoints: { chatStreamUrl: string; approvalToolUrl: string },
|
|
218
|
-
messagesRef: MutableRefObject<Message[]>,
|
|
219
227
|
sourcesMapRef: MutableRefObject<Map<number, ChatSource[]>>,
|
|
220
228
|
refsMapRef: MutableRefObject<Map<number, Record<string, ChatRef>>>,
|
|
221
229
|
metaMapRef: MutableRefObject<Map<number, ChatTurnMeta>>,
|
|
222
230
|
setStreamingPhase: (phase: StreamingPhase) => void,
|
|
223
231
|
bumpMetaTick: () => void,
|
|
224
232
|
sendCountRef: MutableRefObject<number>,
|
|
233
|
+
conversationIdRef: MutableRefObject<string | null>,
|
|
234
|
+
conversationStorage: LocalStorageAdapter<PersistedChatConversation>,
|
|
225
235
|
) {
|
|
226
236
|
// CRITICAL: the decoder + buffer MUST live INSIDE the returned async-
|
|
227
237
|
// generator function (per-call closure), NOT at the factory level. A
|
|
@@ -233,22 +243,6 @@ function createDocStreamFn(
|
|
|
233
243
|
signal?: AbortSignal,
|
|
234
244
|
extra?: StreamFnExtraOptions,
|
|
235
245
|
): AsyncGenerator<MessageSegment> {
|
|
236
|
-
const currentMessages = messagesRef.current || []
|
|
237
|
-
// Filter `hidden:true` messages out of the API history. The approval-
|
|
238
|
-
// action turn injects a hidden user message with `content=''`.
|
|
239
|
-
// `flattenAssistantContent` joins text-segment arrays into a single
|
|
240
|
-
// string so the server sees the receipt + auto-continuation Qs.
|
|
241
|
-
const apiMessages = [
|
|
242
|
-
...currentMessages
|
|
243
|
-
.filter((m) => (m.role === 'user' || m.role === 'assistant') && !m.hidden)
|
|
244
|
-
.map((m) => ({
|
|
245
|
-
role: m.role,
|
|
246
|
-
content:
|
|
247
|
-
typeof m.content === 'string' ? m.content : flattenAssistantContent(m.content),
|
|
248
|
-
})),
|
|
249
|
-
{ role: 'user', content: message },
|
|
250
|
-
]
|
|
251
|
-
|
|
252
246
|
// URL + body branch — approvalAction routes to the approval-tool
|
|
253
247
|
// endpoint, the standard chat path routes to the chat-stream endpoint.
|
|
254
248
|
const targetPath = extra?.approvalAction
|
|
@@ -258,26 +252,30 @@ function createDocStreamFn(
|
|
|
258
252
|
// it server-side via its own platform-detection — tamper-proof binding
|
|
259
253
|
// so a client on one platform can't POST a different platform's
|
|
260
254
|
// conversation.
|
|
255
|
+
//
|
|
256
|
+
// The server is the single source of conversation history: it re-reads
|
|
257
|
+
// `chat_messages` by conversation id on every turn. The wire therefore
|
|
258
|
+
// carries ONLY the new user message — never the prior conversation.
|
|
259
|
+
// The conversation id is SERVER-minted: the first message of a session
|
|
260
|
+
// sends none, the server mints one and echoes it in the leading metadata
|
|
261
|
+
// frame (captured below into `conversationIdRef`), and every later turn
|
|
262
|
+
// echoes it back.
|
|
263
|
+
const conversationId = conversationIdRef.current
|
|
261
264
|
const requestBody = extra?.approvalAction
|
|
262
265
|
? {
|
|
263
266
|
proposal_id: extra.approvalAction.proposalId,
|
|
264
267
|
action: extra.approvalAction.action,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
role: m.role,
|
|
269
|
-
content:
|
|
270
|
-
typeof m.content === 'string'
|
|
271
|
-
? m.content
|
|
272
|
-
: flattenAssistantContent(m.content),
|
|
273
|
-
})),
|
|
268
|
+
// Always present here — an approval can only happen inside an
|
|
269
|
+
// established conversation (the proposal turn captured the id).
|
|
270
|
+
conversationId,
|
|
274
271
|
}
|
|
275
272
|
: {
|
|
276
|
-
messages:
|
|
273
|
+
messages: [{ role: 'user', content: message }],
|
|
277
274
|
...(extra?.commandOverride ? { commandOverride: extra.commandOverride } : {}),
|
|
278
275
|
...(extra?.pendingAttachments && extra.pendingAttachments.length > 0
|
|
279
276
|
? { pendingAttachments: extra.pendingAttachments }
|
|
280
277
|
: {}),
|
|
278
|
+
...(conversationId ? { conversationId } : {}),
|
|
281
279
|
}
|
|
282
280
|
// `chatAuthedFetch` carries the bearer-act-as headers (+ Supabase
|
|
283
281
|
// session cookies) — same wrapper `use-chat-attachments` and
|
|
@@ -489,6 +487,18 @@ function createDocStreamFn(
|
|
|
489
487
|
})
|
|
490
488
|
bumpMetaTick()
|
|
491
489
|
}
|
|
490
|
+
// Server-minted conversation id, echoed on every turn. On the
|
|
491
|
+
// session's FIRST turn this is where the client learns its id —
|
|
492
|
+
// capture it and persist it so later turns and future visits
|
|
493
|
+
// continue the same conversation.
|
|
494
|
+
if (typeof meta.conversationId === 'string' && meta.conversationId) {
|
|
495
|
+
if (conversationIdRef.current !== meta.conversationId) {
|
|
496
|
+
conversationIdRef.current = meta.conversationId
|
|
497
|
+
conversationStorage.save({ conversationId: meta.conversationId })
|
|
498
|
+
}
|
|
499
|
+
mergeTurnMeta(metaMapRef, sendIdx, { conversationId: meta.conversationId })
|
|
500
|
+
bumpMetaTick()
|
|
501
|
+
}
|
|
492
502
|
const parsedAnchor = parseScrollAnchor(meta.scrollAnchor)
|
|
493
503
|
if (parsedAnchor !== null) {
|
|
494
504
|
mergeTurnMeta(metaMapRef, sendIdx, { scrollAnchor: parsedAnchor })
|
|
@@ -587,94 +597,25 @@ function mergeTurnMeta(
|
|
|
587
597
|
}
|
|
588
598
|
|
|
589
599
|
// =============================================================================
|
|
590
|
-
//
|
|
600
|
+
// Local persistence — the server-issued conversation id ONLY
|
|
591
601
|
// =============================================================================
|
|
602
|
+
//
|
|
603
|
+
// The ONLY thing persisted locally is the conversation id the SERVER minted
|
|
604
|
+
// and echoed in the leading metadata frame of the session's first turn.
|
|
605
|
+
// The client never generates ids. Message history lives server-side in
|
|
606
|
+
// `chat_conversations` / `chat_messages` (recorded turn-by-turn by the chat
|
|
607
|
+
// route) and is rehydrated on mount via `GET <chatStreamUrl>/history`.
|
|
608
|
+
// localStorage never stores messages, sources, refs, or send counts —
|
|
609
|
+
// the server transcript is the single source of truth for history.
|
|
592
610
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
/** localStorage history namespace used when no `source` is configured on the
|
|
611
|
+
/** localStorage namespace used when no `source` is configured on the
|
|
596
612
|
* runtime. Embedders are platform-agnostic (see `ChatRuntime.source`), so any
|
|
597
613
|
* stable string works here — the hub passes its real platform instead. */
|
|
598
614
|
const DEFAULT_CHAT_SOURCE = 'embed'
|
|
599
615
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const base = `mingo-chat-${source}-v${CHAT_STORAGE_VERSION}`
|
|
604
|
-
const auth = getChatProxyAuth()
|
|
605
|
-
if (auth?.email) {
|
|
606
|
-
return `${base}-u-${encodeURIComponent(auth.email.toLowerCase())}`
|
|
607
|
-
}
|
|
608
|
-
return base
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/** Sweep stale per-user chat-history keys. Drops any key whose email
|
|
612
|
-
* differs from the CURRENT proxy-auth identity. */
|
|
613
|
-
function pruneStaleChatStorage(source: DocSource): void {
|
|
614
|
-
if (typeof window === 'undefined') return
|
|
615
|
-
try {
|
|
616
|
-
const currentKey = chatStorageKey(source)
|
|
617
|
-
const prefix = `mingo-chat-${source}-v${CHAT_STORAGE_VERSION}-u-`
|
|
618
|
-
const legacy = `mingo-chat-${source}-v${CHAT_STORAGE_VERSION}`
|
|
619
|
-
const toRemove: string[] = []
|
|
620
|
-
for (let i = 0; i < window.localStorage.length; i++) {
|
|
621
|
-
const k = window.localStorage.key(i)
|
|
622
|
-
if (!k) continue
|
|
623
|
-
if (!k.startsWith(prefix)) continue
|
|
624
|
-
if (k === currentKey) continue
|
|
625
|
-
if (k === legacy) continue
|
|
626
|
-
toRemove.push(k)
|
|
627
|
-
}
|
|
628
|
-
for (const k of toRemove) {
|
|
629
|
-
window.localStorage.removeItem(k)
|
|
630
|
-
}
|
|
631
|
-
} catch {
|
|
632
|
-
// localStorage access blocked (Safari private mode etc.) — non-fatal.
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
interface PersistedChatState {
|
|
637
|
-
messages: Message[]
|
|
638
|
-
sources: Array<[number, ChatSource[]]>
|
|
639
|
-
/** Per-turn refs for inline object cards. */
|
|
640
|
-
refs?: Array<[number, Record<string, ChatRef>]>
|
|
641
|
-
sendCount: number
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function loadPersistedChat(source: DocSource): PersistedChatState | null {
|
|
645
|
-
if (typeof window === 'undefined') return null
|
|
646
|
-
try {
|
|
647
|
-
const raw = window.localStorage.getItem(chatStorageKey(source))
|
|
648
|
-
if (!raw) return null
|
|
649
|
-
const parsed = JSON.parse(raw) as PersistedChatState
|
|
650
|
-
if (!parsed || !Array.isArray(parsed.messages)) return null
|
|
651
|
-
// Rehydrate Date objects + run forward-compat migrations.
|
|
652
|
-
for (const m of parsed.messages) {
|
|
653
|
-
if (typeof m.timestamp === 'string') m.timestamp = new Date(m.timestamp)
|
|
654
|
-
// Forward-migration for auto-continuation directive bubbles.
|
|
655
|
-
if (
|
|
656
|
-
m.role === 'user' &&
|
|
657
|
-
!m.hidden &&
|
|
658
|
-
typeof m.content === 'string' &&
|
|
659
|
-
m.content.startsWith(AUTO_CONTINUATION_DIRECTIVE_PREFIX)
|
|
660
|
-
) {
|
|
661
|
-
m.hidden = true
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return parsed
|
|
665
|
-
} catch {
|
|
666
|
-
return null
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function savePersistedChat(source: DocSource, state: PersistedChatState) {
|
|
671
|
-
if (typeof window === 'undefined') return
|
|
672
|
-
try {
|
|
673
|
-
window.localStorage.setItem(chatStorageKey(source), JSON.stringify(state))
|
|
674
|
-
} catch {
|
|
675
|
-
// Quota exceeded or private mode — silently drop.
|
|
676
|
-
}
|
|
677
|
-
}
|
|
616
|
+
// Persistence itself lives in `../utils/chat-conversation-storage.ts` (built
|
|
617
|
+
// on the lib-standard `createLocalStorageAdapter`); the mount-time history
|
|
618
|
+
// rebuild lives in `./use-chat-history-hydration.ts`.
|
|
678
619
|
|
|
679
620
|
// =============================================================================
|
|
680
621
|
// useSseChatAdapter — public hook
|
|
@@ -698,10 +639,10 @@ export function useSseChatAdapter(
|
|
|
698
639
|
// (hub) / embedder's provider must wrap the tree.
|
|
699
640
|
const runtime = useRequiredChatRuntime()
|
|
700
641
|
// `source` is OPTIONAL — embedders are platform-agnostic (see ChatRuntime.source).
|
|
701
|
-
// Here it's used ONLY for the localStorage
|
|
702
|
-
// labels; it is NEVER sent on the wire (the hub resolves source server-side
|
|
703
|
-
// currentPlatform()). Fall back to a stable constant so the persistence key
|
|
704
|
-
// well-formed when the embedder leaves source unset.
|
|
642
|
+
// Here it's used ONLY for the conversation-id localStorage namespace + client-side
|
|
643
|
+
// meta labels; it is NEVER sent on the wire (the hub resolves source server-side
|
|
644
|
+
// via currentPlatform()). Fall back to a stable constant so the persistence key
|
|
645
|
+
// stays well-formed when the embedder leaves source unset.
|
|
705
646
|
const source = runtime.source || DEFAULT_CHAT_SOURCE
|
|
706
647
|
// Fall back to the lib-baked hub-canonical map when the embedder
|
|
707
648
|
// didn't supply an override. Keeps Ask + Display working in any
|
|
@@ -711,23 +652,24 @@ export function useSseChatAdapter(
|
|
|
711
652
|
const tableIdForDocumentType =
|
|
712
653
|
options?.tableIdForDocumentType ?? defaultTableIdForDocumentType
|
|
713
654
|
|
|
714
|
-
// Restore
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
655
|
+
// Restore the server-issued conversation id once on mount. Null = no
|
|
656
|
+
// conversation yet — the FIRST send goes out without an id, the server
|
|
657
|
+
// mints one, and the metadata-frame capture (in the stream fn) stores it.
|
|
658
|
+
// The client NEVER generates ids. This is the ONLY local persistence —
|
|
659
|
+
// message history hydrates from the server transcript below.
|
|
660
|
+
const conversationStorage = useMemo(() => createChatConversationStorage(source), [source])
|
|
661
|
+
const conversationIdRef = useRef<string | null>(null)
|
|
662
|
+
const restoredConversationRef = useRef(false)
|
|
663
|
+
if (!restoredConversationRef.current) {
|
|
664
|
+
restoredConversationRef.current = true
|
|
665
|
+
pruneStaleChatConversationStorage(source)
|
|
666
|
+
conversationIdRef.current = conversationStorage.load()?.conversationId ?? null
|
|
720
667
|
}
|
|
721
668
|
|
|
722
|
-
const sourcesMapRef = useRef<Map<number, ChatSource[]>>(
|
|
723
|
-
|
|
724
|
-
)
|
|
725
|
-
const refsMapRef = useRef<Map<number, Record<string, ChatRef>>>(
|
|
726
|
-
new Map(persistedRef.current.refs ?? []),
|
|
727
|
-
)
|
|
669
|
+
const sourcesMapRef = useRef<Map<number, ChatSource[]>>(new Map())
|
|
670
|
+
const refsMapRef = useRef<Map<number, Record<string, ChatRef>>>(new Map())
|
|
728
671
|
const metaMapRef = useRef<Map<number, ChatTurnMeta>>(new Map())
|
|
729
|
-
const
|
|
730
|
-
const sendCountRef = useRef(persistedRef.current.sendCount)
|
|
672
|
+
const sendCountRef = useRef(0)
|
|
731
673
|
const [streamingPhase, setStreamingPhase] = useState<StreamingPhase>('idle')
|
|
732
674
|
const [metaTick, setMetaTick] = useState(0)
|
|
733
675
|
const bumpMetaTick = useCallback(() => setMetaTick((t) => t + 1), [])
|
|
@@ -740,19 +682,21 @@ export function useSseChatAdapter(
|
|
|
740
682
|
chatStreamUrl: runtime.endpoints.chatStreamUrl,
|
|
741
683
|
approvalToolUrl: runtime.endpoints.approvalToolUrl,
|
|
742
684
|
},
|
|
743
|
-
messagesRef,
|
|
744
685
|
sourcesMapRef,
|
|
745
686
|
refsMapRef,
|
|
746
687
|
metaMapRef,
|
|
747
688
|
setStreamingPhase,
|
|
748
689
|
bumpMetaTick,
|
|
749
690
|
sendCountRef,
|
|
691
|
+
conversationIdRef,
|
|
692
|
+
conversationStorage,
|
|
750
693
|
),
|
|
751
694
|
[
|
|
752
695
|
source,
|
|
753
696
|
runtime.endpoints.chatStreamUrl,
|
|
754
697
|
runtime.endpoints.approvalToolUrl,
|
|
755
698
|
bumpMetaTick,
|
|
699
|
+
conversationStorage,
|
|
756
700
|
],
|
|
757
701
|
)
|
|
758
702
|
|
|
@@ -792,20 +736,6 @@ export function useSseChatAdapter(
|
|
|
792
736
|
return map
|
|
793
737
|
}, [slashCommands])
|
|
794
738
|
|
|
795
|
-
// Persist on every messages change. Sources + sendCount live in refs,
|
|
796
|
-
// so we read their current values at write time.
|
|
797
|
-
const persist = useCallback(
|
|
798
|
-
(nextMessages: Message[]) => {
|
|
799
|
-
savePersistedChat(source, {
|
|
800
|
-
messages: nextMessages,
|
|
801
|
-
sources: Array.from(sourcesMapRef.current.entries()),
|
|
802
|
-
refs: Array.from(refsMapRef.current.entries()),
|
|
803
|
-
sendCount: sendCountRef.current,
|
|
804
|
-
})
|
|
805
|
-
},
|
|
806
|
-
[source],
|
|
807
|
-
)
|
|
808
|
-
|
|
809
739
|
const {
|
|
810
740
|
messages,
|
|
811
741
|
isTyping,
|
|
@@ -813,16 +743,32 @@ export function useSseChatAdapter(
|
|
|
813
743
|
sendMessage: chatSendMessage,
|
|
814
744
|
stopMessage: chatStopMessage,
|
|
815
745
|
clearMessages: chatClearMessages,
|
|
746
|
+
hydrateMessages,
|
|
816
747
|
hasMessages,
|
|
817
748
|
} = useChat({
|
|
818
749
|
useMock: false,
|
|
819
750
|
assistantName: 'Mingo AI',
|
|
820
751
|
streamFn,
|
|
821
|
-
initialMessages: persistedRef.current.messages,
|
|
822
|
-
onMessagesChange: persist,
|
|
823
752
|
})
|
|
824
753
|
|
|
825
|
-
|
|
754
|
+
// ─── Server hydration (history SSOT) ───
|
|
755
|
+
// Mount-time rebuild of the message list from the server transcript — the
|
|
756
|
+
// single store of conversation history. Extracted to its own hook; see
|
|
757
|
+
// `use-chat-history-hydration.ts` for the full contract + failure
|
|
758
|
+
// semantics (a miss starts the UI empty, never loses server context).
|
|
759
|
+
const historyUrl =
|
|
760
|
+
runtime.endpoints.chatHistoryUrl ??
|
|
761
|
+
`${runtime.endpoints.chatStreamUrl.replace(/\/+$/, '')}/history`
|
|
762
|
+
const { isHydratingHistory, hydratedKeyRef } = useChatHistoryHydration({
|
|
763
|
+
active,
|
|
764
|
+
source,
|
|
765
|
+
historyUrl,
|
|
766
|
+
conversationIdRef,
|
|
767
|
+
refsMapRef,
|
|
768
|
+
sendCountRef,
|
|
769
|
+
hydrateMessages,
|
|
770
|
+
bumpMetaTick,
|
|
771
|
+
})
|
|
826
772
|
|
|
827
773
|
// Index sources/refs/scrollAnchor by USER-SEND count (`sendIdx`), not
|
|
828
774
|
// by assistant-message count. Each user send produces exactly ONE
|
|
@@ -1008,17 +954,17 @@ export function useSseChatAdapter(
|
|
|
1008
954
|
refsMapRef.current.clear()
|
|
1009
955
|
metaMapRef.current.clear()
|
|
1010
956
|
sendCountRef.current = 0
|
|
957
|
+
// New chat = drop the stored conversation id. The old row stays frozen
|
|
958
|
+
// server-side; the NEXT send goes out id-less and the server mints a
|
|
959
|
+
// fresh conversation (echoed back and re-captured then).
|
|
960
|
+
conversationIdRef.current = null
|
|
961
|
+
conversationStorage.clear()
|
|
962
|
+
hydratedKeyRef.current = null
|
|
1011
963
|
setStreamingPhase('idle')
|
|
1012
964
|
// Force the latestMeta useMemo to recompute with the cleared map.
|
|
1013
965
|
bumpMetaTick()
|
|
1014
966
|
chatClearMessages()
|
|
1015
|
-
|
|
1016
|
-
if (typeof window !== 'undefined') {
|
|
1017
|
-
try {
|
|
1018
|
-
window.localStorage.removeItem(chatStorageKey(source))
|
|
1019
|
-
} catch {}
|
|
1020
|
-
}
|
|
1021
|
-
}, [chatClearMessages, source, bumpMetaTick])
|
|
967
|
+
}, [chatClearMessages, conversationStorage, hydratedKeyRef, bumpMetaTick])
|
|
1022
968
|
|
|
1023
969
|
// Reset to idle whenever both flags drop off.
|
|
1024
970
|
useEffect(() => {
|
|
@@ -1047,6 +993,8 @@ export function useSseChatAdapter(
|
|
|
1047
993
|
stopMessage,
|
|
1048
994
|
clearMessages,
|
|
1049
995
|
streamingPhase,
|
|
996
|
+
/** True while the mount-time rebuild from the server transcript runs. */
|
|
997
|
+
isHydratingHistory,
|
|
1050
998
|
/** Provider key for the lib's `<ModelDisplay>` icon. */
|
|
1051
999
|
currentProvider: latestMeta?.provider ?? null,
|
|
1052
1000
|
currentModelLabel: latestMeta?.modelLabel ?? null,
|
|
@@ -1061,12 +1009,11 @@ export function useSseChatAdapter(
|
|
|
1061
1009
|
* token counts). null until the trailing usage frame lands. */
|
|
1062
1010
|
currentUsageBreakdown: latestMeta?.breakdown ?? null,
|
|
1063
1011
|
// ─── Dialog management — stubs for v1 ────────────────────────────────
|
|
1064
|
-
// Guide mode
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1069
|
-
// `dialogs.length === 0`.
|
|
1012
|
+
// Guide mode keeps ONE server-side conversation per stored id
|
|
1013
|
+
// (`chat_conversations`, hydrated on mount). Surfacing multiple threads
|
|
1014
|
+
// as a structured dialog list is a follow-up; for now the shape is
|
|
1015
|
+
// satisfied with empty defaults so the unified contract type-checks and
|
|
1016
|
+
// EmbeddableChat hides sidebar affordances when `dialogs.length === 0`.
|
|
1070
1017
|
dialogs: SSE_EMPTY_DIALOGS,
|
|
1071
1018
|
activeDialogId: null,
|
|
1072
1019
|
selectDialog: noopSelectDialog,
|
|
@@ -6,6 +6,7 @@ export * from './approval-batch-message'
|
|
|
6
6
|
export * from './context-compaction-display'
|
|
7
7
|
export * from './expand-chevron'
|
|
8
8
|
export * from './thinking-display'
|
|
9
|
+
export * from './guide-display'
|
|
9
10
|
export * from './error-message-display'
|
|
10
11
|
export * from './chat-container'
|
|
11
12
|
export * from './chat-input'
|
|
@@ -556,6 +556,15 @@ export interface ThinkingDisplayProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
556
556
|
isStreaming?: boolean
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
// ========== Guide Display Props ==========
|
|
560
|
+
|
|
561
|
+
export interface GuideDisplayProps extends HTMLAttributes<HTMLDivElement> {
|
|
562
|
+
/** Rendered guide body. `ChatMessageEnhanced` passes the SAME markdown
|
|
563
|
+
* output a text segment gets, so `[card://]` cards and mention chips
|
|
564
|
+
* behave identically inside a guide. */
|
|
565
|
+
children?: ReactNode
|
|
566
|
+
}
|
|
567
|
+
|
|
559
568
|
// ========== Model Display Props ==========
|
|
560
569
|
|
|
561
570
|
export interface ModelDisplayProps extends HTMLAttributes<HTMLDivElement> {
|
|
@@ -10,6 +10,7 @@ import type { AssistantType, AuthorType, ChatApprovalStatus, MessageOwner } from
|
|
|
10
10
|
export const MESSAGE_TYPE = {
|
|
11
11
|
TEXT: 'TEXT',
|
|
12
12
|
THINKING: 'THINKING',
|
|
13
|
+
GUIDE: 'GUIDE',
|
|
13
14
|
EXECUTING_TOOL: 'EXECUTING_TOOL',
|
|
14
15
|
EXECUTED_TOOL: 'EXECUTED_TOOL',
|
|
15
16
|
APPROVAL_REQUEST: 'APPROVAL_REQUEST',
|
|
@@ -170,6 +171,15 @@ export type ThinkingSegment = {
|
|
|
170
171
|
text: string
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
/** Guide answer body — the assistant's how-to/documentation reply, rendered as
|
|
175
|
+
* a titled "OpenFrame Guide" card instead of a bare paragraph. `text` is
|
|
176
|
+
* markdown, streamed in fragments like a `text` segment and coalesced by the
|
|
177
|
+
* accumulator. */
|
|
178
|
+
export type GuideSegment = {
|
|
179
|
+
type: 'guide'
|
|
180
|
+
text: string
|
|
181
|
+
}
|
|
182
|
+
|
|
173
183
|
export type ToolExecutionSegment = {
|
|
174
184
|
type: 'tool_execution'
|
|
175
185
|
data: ToolExecutionData
|
|
@@ -208,7 +218,7 @@ export type ContextCompactionSegment = {
|
|
|
208
218
|
summary?: string
|
|
209
219
|
}
|
|
210
220
|
|
|
211
|
-
export type MessageSegment = TextSegment | ThinkingSegment | ToolExecutionSegment | ApprovalRequestSegment | ApprovalBatchSegment | ErrorSegment | ContextCompactionSegment
|
|
221
|
+
export type MessageSegment = TextSegment | ThinkingSegment | GuideSegment | ToolExecutionSegment | ApprovalRequestSegment | ApprovalBatchSegment | ErrorSegment | ContextCompactionSegment
|
|
212
222
|
|
|
213
223
|
export type MessageContent = string | MessageSegment[]
|
|
214
224
|
|
|
@@ -228,6 +238,11 @@ export interface ThinkingMessageData extends MessageDataBase {
|
|
|
228
238
|
text?: string
|
|
229
239
|
}
|
|
230
240
|
|
|
241
|
+
export interface GuideMessageData extends MessageDataBase {
|
|
242
|
+
type: 'GUIDE'
|
|
243
|
+
text?: string
|
|
244
|
+
}
|
|
245
|
+
|
|
231
246
|
export interface ExecutingToolMessageData extends MessageDataBase {
|
|
232
247
|
type: 'EXECUTING_TOOL'
|
|
233
248
|
integratedToolType?: string
|
|
@@ -309,6 +324,7 @@ export interface ContextCompactionEndMessageData extends MessageDataBase {
|
|
|
309
324
|
export type MessageData =
|
|
310
325
|
| TextMessageData
|
|
311
326
|
| ThinkingMessageData
|
|
327
|
+
| GuideMessageData
|
|
312
328
|
| ExecutingToolMessageData
|
|
313
329
|
| ExecutedToolMessageData
|
|
314
330
|
| ApprovalRequestMessageData
|
|
@@ -16,6 +16,7 @@ export type ParsedChunkAction =
|
|
|
16
16
|
| { action: 'metadata'; modelDisplayName: string; modelName: string; providerName: string; contextWindow: number }
|
|
17
17
|
| { action: 'text'; text: string }
|
|
18
18
|
| { action: 'thinking'; text: string }
|
|
19
|
+
| { action: 'guide'; text: string }
|
|
19
20
|
| { action: 'tool_execution'; segment: ToolExecutionSegment }
|
|
20
21
|
| { action: 'approval_request'; requestId: string; command: string; explanation?: string; approvalType: string }
|
|
21
22
|
| { action: 'approval_batch'; requestId: string; approvalType: string; toolCalls: PendingToolCallData[] }
|
|
@@ -251,6 +251,12 @@ export interface UnifiedChatState {
|
|
|
251
251
|
/** Granular phase for the "Thinking..."/"Streaming..." status row above input. */
|
|
252
252
|
streamingPhase: StreamingPhase
|
|
253
253
|
|
|
254
|
+
/** True while the adapter is rebuilding the message list from the
|
|
255
|
+
* server-side transcript store (SSE/Guide mount-time hydration). Optional —
|
|
256
|
+
* transports without server hydration (NATS manages its own dialog
|
|
257
|
+
* loading) never set it. */
|
|
258
|
+
isHydratingHistory?: boolean
|
|
259
|
+
|
|
254
260
|
// ─── Actions ──────────────────────────────────────────────────────────────
|
|
255
261
|
sendMessage: (
|
|
256
262
|
text: string,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat conversation-id persistence — the SSE/Guide chat's ONLY local state.
|
|
3
|
+
*
|
|
4
|
+
* The stored value is the SERVER-minted conversation id echoed in the chat
|
|
5
|
+
* stream's leading metadata frame; the client never generates ids. Message
|
|
6
|
+
* history itself lives server-side (`chat_conversations` / `chat_messages`)
|
|
7
|
+
* and is rehydrated via the history endpoint — see
|
|
8
|
+
* `use-chat-history-hydration.ts`.
|
|
9
|
+
*
|
|
10
|
+
* Built on the lib-standard `createLocalStorageAdapter` (SSR guard,
|
|
11
|
+
* try/catch, quota-failure logging, dynamic namespacing) instead of raw
|
|
12
|
+
* `window.localStorage`. The namespace resolver runs on EVERY read/write, so
|
|
13
|
+
* a proxy-auth identity switch mid-page automatically re-partitions the key.
|
|
14
|
+
*
|
|
15
|
+
* Key shape: `mingo-chat-<source>[-u-<email>].conversation`
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
createLocalStorageAdapter,
|
|
20
|
+
type LocalStorageAdapter,
|
|
21
|
+
} from '../../../utils/local-storage-adapter'
|
|
22
|
+
import { getChatProxyAuth } from './chat-proxy-auth-storage'
|
|
23
|
+
|
|
24
|
+
export interface PersistedChatConversation {
|
|
25
|
+
conversationId: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const STORAGE_KEY = 'conversation'
|
|
29
|
+
|
|
30
|
+
const namespaceFor = (source: string): string => {
|
|
31
|
+
const base = `mingo-chat-${source}`
|
|
32
|
+
const auth = getChatProxyAuth()
|
|
33
|
+
return auth?.email ? `${base}-u-${encodeURIComponent(auth.email.toLowerCase())}` : base
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One adapter per chat `source` (= platform). Memoize per mount. */
|
|
37
|
+
export function createChatConversationStorage(
|
|
38
|
+
source: string,
|
|
39
|
+
): LocalStorageAdapter<PersistedChatConversation> {
|
|
40
|
+
return createLocalStorageAdapter<PersistedChatConversation>({
|
|
41
|
+
key: STORAGE_KEY,
|
|
42
|
+
namespace: () => namespaceFor(source),
|
|
43
|
+
validate: (parsed): parsed is PersistedChatConversation =>
|
|
44
|
+
!!parsed &&
|
|
45
|
+
typeof (parsed as PersistedChatConversation).conversationId === 'string' &&
|
|
46
|
+
(parsed as PersistedChatConversation).conversationId.length > 0,
|
|
47
|
+
logTag: '[chat-conversation-storage]',
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sweep THIS source's stale keys: the other-identity variants (base vs
|
|
53
|
+
* impersonation), and retired key shapes from before the server-minted-id
|
|
54
|
+
* design (raw `mingo-chat-<source>` and versioned `-v1`/`-v2` full-history
|
|
55
|
+
* stores). Boundary-aware: a hyphen-extended sibling source (`flamingo` vs
|
|
56
|
+
* `flamingo-teaser`) is NOT this source's namespace and must never be swept.
|
|
57
|
+
*
|
|
58
|
+
* Raw storage enumeration is unavoidable here — the adapter abstraction is
|
|
59
|
+
* per-key and Web Storage only exposes iteration on the raw object.
|
|
60
|
+
*/
|
|
61
|
+
export function pruneStaleChatConversationStorage(source: string): void {
|
|
62
|
+
if (typeof window === 'undefined') return
|
|
63
|
+
try {
|
|
64
|
+
const base = `mingo-chat-${source}`
|
|
65
|
+
const currentKey = `${namespaceFor(source)}.${STORAGE_KEY}`
|
|
66
|
+
const ownedKey = (k: string) =>
|
|
67
|
+
k === base || // retired raw key (pre-adapter shape)
|
|
68
|
+
k === `${base}.${STORAGE_KEY}` || // current no-identity shape
|
|
69
|
+
k.startsWith(`${base}-u-`) || // per-identity shapes (current + retired)
|
|
70
|
+
k.startsWith(`${base}-v`) // retired versioned full-history shapes
|
|
71
|
+
const toRemove: string[] = []
|
|
72
|
+
for (let i = 0; i < window.localStorage.length; i++) {
|
|
73
|
+
const k = window.localStorage.key(i)
|
|
74
|
+
if (!k) continue
|
|
75
|
+
if (ownedKey(k) && k !== currentKey) toRemove.push(k)
|
|
76
|
+
}
|
|
77
|
+
for (const k of toRemove) {
|
|
78
|
+
window.localStorage.removeItem(k)
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// localStorage access blocked (Safari private mode etc.) — non-fatal.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -65,6 +65,12 @@ export function parseChunkToAction(chunk: unknown): ParsedChunkAction | null {
|
|
|
65
65
|
}
|
|
66
66
|
return null
|
|
67
67
|
|
|
68
|
+
case MESSAGE_TYPE.GUIDE:
|
|
69
|
+
if (typeof data.text === 'string') {
|
|
70
|
+
return { action: 'guide', text: data.text }
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
|
|
68
74
|
case MESSAGE_TYPE.EXECUTING_TOOL:
|
|
69
75
|
return {
|
|
70
76
|
action: 'tool_execution',
|
|
@@ -139,6 +139,22 @@ export class MessageSegmentAccumulator {
|
|
|
139
139
|
return this.getSegments()
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Append guide text to the current message.
|
|
144
|
+
* If the last segment is guide, append to it; otherwise start a new guide segment.
|
|
145
|
+
*/
|
|
146
|
+
appendGuide(text: string): MessageSegment[] {
|
|
147
|
+
const lastSegment = this.segments[this.segments.length - 1]
|
|
148
|
+
|
|
149
|
+
if (lastSegment && lastSegment.type === 'guide') {
|
|
150
|
+
this.segments[this.segments.length - 1] = { type: 'guide', text: lastSegment.text + text }
|
|
151
|
+
} else {
|
|
152
|
+
this.segments.push({ type: 'guide', text })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return this.getSegments()
|
|
156
|
+
}
|
|
157
|
+
|
|
142
158
|
/**
|
|
143
159
|
* Add a tool execution segment.
|
|
144
160
|
*
|
|
@@ -516,6 +532,9 @@ export class MessageSegmentAccumulator {
|
|
|
516
532
|
case 'thinking':
|
|
517
533
|
if (segment.text) this.appendThinking(segment.text)
|
|
518
534
|
break
|
|
535
|
+
case 'guide':
|
|
536
|
+
if (segment.text) this.appendGuide(segment.text)
|
|
537
|
+
break
|
|
519
538
|
case 'tool_execution':
|
|
520
539
|
this.addToolExecution(segment)
|
|
521
540
|
break
|