@meistrari/chat-nuxt 3.16.0-rc.4 → 3.16.0

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/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "3.16.0-rc.4",
4
+ "version": "3.16.0-rc.5",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -14,8 +14,6 @@ const {
14
14
  renameTitle,
15
15
  renameInputRef,
16
16
  showDeleteModal,
17
- menuButtonRef,
18
- isMenuOpen,
19
17
  conversationPath,
20
18
  displayTitle,
21
19
  menuItems,
@@ -32,9 +30,6 @@ const {
32
30
  function setRenameInputRef(element) {
33
31
  renameInputRef.value = element;
34
32
  }
35
- function setMenuButtonRef(element) {
36
- menuButtonRef.value = element;
37
- }
38
33
  </script>
39
34
 
40
35
  <template>
@@ -115,23 +110,21 @@ function setMenuButtonRef(element) {
115
110
  mt-2px
116
111
  flex
117
112
  items-center
118
- class="opacity-0 group-hover:opacity-100"
119
- :class="isMenuOpen && 'opacity-100'"
113
+ class="opacity-0 group-hover:opacity-100 focus-within:opacity-100 [&:has([data-state=open])]:opacity-100"
120
114
  >
121
115
  <TelaDropdownMenu :items="menuItems" content-class="chat-sidebar-floating">
122
116
  <button
123
- :ref="setMenuButtonRef"
124
117
  p-4px
125
118
  rounded-8px
126
119
  hover:bg-lowered
127
120
  cursor-pointer
128
- :class="isMenuOpen && '!bg-lowered'"
121
+ class="group data-[state=open]:!bg-lowered"
129
122
  @click.prevent.stop
130
123
  >
131
124
  <TelaIcon
132
125
  name="i-ph-dots-three"
133
126
  size="18px"
134
- :color="isMenuOpen ? 'icon-tertiary' : 'icon-subtle group-hover:icon-tertiary'"
127
+ color="icon-subtle group-hover:icon-tertiary group-data-[state=open]:icon-tertiary"
135
128
  />
136
129
  </button>
137
130
  </TelaDropdownMenu>
@@ -20,8 +20,6 @@ export declare function useConversationItem(options: ConversationItemOptions, em
20
20
  renameTitle: any;
21
21
  renameInputRef: any;
22
22
  showDeleteModal: any;
23
- menuButtonRef: any;
24
- isMenuOpen: any;
25
23
  conversationPath: any;
26
24
  displayTitle: any;
27
25
  menuItems: ({
@@ -1,4 +1,3 @@
1
- import { useMagicKeys, useMutationObserver } from "@vueuse/core";
2
1
  import { resolveConversationRenameTitle } from "../title.js";
3
2
  import { capitalizeFirst } from "../../shared/string.js";
4
3
  const conversationNavigationDelayMs = 250;
@@ -8,28 +7,27 @@ export function useConversationItem(options, emit) {
8
7
  const renameInputRef = ref(null);
9
8
  const isIgnoringInitialRenameBlur = ref(false);
10
9
  const showDeleteModal = ref(false);
11
- const menuButtonRef = ref(null);
12
- const isMenuOpen = ref(false);
13
10
  const conversationPath = computed(() => `/${options.conversation.id}`);
14
11
  const displayTitle = computed(() => capitalizeFirst(options.conversation.title));
15
- const { escape } = useMagicKeys();
16
12
  let conversationNavigationTimer = null;
17
13
  let renameBlurTimer = null;
18
- watch(() => escape?.value, (pressed) => {
19
- if (pressed && isRenamingInline.value) {
14
+ const escapeCancellable = computed(() => isRenamingInline.value || showDeleteModal.value);
15
+ function handleEscapeKeydown(event) {
16
+ if (event.key !== "Escape")
17
+ return;
18
+ if (isRenamingInline.value)
20
19
  cancelRename();
21
- }
22
- if (pressed && showDeleteModal.value) {
20
+ if (showDeleteModal.value)
23
21
  cancelDelete();
24
- }
22
+ }
23
+ watch(escapeCancellable, (active) => {
24
+ if (typeof window === "undefined")
25
+ return;
26
+ if (active)
27
+ window.addEventListener("keydown", handleEscapeKeydown);
28
+ else
29
+ window.removeEventListener("keydown", handleEscapeKeydown);
25
30
  });
26
- useMutationObserver(menuButtonRef, (mutations) => {
27
- for (const mutation of mutations) {
28
- if (mutation.attributeName === "data-state") {
29
- isMenuOpen.value = menuButtonRef.value?.getAttribute("data-state") === "open";
30
- }
31
- }
32
- }, { attributes: true });
33
31
  function clearPendingConversationNavigation() {
34
32
  if (conversationNavigationTimer === null)
35
33
  return;
@@ -79,7 +77,6 @@ export function useConversationItem(options, emit) {
79
77
  allowRenameBlurSoon();
80
78
  }
81
79
  function startRename() {
82
- isMenuOpen.value = false;
83
80
  clearPendingConversationNavigation();
84
81
  clearRenameBlurTimer();
85
82
  renameTitle.value = options.conversation.title;
@@ -151,14 +148,14 @@ export function useConversationItem(options, emit) {
151
148
  onBeforeUnmount(() => {
152
149
  clearPendingConversationNavigation();
153
150
  clearRenameBlurTimer();
151
+ if (typeof window !== "undefined")
152
+ window.removeEventListener("keydown", handleEscapeKeydown);
154
153
  });
155
154
  return {
156
155
  isRenamingInline,
157
156
  renameTitle,
158
157
  renameInputRef,
159
158
  showDeleteModal,
160
- menuButtonRef,
161
- isMenuOpen,
162
159
  conversationPath,
163
160
  displayTitle,
164
161
  menuItems,
@@ -0,0 +1,16 @@
1
+ import type { ChatDebugConversationFixture, ChatDebugSidebarFixture } from '../debug/chat-debug-scenarios.js';
2
+ type __VLS_Props = {
3
+ sidebar: ChatDebugSidebarFixture;
4
+ activeConversationId: string | null;
5
+ /** Bumped by the workbench on restart so local rename/delete state rewinds with the timeline. */
6
+ resetKey: number;
7
+ };
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
+ switch: (fixture: ChatDebugConversationFixture) => any;
10
+ "update:mountedCount": (count: number) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onSwitch?: ((fixture: ChatDebugConversationFixture) => any) | undefined;
13
+ "onUpdate:mountedCount"?: ((count: number) => any) | undefined;
14
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
@@ -0,0 +1,104 @@
1
+ <script setup>
2
+ import ChatConversationItem from "../../conversations/components/chat-conversation-item.vue";
3
+ const props = defineProps({
4
+ sidebar: { type: Object, required: true },
5
+ activeConversationId: { type: [String, null], required: true },
6
+ resetKey: { type: Number, required: true }
7
+ });
8
+ const emit = defineEmits(["switch", "update:mountedCount"]);
9
+ const stress = ref(false);
10
+ const titleOverrides = ref({});
11
+ const deletedIds = ref(/* @__PURE__ */ new Set());
12
+ const lastAction = ref(null);
13
+ const fixturesById = computed(() => new Map(
14
+ props.sidebar.conversations.map((fixture) => [fixture.conversation.id, fixture])
15
+ ));
16
+ const visibleItems = computed(() => {
17
+ const count = stress.value ? props.sidebar.stressCount : props.sidebar.defaultCount;
18
+ return props.sidebar.conversations.slice(0, count).filter((fixture) => !deletedIds.value.has(fixture.conversation.id)).map((fixture) => {
19
+ const title = titleOverrides.value[fixture.conversation.id];
20
+ return title ? { ...fixture.conversation, title } : fixture.conversation;
21
+ });
22
+ });
23
+ watch(() => visibleItems.value.length, (count) => emit("update:mountedCount", count), { immediate: true });
24
+ watch(() => props.resetKey, () => {
25
+ titleOverrides.value = {};
26
+ deletedIds.value = /* @__PURE__ */ new Set();
27
+ lastAction.value = null;
28
+ });
29
+ function selectConversation(id) {
30
+ const fixture = fixturesById.value.get(id);
31
+ if (!fixture)
32
+ return;
33
+ lastAction.value = `switch \u2192 ${fixture.conversation.title}`;
34
+ emit("switch", fixture);
35
+ }
36
+ function handleRename(id, title) {
37
+ titleOverrides.value = { ...titleOverrides.value, [id]: title };
38
+ lastAction.value = `rename \u2192 ${title}`;
39
+ }
40
+ function handleDelete(id) {
41
+ deletedIds.value = /* @__PURE__ */ new Set([...deletedIds.value, id]);
42
+ lastAction.value = `delete \u2192 ${id}`;
43
+ }
44
+ function recordAction(action) {
45
+ lastAction.value = action;
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <aside class="debug-conversation-rail" flex="~ col" min-h-0 shrink-0>
51
+ <div flex items-center justify-between gap-8px px-8px pb-8px>
52
+ <span body-caption-9-medium text-tertiary>
53
+ Conversas · {{ visibleItems.length }}
54
+ </span>
55
+ <div flex items-center gap-8px>
56
+ <button
57
+ type="button"
58
+ body-10-regular
59
+ :class="stress ? 'text-tertiary hover:text-secondary' : 'text-primary'"
60
+ transition-colors
61
+ @click="stress = false"
62
+ >
63
+ Default ({{ sidebar.defaultCount }})
64
+ </button>
65
+ <button
66
+ type="button"
67
+ body-10-regular
68
+ :class="stress ? 'text-primary' : 'text-tertiary hover:text-secondary'"
69
+ transition-colors
70
+ @click="stress = true"
71
+ >
72
+ Stress ({{ sidebar.stressCount }})
73
+ </button>
74
+ </div>
75
+ </div>
76
+
77
+ <div flex="~ col" gap-1px min-h-0 grow overflow-y-auto px-4px>
78
+ <ChatConversationItem
79
+ v-for="item in visibleItems"
80
+ :key="item.id"
81
+ :conversation="item"
82
+ :active="item.id === activeConversationId"
83
+ :draggable="true"
84
+ :select-conversation="selectConversation"
85
+ @rename="handleRename"
86
+ @delete="handleDelete"
87
+ @duplicate="recordAction(`duplicate \u2192 ${$event}`)"
88
+ @export="recordAction(`export \u2192 ${$event}`)"
89
+ @drag-start="recordAction(`drag \u2192 ${$event}`)"
90
+ @drag-end="recordAction('drag end')"
91
+ />
92
+ </div>
93
+
94
+ <div h-24px px-8px flex items-center shrink-0>
95
+ <span v-if="lastAction" body-10-regular text-tertiary truncate>
96
+ {{ lastAction }}
97
+ </span>
98
+ </div>
99
+ </aside>
100
+ </template>
101
+
102
+ <style scoped>
103
+ .debug-conversation-rail{border-right:1px solid rgba(229,231,235,.7);padding-top:8px;width:232px}
104
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { ChatDebugConversationFixture, ChatDebugSidebarFixture } from '../debug/chat-debug-scenarios.js';
2
+ type __VLS_Props = {
3
+ sidebar: ChatDebugSidebarFixture;
4
+ activeConversationId: string | null;
5
+ /** Bumped by the workbench on restart so local rename/delete state rewinds with the timeline. */
6
+ resetKey: number;
7
+ };
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
9
+ switch: (fixture: ChatDebugConversationFixture) => any;
10
+ "update:mountedCount": (count: number) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onSwitch?: ((fixture: ChatDebugConversationFixture) => any) | undefined;
13
+ "onUpdate:mountedCount"?: ((count: number) => any) | undefined;
14
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
@@ -1,9 +1,11 @@
1
1
  <script setup>
2
+ import ChatDebugConversationRail from "./chat-debug-conversation-rail.vue";
2
3
  import ChatMessageList from "./chat-message-list.vue";
3
4
  import { provideEmbedConfig } from "../../embed/composables/embed-config";
4
5
  import { createChatDebugCitationsConfig } from "../debug/chat-debug-citations";
6
+ import { createReasoningCache, resolveReasoningDisplayMessages } from "../message-reasoning";
5
7
  import { createChatDebugScenarios } from "../debug/chat-debug-scenarios";
6
- import { useChatDebugDiagnostics } from "../debug/use-chat-debug-diagnostics";
8
+ import { useChatDebugDiagnostics, useChatDebugWindowListenerProbe } from "../debug/use-chat-debug-diagnostics";
7
9
  import { formatChatDebugTime as formatTime, useChatDebugPlayback } from "../debug/use-chat-debug-playback";
8
10
  const scenarios = createChatDebugScenarios();
9
11
  const viewportWidth = ref("default");
@@ -32,7 +34,8 @@ const {
32
34
  togglePlayback,
33
35
  jumpToEnd,
34
36
  stepEvent,
35
- selectSpeed
37
+ selectSpeed,
38
+ applyManualEvent
36
39
  } = useChatDebugPlayback(scenarios, {
37
40
  onFrame: handlePlaybackFrame,
38
41
  onReset: handlePlaybackReset
@@ -47,11 +50,47 @@ const { diagnostics, reset: resetDiagnostics, recordFrame } = useChatDebugDiagno
47
50
  speed,
48
51
  seekRevision
49
52
  });
53
+ const windowListeners = useChatDebugWindowListenerProbe();
54
+ const reasoningCache = createReasoningCache();
55
+ const displayMessages = computed(() => selectedScenario.value.reasoningFormat === "raw" ? resolveReasoningDisplayMessages(playbackState.value.messages, reasoningCache) : playbackState.value.messages);
56
+ const reasoningCacheEntries = ref(0);
57
+ watch(displayMessages, () => {
58
+ reasoningCacheEntries.value = reasoningCache.bySignature.size;
59
+ }, { immediate: true });
60
+ const switchCount = ref(0);
61
+ const railResetKey = ref(0);
62
+ const sidebarMountedCount = ref(0);
63
+ let manualSwitchSequence = 0;
64
+ watch(() => playbackState.value.activeConversationId, (next, previous) => {
65
+ if (next && next !== previous)
66
+ switchCount.value += 1;
67
+ });
68
+ const renderedConversationId = computed(() => displayMessages.value[0]?.conversationId ?? null);
69
+ const renderedConversationStale = computed(() => {
70
+ const active = playbackState.value.activeConversationId;
71
+ return Boolean(active && renderedConversationId.value && renderedConversationId.value !== active);
72
+ });
73
+ function handleRailSwitch(fixture) {
74
+ if (playing.value)
75
+ togglePlayback();
76
+ manualSwitchSequence += 1;
77
+ applyManualEvent({
78
+ id: `manual-switch-${manualSwitchSequence}`,
79
+ atMs: elapsedMs.value,
80
+ label: `Manual: ${fixture.conversation.title}`,
81
+ tone: "user",
82
+ type: "conversation.switch",
83
+ conversationId: fixture.conversation.id,
84
+ messages: fixture.messages
85
+ });
86
+ }
50
87
  function handlePlaybackFrame(frameDeltaMs) {
51
88
  recordFrame(frameDeltaMs);
52
89
  }
53
90
  function handlePlaybackReset() {
54
91
  resetDiagnostics();
92
+ switchCount.value = 0;
93
+ railResetKey.value += 1;
55
94
  }
56
95
  const messageCharacters = computed(() => playbackState.value.messages.reduce(
57
96
  (total, message) => total + message.content.length,
@@ -189,24 +228,35 @@ function eventToneClass(tone) {
189
228
  </div>
190
229
  </div>
191
230
 
192
- <div ref="stageRef" class="stage-viewport" min-h-0 grow flex justify-center px-24px pb-24px>
193
- <div
194
- class="chat-under-test"
195
- :class="[
231
+ <div class="stage-body" min-h-0 grow flex>
232
+ <ChatDebugConversationRail
233
+ v-if="selectedScenario.sidebar"
234
+ :sidebar="selectedScenario.sidebar"
235
+ :active-conversation-id="playbackState.activeConversationId"
236
+ :reset-key="railResetKey"
237
+ ml-24px mb-24px
238
+ @switch="handleRailSwitch"
239
+ @update:mounted-count="sidebarMountedCount = $event"
240
+ />
241
+ <div ref="stageRef" class="stage-viewport" min-h-0 min-w-0 grow flex justify-center px-24px pb-24px>
242
+ <div
243
+ class="chat-under-test"
244
+ :class="[
196
245
  `chat-under-test--${viewportWidth}`,
197
246
  showBounds && 'chat-under-test--bounded'
198
247
  ]"
199
- >
200
- <TelaChatBody :key="conversationVariant" :variant="conversationVariant" h-full>
201
- <ChatMessageList
202
- :messages="playbackState.messages"
203
- :loading="playbackState.loading"
204
- :streaming-active-message-id="playbackState.streamingActiveMessageId"
205
- :preparing-steps="playbackState.preparingSteps"
206
- :variant="conversationVariant"
207
- @retry="stepEvent(-1)"
208
- />
209
- </TelaChatBody>
248
+ >
249
+ <TelaChatBody :key="conversationVariant" :variant="conversationVariant" h-full>
250
+ <ChatMessageList
251
+ :messages="displayMessages"
252
+ :loading="playbackState.loading"
253
+ :streaming-active-message-id="playbackState.streamingActiveMessageId"
254
+ :preparing-steps="playbackState.preparingSteps"
255
+ :variant="conversationVariant"
256
+ @retry="stepEvent(-1)"
257
+ />
258
+ </TelaChatBody>
259
+ </div>
210
260
  </div>
211
261
  </div>
212
262
  </main>
@@ -348,6 +398,63 @@ function eventToneClass(tone) {
348
398
  </dl>
349
399
  </section>
350
400
 
401
+ <section v-if="selectedScenario.sidebar" mt-28px>
402
+ <div body-10-regular text-tertiary uppercase tracking-0.08em mb-14px>
403
+ Switching
404
+ </div>
405
+ <div flex items-center gap-8px body-12-medium>
406
+ <span
407
+ w-6px h-6px rounded-full
408
+ :class="renderedConversationStale ? 'bg-amber-500' : 'bg-emerald-500'"
409
+ />
410
+ {{ renderedConversationStale ? "stale content" : "thread in sync" }}
411
+ </div>
412
+ <dl class="detail-list" body-12-regular mt-12px>
413
+ <div>
414
+ <dt text-tertiary>
415
+ Switches
416
+ </dt><dd text-primary tabular-nums>
417
+ {{ switchCount }}
418
+ </dd>
419
+ </div>
420
+ <div>
421
+ <dt text-tertiary>
422
+ Sidebar items
423
+ </dt><dd text-primary tabular-nums>
424
+ {{ sidebarMountedCount }}
425
+ </dd>
426
+ </div>
427
+ <div>
428
+ <dt text-tertiary>
429
+ Reasoning cache
430
+ </dt><dd text-primary tabular-nums>
431
+ {{ reasoningCacheEntries }}
432
+ </dd>
433
+ </div>
434
+ <div>
435
+ <dt text-tertiary>
436
+ Window listeners Δ
437
+ </dt><dd text-primary tabular-nums>
438
+ {{ windowListeners.active }} (peak {{ windowListeners.peak }})
439
+ </dd>
440
+ </div>
441
+ <div>
442
+ <dt text-tertiary>
443
+ Keydown listeners
444
+ </dt><dd text-primary tabular-nums>
445
+ {{ windowListeners.byType.keydown ?? 0 }}
446
+ </dd>
447
+ </div>
448
+ <div>
449
+ <dt text-tertiary>
450
+ Rendered thread
451
+ </dt><dd text-primary truncate max-w-120px>
452
+ {{ renderedConversationId ?? "\u2014" }}
453
+ </dd>
454
+ </div>
455
+ </dl>
456
+ </section>
457
+
351
458
  <section mt-28px>
352
459
  <div flex items-center justify-between gap-12px mb-14px>
353
460
  <div body-10-regular text-tertiary uppercase tracking-0.08em>
@@ -1,7 +1,47 @@
1
+ const DEBUG_DOCUMENTS = {
2
+ "debug_fixture/parecer-7": {
3
+ meta: {
4
+ title: "Parecer sint\xE9tico n\xBA 7 \u2014 microrrede de Ilha Verde",
5
+ sourceLabel: "Fixture de depura\xE7\xE3o",
6
+ identifier: "PARECER-7",
7
+ date: "2026-05-14",
8
+ dateLabel: "emiss\xE3o",
9
+ snippet: "A microrrede fict\xEDcia de Ilha Verde mant\xE9m autonomia de 36 horas com o banco de baterias descrito no anexo B."
10
+ },
11
+ content: [
12
+ "Parecer sint\xE9tico n\xBA 7 \u2014 microrrede de Ilha Verde.",
13
+ "Documento gerado para o workbench de depura\xE7\xE3o; nenhum dado real.",
14
+ "A microrrede fict\xEDcia de Ilha Verde mant\xE9m autonomia de 36 horas com o banco de baterias descrito no anexo B.",
15
+ "A proje\xE7\xE3o de expans\xE3o considera 120 unidades consumidoras e um fator de simultaneidade de 0,62."
16
+ ].join("\n\n")
17
+ },
18
+ "debug_fixture/relatorio-12": {
19
+ meta: {
20
+ title: "Relat\xF3rio sint\xE9tico n\xBA 12 \u2014 inspe\xE7\xE3o dos inversores",
21
+ sourceLabel: "Fixture de depura\xE7\xE3o",
22
+ identifier: "RELATORIO-12",
23
+ date: "2026-06-02",
24
+ dateLabel: "inspe\xE7\xE3o",
25
+ snippet: "Os doze inversores fict\xEDcios operaram dentro da faixa nominal durante o ciclo de testes."
26
+ },
27
+ content: [
28
+ "Relat\xF3rio sint\xE9tico n\xBA 12 \u2014 inspe\xE7\xE3o dos inversores.",
29
+ "Documento gerado para o workbench de depura\xE7\xE3o; nenhum dado real.",
30
+ "Os doze inversores fict\xEDcios operaram dentro da faixa nominal durante o ciclo de testes.",
31
+ "O desvio m\xE1ximo de tens\xE3o registrado foi de 1,8% no alimentador norte."
32
+ ].join("\n\n")
33
+ }
34
+ };
35
+ function debugDocument(ref) {
36
+ return DEBUG_DOCUMENTS[`${ref.namespace}/${ref.id}`];
37
+ }
1
38
  function humanizeNamespace(namespace) {
2
39
  return namespace.split(/[_-]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3
40
  }
4
41
  export function chatDebugCitationMeta(ref) {
42
+ const document = debugDocument(ref);
43
+ if (document)
44
+ return document.meta;
5
45
  return {
6
46
  title: `Documento ${ref.id}`,
7
47
  sourceLabel: humanizeNamespace(ref.namespace),
@@ -11,6 +51,10 @@ export function chatDebugCitationMeta(ref) {
11
51
  }
12
52
  export function createChatDebugCitationsConfig() {
13
53
  return {
14
- resolve: async (ref) => chatDebugCitationMeta(ref)
54
+ resolve: async (ref) => chatDebugCitationMeta(ref),
55
+ getDocument: async (ref) => {
56
+ const document = debugDocument(ref);
57
+ return document ? { content: document.content } : null;
58
+ }
15
59
  };
16
60
  }
@@ -1,4 +1,4 @@
1
- import type { Message } from '../../types/schemas/chat/conversations.js';
1
+ import type { Conversation, Message } from '../../types/schemas/chat/conversations.js';
2
2
  export type ChatDebugEventTone = 'neutral' | 'user' | 'assistant' | 'reasoning' | 'tool' | 'error';
3
3
  type ChatDebugEventBase = {
4
4
  id: string;
@@ -16,19 +16,47 @@ export type ChatDebugEvent = ChatDebugEventBase & ({
16
16
  } | {
17
17
  type: 'playback.patch';
18
18
  patch: Partial<Omit<ChatDebugPlaybackState, 'messages'>>;
19
+ } | {
20
+ type: 'conversation.switch';
21
+ conversationId: string;
22
+ messages: Message[];
19
23
  });
24
+ /** One switchable conversation of a sidebar fixture: identity plus its full history. */
25
+ export type ChatDebugConversationFixture = {
26
+ conversation: Conversation;
27
+ messages: Message[];
28
+ };
29
+ /**
30
+ * Sidebar fixture for switch scenarios. Only `defaultCount` items mount until
31
+ * the stress control is engaged, so a field-scale fixture never makes the
32
+ * workbench itself unusable.
33
+ */
34
+ export type ChatDebugSidebarFixture = {
35
+ defaultCount: number;
36
+ stressCount: number;
37
+ conversations: ChatDebugConversationFixture[];
38
+ };
20
39
  export type ChatDebugScenario = {
21
40
  id: string;
22
41
  name: string;
23
42
  intent: string;
24
43
  durationMs: number;
25
44
  events: ChatDebugEvent[];
45
+ /** Present on switch scenarios: mounts the conversation rail around the stage. */
46
+ sidebar?: ChatDebugSidebarFixture;
47
+ /**
48
+ * `raw` marks fixtures whose reasoningData is the polling wire format, to be
49
+ * routed through `resolveReasoningDisplayMessages` (and its cache) exactly
50
+ * like production. Absent means the fixture ships pre-parsed steps.
51
+ */
52
+ reasoningFormat?: 'raw';
26
53
  };
27
54
  export type ChatDebugPlaybackState = {
28
55
  messages: Message[];
29
56
  loading: boolean;
30
57
  streamingActiveMessageId: string | null;
31
58
  preparingSteps: string[] | null;
59
+ activeConversationId: string | null;
32
60
  };
33
61
  export declare const EMPTY_CHAT_DEBUG_PLAYBACK_STATE: ChatDebugPlaybackState;
34
62
  export declare function applyChatDebugEvent(state: ChatDebugPlaybackState, event: ChatDebugEvent): ChatDebugPlaybackState;
@@ -3,11 +3,13 @@ var __defProp = Object.defineProperty;
3
3
  var __template = (cooked, raw) => __freeze(__defProp(cooked, "raw", { value: __freeze(raw || cooked.slice()) }));
4
4
  var _a;
5
5
  import { CHAT_PREPARING_STEPS } from "../../embed/loading-messages.js";
6
+ import { createConversationSwitchScenario } from "./chat-debug-switch-scenario.js";
6
7
  export const EMPTY_CHAT_DEBUG_PLAYBACK_STATE = {
7
8
  messages: [],
8
9
  loading: false,
9
10
  streamingActiveMessageId: null,
10
- preparingSteps: null
11
+ preparingSteps: null,
12
+ activeConversationId: null
11
13
  };
12
14
  function cloneMessage(message2) {
13
15
  return {
@@ -17,6 +19,16 @@ function cloneMessage(message2) {
17
19
  };
18
20
  }
19
21
  export function applyChatDebugEvent(state, event2) {
22
+ if (event2.type === "conversation.switch") {
23
+ return {
24
+ ...state,
25
+ messages: event2.messages.map(cloneMessage),
26
+ loading: false,
27
+ streamingActiveMessageId: null,
28
+ preparingSteps: null,
29
+ activeConversationId: event2.conversationId
30
+ };
31
+ }
20
32
  if (event2.type === "playback.patch") {
21
33
  const preparingSteps = "preparingSteps" in event2.patch ? event2.patch.preparingSteps ? [...event2.patch.preparingSteps] : null : state.preparingSteps;
22
34
  return {
@@ -1265,6 +1277,7 @@ export function createChatDebugScenarios(baseTime = Date.now()) {
1265
1277
  })
1266
1278
  ]
1267
1279
  },
1280
+ createConversationSwitchScenario(baseTime),
1268
1281
  {
1269
1282
  id: "failure",
1270
1283
  name: "Failure + recovery",
@@ -0,0 +1,21 @@
1
+ import type { ChatDebugScenario, ChatDebugSidebarFixture } from './chat-debug-scenarios.js';
2
+ export declare const CONVERSATION_SWITCH_SCENARIO_ID = "conversation-switch";
3
+ export declare const SWITCH_HEAVY_CONVERSATION_ID = "switch-heavy";
4
+ export declare const SWITCH_LIGHT_CONVERSATION_ID = "switch-light";
5
+ /**
6
+ * Mirrors the field report that motivated this fixture: ~346 conversation
7
+ * links rendered eagerly in the sidebar. Everything here is synthetic — no
8
+ * customer text, identifiers or staging data.
9
+ */
10
+ export declare const SWITCH_SIDEBAR_STRESS_COUNT = 346;
11
+ export declare const SWITCH_SIDEBAR_DEFAULT_COUNT = 24;
12
+ export declare function createConversationSwitchSidebar(baseTime: number): ChatDebugSidebarFixture;
13
+ /**
14
+ * The conversation-switch freeze reproducer. The timeline performs the same
15
+ * switch a user performs by clicking the sidebar, alternating between a
16
+ * markdown-heavy conversation, a light one and a filler, at a cadence close to
17
+ * the switching that froze the field deployment. Play = repeated switching;
18
+ * event stepping = one switch per press; the rail items themselves are the
19
+ * manual control.
20
+ */
21
+ export declare function createConversationSwitchScenario(baseTime?: number): ChatDebugScenario;
@@ -0,0 +1,271 @@
1
+ export const CONVERSATION_SWITCH_SCENARIO_ID = "conversation-switch";
2
+ export const SWITCH_HEAVY_CONVERSATION_ID = "switch-heavy";
3
+ export const SWITCH_LIGHT_CONVERSATION_ID = "switch-light";
4
+ export const SWITCH_SIDEBAR_STRESS_COUNT = 346;
5
+ export const SWITCH_SIDEBAR_DEFAULT_COUNT = 24;
6
+ const SWITCH_CADENCE_MS = 1500;
7
+ function conversation(baseTime, input) {
8
+ return {
9
+ workspaceId: "chat-debug-workbench",
10
+ groupId: null,
11
+ telaAgentId: null,
12
+ userId: "debug-user",
13
+ threadSessionId: null,
14
+ model: "claude-sonnet-4-5",
15
+ appliedSettingsAt: null,
16
+ appliedSettingsSnapshot: null,
17
+ createdBy: "debugger@tela.com",
18
+ conversationScope: null,
19
+ createdAt: new Date(baseTime - 864e5),
20
+ updatedAt: new Date(baseTime - 36e5),
21
+ ...input
22
+ };
23
+ }
24
+ function message(baseTime, offsetMs, input) {
25
+ const createdAt = new Date(baseTime + offsetMs);
26
+ return {
27
+ content: "",
28
+ status: "completed",
29
+ failReason: null,
30
+ reasoningData: null,
31
+ files: null,
32
+ externalUuid: null,
33
+ ttft: null,
34
+ createdBy: input.role === "user" ? "debugger@tela.com" : null,
35
+ createdAt,
36
+ updatedAt: createdAt,
37
+ ...input
38
+ };
39
+ }
40
+ function rawThinkingReasoning(text) {
41
+ return [
42
+ {
43
+ type: "assistant",
44
+ message: { content: [{ type: "thinking", text }] }
45
+ }
46
+ ];
47
+ }
48
+ function rawHeavyFinalReasoning(answer) {
49
+ return [
50
+ {
51
+ type: "assistant",
52
+ message: {
53
+ content: [
54
+ { type: "thinking", text: "Consolidando as medi\xE7\xF5es sint\xE9ticas das tr\xEAs visitas antes de montar o comparativo final." },
55
+ {
56
+ type: "tool_use",
57
+ id: "switch-heavy-survey",
58
+ name: "WebSearch",
59
+ input: { query: "microrrede sint\xE9tica Ilha Verde comparativo de autonomia" }
60
+ }
61
+ ]
62
+ }
63
+ },
64
+ {
65
+ type: "user",
66
+ message: {
67
+ content: [
68
+ {
69
+ type: "tool_result",
70
+ tool_use_id: "switch-heavy-survey",
71
+ content: "18 resultados sint\xE9ticos sobre a microrrede fict\xEDcia de Ilha Verde.",
72
+ is_error: false
73
+ }
74
+ ]
75
+ }
76
+ },
77
+ {
78
+ type: "assistant",
79
+ message: { content: [{ type: "text", text: answer }] }
80
+ }
81
+ ];
82
+ }
83
+ function heavyAnswerMarkdown() {
84
+ const inverterRows = Array.from({ length: 36 }, (_, index) => {
85
+ const id = String(index + 1).padStart(2, "0");
86
+ const load = 42 + index * 7 % 31;
87
+ const drift = index * 13 % 20;
88
+ return `| INV-${id} | Alimentador ${index % 2 === 0 ? "norte" : "sul"} | ${load} kW | 0,${String(drift).padStart(2, "0")}% | ${index % 5 === 0 ? "revisar" : "nominal"} |`;
89
+ });
90
+ const configLines = Array.from({ length: 24 }, (_, index) => ` { feeder: '${index % 2 === 0 ? "norte" : "sul"}', inverter: 'INV-${String(index + 1).padStart(2, "0")}', limitKw: ${40 + index}, curtailment: ${index % 3 === 0} },`);
91
+ return [
92
+ "# Comparativo final da microrrede sint\xE9tica de Ilha Verde",
93
+ "",
94
+ "Este \xE9 um cen\xE1rio totalmente fict\xEDcio para o workbench de depura\xE7\xE3o. O comparativo abaixo consolida as tr\xEAs visitas simuladas e serve para estressar a renderiza\xE7\xE3o de Markdown longo em uma \xFAnica mensagem final.",
95
+ "",
96
+ "A autonomia projetada permanece em **36 horas**, conforme o parecer t\xE9cnico sint\xE9tico (cite://debug_fixture/parecer-7#q=autonomia), e a inspe\xE7\xE3o dos inversores n\xE3o registrou desvio relevante de tens\xE3o (cite://debug_fixture/relatorio-12#q=desvio).",
97
+ "",
98
+ "## 1. Panorama das visitas",
99
+ "",
100
+ "- **Visita 1** \u2014 levantamento das cargas e do fator de simultaneidade.",
101
+ "- **Visita 2** \u2014 medi\xE7\xE3o t\xE9rmica dos inversores em regime de ponta.",
102
+ " - Termografia dos barramentos norte e sul.",
103
+ " - Verifica\xE7\xE3o das prote\xE7\xF5es de sobrecorrente.",
104
+ "- **Visita 3** \u2014 ensaio de ilhamento com transfer\xEAncia autom\xE1tica.",
105
+ "",
106
+ "> Nota sint\xE9tica: nenhuma das medi\xE7\xF5es abaixo corresponde a um equipamento real; os n\xFAmeros s\xE3o gerados deterministicamente pelo fixture.",
107
+ "",
108
+ "## 2. Invent\xE1rio de inversores",
109
+ "",
110
+ "| Inversor | Alimentador | Carga de ponta | Desvio | Situa\xE7\xE3o |",
111
+ "| --- | --- | --- | --- | --- |",
112
+ ...inverterRows,
113
+ "",
114
+ "## 3. Configura\xE7\xE3o proposta de despacho",
115
+ "",
116
+ "```ts",
117
+ "export const despachoIlhaVerde = {",
118
+ " autonomiaAlvoHoras: 36,",
119
+ " fatorSimultaneidade: 0.62,",
120
+ " alimentadores: [",
121
+ ...configLines,
122
+ " ],",
123
+ "}",
124
+ "```",
125
+ "",
126
+ "## 4. Riscos e mitiga\xE7\xE3o",
127
+ "",
128
+ "1. **Degrada\xE7\xE3o do banco de baterias** \u2014 repetir o ensaio de capacidade a cada seis meses.",
129
+ "2. **Curtailment excessivo no alimentador sul** \u2014 rebalancear as cargas INV-13 a INV-21.",
130
+ "3. **Perda de telemetria** \u2014 o gateway fict\xEDcio deve armazenar 48 horas de buffer local.",
131
+ "",
132
+ "## 5. Conclus\xE3o",
133
+ "",
134
+ "O arranjo proposto mant\xE9m a microrrede sint\xE9tica dentro da autonomia alvo com margem de 11%. Os detalhes de medi\xE7\xE3o est\xE3o no [Parecer sint\xE9tico n\xBA 7](cite://debug_fixture/parecer-7) e no [Relat\xF3rio sint\xE9tico n\xBA 12](cite://debug_fixture/relatorio-12); ambos s\xE3o documentos gerados pelo pr\xF3prio fixture."
135
+ ].join("\n");
136
+ }
137
+ function heavyConversation(baseTime) {
138
+ const id = SWITCH_HEAVY_CONVERSATION_ID;
139
+ const answer = heavyAnswerMarkdown();
140
+ return {
141
+ conversation: conversation(baseTime, { id, title: "An\xE1lise longa da microrrede (fixture)" }),
142
+ messages: [
143
+ message(baseTime, 0, {
144
+ id: `${id}-user-1`,
145
+ conversationId: id,
146
+ role: "user",
147
+ content: "Levante as cargas da microrrede sint\xE9tica de Ilha Verde e me diga se a autonomia alvo \xE9 vi\xE1vel."
148
+ }),
149
+ message(baseTime, 4e4, {
150
+ id: `${id}-assistant-1`,
151
+ conversationId: id,
152
+ role: "assistant",
153
+ content: "Com o fator de simultaneidade medido em 0,62, a autonomia alvo de 36 horas \xE9 vi\xE1vel desde que o alimentador sul n\xE3o ultrapasse 480 kW de ponta.",
154
+ reasoningData: rawThinkingReasoning("Estimando a ponta agregada a partir das 120 unidades consumidoras sint\xE9ticas."),
155
+ ttft: "1900"
156
+ }),
157
+ message(baseTime, 9e4, {
158
+ id: `${id}-user-2`,
159
+ conversationId: id,
160
+ role: "user",
161
+ content: "E a termografia dos inversores? Algum ponto de aten\xE7\xE3o?"
162
+ }),
163
+ message(baseTime, 13e4, {
164
+ id: `${id}-assistant-2`,
165
+ conversationId: id,
166
+ role: "assistant",
167
+ content: "A termografia sint\xE9tica apontou aquecimento moderado apenas nos inversores INV-06 e INV-16; nada fora da faixa nominal.",
168
+ reasoningData: rawThinkingReasoning("Cruzando as leituras t\xE9rmicas simuladas com os limites nominais de cada inversor."),
169
+ ttft: "1600"
170
+ }),
171
+ message(baseTime, 18e4, {
172
+ id: `${id}-user-3`,
173
+ conversationId: id,
174
+ role: "user",
175
+ content: "Feche o comparativo completo das tr\xEAs visitas com a configura\xE7\xE3o de despacho recomendada."
176
+ }),
177
+ message(baseTime, 26e4, {
178
+ id: `${id}-assistant-3`,
179
+ conversationId: id,
180
+ role: "assistant",
181
+ content: answer,
182
+ reasoningData: rawHeavyFinalReasoning(answer),
183
+ ttft: "2400"
184
+ })
185
+ ]
186
+ };
187
+ }
188
+ function lightConversation(baseTime) {
189
+ const id = SWITCH_LIGHT_CONVERSATION_ID;
190
+ return {
191
+ conversation: conversation(baseTime, { id, title: "Resposta curta (fixture)" }),
192
+ messages: [
193
+ message(baseTime, 0, {
194
+ id: `${id}-user-1`,
195
+ conversationId: id,
196
+ role: "user",
197
+ content: "Qual \xE9 a autonomia alvo da microrrede sint\xE9tica?"
198
+ }),
199
+ message(baseTime, 2e4, {
200
+ id: `${id}-assistant-1`,
201
+ conversationId: id,
202
+ role: "assistant",
203
+ content: "A autonomia alvo do fixture \xE9 de **36 horas**.",
204
+ reasoningData: rawThinkingReasoning("Respondendo direto do par\xE2metro do fixture."),
205
+ ttft: "800"
206
+ })
207
+ ]
208
+ };
209
+ }
210
+ function fillerConversation(baseTime, index) {
211
+ const id = `switch-filler-${String(index).padStart(3, "0")}`;
212
+ return {
213
+ conversation: conversation(baseTime, { id, title: `Conversa sint\xE9tica ${String(index).padStart(3, "0")}` }),
214
+ messages: [
215
+ message(baseTime, 0, {
216
+ id: `${id}-user-1`,
217
+ conversationId: id,
218
+ role: "user",
219
+ content: `Pergunta sint\xE9tica n\xFAmero ${index}.`
220
+ }),
221
+ message(baseTime, 15e3, {
222
+ id: `${id}-assistant-1`,
223
+ conversationId: id,
224
+ role: "assistant",
225
+ content: `Resposta sint\xE9tica n\xFAmero ${index}, sem conte\xFAdo adicional.`,
226
+ ttft: "700"
227
+ })
228
+ ]
229
+ };
230
+ }
231
+ export function createConversationSwitchSidebar(baseTime) {
232
+ const conversations = [
233
+ heavyConversation(baseTime),
234
+ lightConversation(baseTime)
235
+ ];
236
+ for (let index = 3; conversations.length < SWITCH_SIDEBAR_STRESS_COUNT; index++)
237
+ conversations.push(fillerConversation(baseTime, index));
238
+ return {
239
+ defaultCount: SWITCH_SIDEBAR_DEFAULT_COUNT,
240
+ stressCount: SWITCH_SIDEBAR_STRESS_COUNT,
241
+ conversations
242
+ };
243
+ }
244
+ function switchEvent(index, atMs, fixture) {
245
+ return {
246
+ id: `switch-${index}-${fixture.conversation.id}`,
247
+ atMs,
248
+ label: `Abre "${fixture.conversation.title}"`,
249
+ tone: "user",
250
+ type: "conversation.switch",
251
+ conversationId: fixture.conversation.id,
252
+ messages: fixture.messages
253
+ };
254
+ }
255
+ export function createConversationSwitchScenario(baseTime = Date.now()) {
256
+ const sidebar = createConversationSwitchSidebar(baseTime);
257
+ const heavy = sidebar.conversations[0];
258
+ const light = sidebar.conversations[1];
259
+ const filler = sidebar.conversations[2];
260
+ const sequence = [heavy, light, heavy, light, filler, heavy, light, heavy, filler, light, heavy];
261
+ const events = sequence.map((fixture, index) => switchEvent(index + 1, index * SWITCH_CADENCE_MS, fixture));
262
+ return {
263
+ id: CONVERSATION_SWITCH_SCENARIO_ID,
264
+ name: "Conversation switching",
265
+ intent: "Repeated switches between a markdown-heavy and a light conversation, with a field-scale sidebar behind a stress control.",
266
+ durationMs: events.at(-1).atMs,
267
+ events,
268
+ sidebar,
269
+ reasoningFormat: "raw"
270
+ };
271
+ }
@@ -11,6 +11,37 @@ export declare function shouldIgnoreChatDebugLayoutShift(entry: {
11
11
  hadRecentInput: boolean;
12
12
  startTime: number;
13
13
  }, ignoreUntil: number): boolean;
14
+ type ListenerTarget = {
15
+ addEventListener: (type: string, listener: unknown, options?: unknown) => void;
16
+ removeEventListener: (type: string, listener: unknown, options?: unknown) => void;
17
+ };
18
+ /**
19
+ * Counts the target's listener registrations from `install()` onwards by
20
+ * wrapping add/removeEventListener. Mirrors the platform's identity rules —
21
+ * duplicate (type, listener, capture) triples are ignored, and a remove only
22
+ * counts against a registration this tracker saw. The net count is the signal
23
+ * the conversation-switch reproducer watches: per-item global listeners used
24
+ * to grow with the sidebar and with every switch.
25
+ */
26
+ export declare function createChatDebugListenerTracker(target: ListenerTarget): {
27
+ counters: {
28
+ active: number;
29
+ peak: number;
30
+ byType: Record<string, number>;
31
+ };
32
+ install: () => void;
33
+ uninstall: () => void;
34
+ };
35
+ /**
36
+ * Window-level listener counter for the workbench's lifetime. Dev-only
37
+ * instrumentation: the wrap is installed on mount and fully restored on
38
+ * unmount, and it never alters listener behavior — only counts it.
39
+ */
40
+ export declare function useChatDebugWindowListenerProbe(): {
41
+ active: number;
42
+ peak: number;
43
+ byType: Record<string, number>;
44
+ };
14
45
  /**
15
46
  * Passive probes around the mounted chat component: event-to-paint latency,
16
47
  * dropped frames (reported by playback through `recordFrame`), DOM size,
@@ -46,3 +77,4 @@ export declare function useChatDebugDiagnostics(options: {
46
77
  reset: () => void;
47
78
  recordFrame: (frameDeltaMs: number) => void;
48
79
  };
80
+ export {};
@@ -2,6 +2,64 @@ import { nextTick, onBeforeUnmount, onMounted, reactive, watch } from "vue";
2
2
  export function shouldIgnoreChatDebugLayoutShift(entry, ignoreUntil) {
3
3
  return entry.hadRecentInput || entry.startTime <= ignoreUntil;
4
4
  }
5
+ function resolveListenerCapture(options) {
6
+ if (typeof options === "boolean")
7
+ return options;
8
+ return Boolean(options?.capture);
9
+ }
10
+ export function createChatDebugListenerTracker(target) {
11
+ const counters = reactive({ active: 0, peak: 0, byType: {} });
12
+ const tracked = [];
13
+ let originalAdd = null;
14
+ let originalRemove = null;
15
+ function findIndex(type, listener, capture) {
16
+ return tracked.findIndex(
17
+ (entry) => entry.type === type && entry.listener === listener && entry.capture === capture
18
+ );
19
+ }
20
+ function install() {
21
+ if (originalAdd)
22
+ return;
23
+ originalAdd = target.addEventListener;
24
+ originalRemove = target.removeEventListener;
25
+ target.addEventListener = function(type, listener, options) {
26
+ const capture = resolveListenerCapture(options);
27
+ if (listener && findIndex(type, listener, capture) === -1) {
28
+ tracked.push({ type, listener, capture });
29
+ counters.active = tracked.length;
30
+ counters.peak = Math.max(counters.peak, counters.active);
31
+ counters.byType[type] = (counters.byType[type] ?? 0) + 1;
32
+ }
33
+ return originalAdd.call(this, type, listener, options);
34
+ };
35
+ target.removeEventListener = function(type, listener, options) {
36
+ const index = findIndex(type, listener, resolveListenerCapture(options));
37
+ if (index !== -1) {
38
+ const [removed] = tracked.splice(index, 1);
39
+ counters.active = tracked.length;
40
+ counters.byType[removed.type] = Math.max(0, (counters.byType[removed.type] ?? 1) - 1);
41
+ }
42
+ return originalRemove.call(this, type, listener, options);
43
+ };
44
+ }
45
+ function uninstall() {
46
+ if (!originalAdd || !originalRemove)
47
+ return;
48
+ target.addEventListener = originalAdd;
49
+ target.removeEventListener = originalRemove;
50
+ originalAdd = null;
51
+ originalRemove = null;
52
+ }
53
+ return { counters, install, uninstall };
54
+ }
55
+ export function useChatDebugWindowListenerProbe() {
56
+ const tracker = typeof window === "undefined" ? createChatDebugListenerTracker({ addEventListener: () => {
57
+ }, removeEventListener: () => {
58
+ } }) : createChatDebugListenerTracker(window);
59
+ onMounted(tracker.install);
60
+ onBeforeUnmount(tracker.uninstall);
61
+ return tracker.counters;
62
+ }
5
63
  export function useChatDebugDiagnostics(options) {
6
64
  const diagnostics = reactive({
7
65
  paintLatencyMs: 0,
@@ -1,4 +1,4 @@
1
- import type { ChatDebugScenario } from './chat-debug-scenarios.js';
1
+ import type { ChatDebugEvent, ChatDebugScenario } from './chat-debug-scenarios.js';
2
2
  export type ChatDebugPlaybackSpeed = '1x' | '4x' | 'gap';
3
3
  export declare function formatChatDebugTime(value: number): string;
4
4
  /**
@@ -19,7 +19,7 @@ export declare function useChatDebugPlayback(scenarios: ChatDebugScenario[], opt
19
19
  seekRevision: import("vue").Ref<number, number>;
20
20
  playbackState: import("vue").ComputedRef<import("./chat-debug-scenarios.js").ChatDebugPlaybackState>;
21
21
  appliedEventCount: import("vue").ComputedRef<number>;
22
- currentEvent: import("vue").ComputedRef<import("./chat-debug-scenarios.js").ChatDebugEvent | null>;
22
+ currentEvent: import("vue").ComputedRef<ChatDebugEvent | null>;
23
23
  progress: import("vue").ComputedRef<number>;
24
24
  seek: (nextElapsedMs: number) => void;
25
25
  restart: () => void;
@@ -28,4 +28,5 @@ export declare function useChatDebugPlayback(scenarios: ChatDebugScenario[], opt
28
28
  jumpToEnd: () => void;
29
29
  stepEvent: (direction: -1 | 1) => void;
30
30
  selectSpeed: (nextSpeed: ChatDebugPlaybackSpeed) => void;
31
+ applyManualEvent: (event: ChatDebugEvent) => void;
31
32
  };
@@ -1,5 +1,5 @@
1
- import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
2
- import { countAppliedChatDebugEvents, resolveChatDebugPlayback } from "./chat-debug-scenarios.js";
1
+ import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
2
+ import { applyChatDebugEvent, countAppliedChatDebugEvents, resolveChatDebugPlayback } from "./chat-debug-scenarios.js";
3
3
  export function formatChatDebugTime(value) {
4
4
  const totalSeconds = Math.max(0, value) / 1e3;
5
5
  const minutes = Math.floor(totalSeconds / 60);
@@ -15,20 +15,32 @@ export function useChatDebugPlayback(scenarios, options = {}) {
15
15
  const selectedScenario = computed(
16
16
  () => scenarios.find((scenario) => scenario.id === selectedScenarioId.value) ?? scenarios[0]
17
17
  );
18
- const playbackState = computed(() => resolveChatDebugPlayback(selectedScenario.value, elapsedMs.value));
18
+ const manualEvents = shallowRef([]);
19
+ const playbackState = computed(() => manualEvents.value.reduce(
20
+ applyChatDebugEvent,
21
+ resolveChatDebugPlayback(selectedScenario.value, elapsedMs.value)
22
+ ));
19
23
  const appliedEventCount = computed(() => countAppliedChatDebugEvents(selectedScenario.value, elapsedMs.value));
20
24
  const currentEvent = computed(() => selectedScenario.value.events[appliedEventCount.value - 1] ?? null);
21
25
  const progress = computed(() => selectedScenario.value.durationMs === 0 ? 0 : elapsedMs.value / selectedScenario.value.durationMs);
22
26
  const uniqueEventTimes = computed(() => Array.from(new Set(selectedScenario.value.events.map((event) => event.atMs))));
23
27
  let animationFrame = 0;
24
28
  let previousFrameAt = 0;
29
+ function applyManualEvent(event) {
30
+ manualEvents.value = [...manualEvents.value, event];
31
+ }
25
32
  function setElapsed(nextElapsedMs) {
26
- elapsedMs.value = Math.min(Math.max(nextElapsedMs, 0), selectedScenario.value.durationMs);
33
+ const clamped = Math.min(Math.max(nextElapsedMs, 0), selectedScenario.value.durationMs);
34
+ if (manualEvents.value.length > 0 && countAppliedChatDebugEvents(selectedScenario.value, clamped) !== appliedEventCount.value) {
35
+ manualEvents.value = [];
36
+ }
37
+ elapsedMs.value = clamped;
27
38
  if (elapsedMs.value >= selectedScenario.value.durationMs)
28
39
  playing.value = false;
29
40
  }
30
41
  function seek(nextElapsedMs) {
31
42
  seekRevision.value += 1;
43
+ manualEvents.value = [];
32
44
  setElapsed(nextElapsedMs);
33
45
  }
34
46
  function restart() {
@@ -43,6 +55,7 @@ export function useChatDebugPlayback(scenarios, options = {}) {
43
55
  function togglePlayback() {
44
56
  if (elapsedMs.value >= selectedScenario.value.durationMs) {
45
57
  elapsedMs.value = 0;
58
+ manualEvents.value = [];
46
59
  options.onReset?.();
47
60
  }
48
61
  playing.value = !playing.value;
@@ -130,6 +143,7 @@ export function useChatDebugPlayback(scenarios, options = {}) {
130
143
  togglePlayback,
131
144
  jumpToEnd,
132
145
  stepEvent,
133
- selectSpeed
146
+ selectSpeed,
147
+ applyManualEvent
134
148
  };
135
149
  }
@@ -46,6 +46,16 @@ function groupReasoningSteps(parsedSteps) {
46
46
  function getReasoningCacheKey(messageId, reasoningData) {
47
47
  return `${messageId}:${JSON.stringify(reasoningData)}`;
48
48
  }
49
+ function pruneReasoningCache(cache, messages) {
50
+ if (cache.bySignature.size === 0)
51
+ return;
52
+ const liveMessageIds = new Set(messages.map((message) => message.id));
53
+ for (const key of cache.bySignature.keys()) {
54
+ const messageId = key.slice(0, key.indexOf(":"));
55
+ if (!liveMessageIds.has(messageId))
56
+ cache.bySignature.delete(key);
57
+ }
58
+ }
49
59
  function getParsedReasoning(cache, messageId, reasoningData) {
50
60
  const referenceHit = cache.byReference.get(reasoningData);
51
61
  if (referenceHit?.messageId === messageId)
@@ -176,6 +186,7 @@ function parseReasoningFromPolling(reasoning) {
176
186
  return { steps: groupReasoningSteps(allSteps), thinkingTexts, currentAnswerParts, currentAnswerStepTexts };
177
187
  }
178
188
  export function resolveReasoningDisplayMessages(messages, cache = createReasoningCache()) {
189
+ pruneReasoningCache(cache, messages);
179
190
  const renderedSteps = /* @__PURE__ */ new Set();
180
191
  return messages.map((message) => {
181
192
  if (message.role !== "assistant" || !message.reasoningData || !Array.isArray(message.reasoningData))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
- "version": "3.16.0-rc.4",
3
+ "version": "3.16.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {