@meistrari/chat-nuxt 3.16.0-rc.3 → 3.16.0-rc.5

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 (25) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/assets/css/markstream.css +1 -1
  3. package/dist/runtime/citations/citation-links.d.ts +7 -0
  4. package/dist/runtime/citations/citation-links.js +24 -0
  5. package/dist/runtime/conversations/components/chat-conversation-item.vue +3 -10
  6. package/dist/runtime/conversations/composables/conversation-item.d.ts +0 -2
  7. package/dist/runtime/conversations/composables/conversation-item.js +16 -19
  8. package/dist/runtime/files/components/chat-vault-link.vue +15 -4
  9. package/dist/runtime/messages/components/chat-debug-conversation-rail.d.vue.ts +16 -0
  10. package/dist/runtime/messages/components/chat-debug-conversation-rail.vue +104 -0
  11. package/dist/runtime/messages/components/chat-debug-conversation-rail.vue.d.ts +16 -0
  12. package/dist/runtime/messages/components/chat-debug-workbench.vue +127 -18
  13. package/dist/runtime/messages/components/chat-message-bubble.vue +10 -4
  14. package/dist/runtime/messages/debug/chat-debug-citations.d.ts +8 -0
  15. package/dist/runtime/messages/debug/chat-debug-citations.js +60 -0
  16. package/dist/runtime/messages/debug/chat-debug-scenarios.d.ts +29 -1
  17. package/dist/runtime/messages/debug/chat-debug-scenarios.js +57 -1
  18. package/dist/runtime/messages/debug/chat-debug-switch-scenario.d.ts +21 -0
  19. package/dist/runtime/messages/debug/chat-debug-switch-scenario.js +271 -0
  20. package/dist/runtime/messages/debug/use-chat-debug-diagnostics.d.ts +32 -0
  21. package/dist/runtime/messages/debug/use-chat-debug-diagnostics.js +58 -0
  22. package/dist/runtime/messages/debug/use-chat-debug-playback.d.ts +3 -2
  23. package/dist/runtime/messages/debug/use-chat-debug-playback.js +19 -5
  24. package/dist/runtime/messages/message-reasoning.js +11 -0
  25. package/package.json +1 -1
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.3",
4
+ "version": "3.16.0-rc.5",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -1 +1 @@
1
- .markstream-vue{color:#171717;font-family:Inter,"Inter Fallback: Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:-.15px;line-height:24px;--list-item-marker:#a3a3a3;--list-item-counter-marker:#262626}.markstream-vue code,.markstream-vue kbd,.markstream-vue pre,.markstream-vue samp{font-family:Geist Mono Variable,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:13px;font-variation-settings:normal;line-height:1.5}.markstream-vue ol,.markstream-vue ul{margin:8px 0;padding-left:1.5em}.markstream-vue .hr-node{border-color:#e5e5e5;border-top-width:.5px;margin-bottom:24px;margin-top:24px}.markstream-vue .strong-node{font-weight:580}.markstream-vue .list-node.list-node{margin-bottom:8px;margin-left:4px;margin-top:8px;padding-left:1em}.markstream-vue ul{list-style:disc;list-style-position:outside}.markstream-vue ol{list-style:decimal;list-style-position:outside}.markstream-vue li{margin:4px 0}.markstream-vue .paragraph-node{margin:0}.markstream-vue>.node-slot[data-node-type=paragraph]:not(:first-child){margin-top:12px}.markstream-vue .my-8{margin-bottom:8px;margin-top:8px}.markstream-vue .heading-node.heading-1,.markstream-vue h1{font-size:24px;font-weight:540;letter-spacing:-.8px;line-height:28px;margin:24px 0 16px}.markstream-vue .heading-node.heading-2,.markstream-vue h2{font-size:20px;font-weight:540;letter-spacing:-.5px;line-height:24px;margin:20px 0 12px}.markstream-vue .heading-node.heading-3,.markstream-vue h3{font-size:20px;font-weight:540;letter-spacing:-.5px;line-height:24px;margin:16px 0 8px}.markstream-vue .heading-node.heading-4,.markstream-vue h4{font-size:16px;font-weight:540;letter-spacing:-.2px;line-height:20px;margin:12px 0 6px}.markstream-vue .heading-node.heading-5,.markstream-vue h5{font-size:14px;font-weight:540;letter-spacing:-.15px;line-height:16px;margin:8px 0 4px}.markstream-vue .heading-node.heading-6,.markstream-vue h6{font-size:12px;font-weight:540;letter-spacing:-.1px;line-height:14px;margin:8px 0 4px}.markstream-vue [class*=tooltip],[class*=tooltip-element]{background-color:var(--neutral-900,#1a1a1a)!important;border-radius:4px;color:#fff!important;font-size:12px;padding:4px 8px}.mention-tag{align-items:center;border-radius:9999px;display:inline-flex;font-size:.875em;font-weight:500;gap:4px;padding:2px 8px;vertical-align:middle;white-space:nowrap}.mention-tag-file{background-color:rgba(127,189,247,.12);color:#7fbdf7}.mention-tag-skill{background-color:rgba(155,111,222,.12);color:#9b6fde}.mention-tag-canvas{background-color:rgba(234,161,65,.12);color:#d4882e}.mention-tag>span[class*=i-ph]{flex-shrink:0}
1
+ .markstream-vue{font-family:inherit;--at-apply:"body-16-regular leading-24px text-primary";--list-item-marker:#a3a3a3;--list-item-counter-marker:#262626}.markstream-vue code,.markstream-vue kbd,.markstream-vue pre,.markstream-vue samp{font-family:Geist Mono Variable,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:13px;font-variation-settings:normal;letter-spacing:0;line-height:1.5}.markstream-vue ol,.markstream-vue ul{margin:8px 0;padding-left:1.5em}.markstream-vue .hr-node{border-color:#e5e5e5;border-top-width:.5px;margin-bottom:24px;margin-top:24px}.markstream-vue .strong-node{--at-apply:"font-580"}.markstream-vue .list-node.list-node{margin-bottom:8px;margin-left:4px;margin-top:8px;padding-left:1em}.markstream-vue ul{list-style:disc;list-style-position:outside}.markstream-vue ol{list-style:decimal;list-style-position:outside}.markstream-vue li{margin:4px 0}.markstream-vue .paragraph-node{--at-apply:"body-16-regular leading-24px";margin:0}.markstream-vue>.node-slot[data-node-type=paragraph]:not(:first-child){margin-top:12px}.markstream-vue .my-8{margin-bottom:8px;margin-top:8px}.markstream-vue .heading-node.heading-1,.markstream-vue h1{--at-apply:"heading-h2-semibold text-primary";margin:24px 0 8px}.markstream-vue .heading-node.heading-2,.markstream-vue h2{--at-apply:"heading-h3-semibold text-primary";margin:24px 0 8px}.markstream-vue .heading-node.heading-3,.markstream-vue h3{--at-apply:"heading-h4-semibold text-primary";margin:20px 0 8px}.markstream-vue .heading-node.heading-4,.markstream-vue .heading-node.heading-5,.markstream-vue .heading-node.heading-6,.markstream-vue h4,.markstream-vue h5,.markstream-vue h6{--at-apply:"heading-h5-semibold text-primary";margin:16px 0 8px}.markstream-vue>.node-slot:first-child .heading-node{margin-top:0}.markstream-vue [class*=tooltip],[class*=tooltip-element]{background-color:var(--neutral-900,#1a1a1a)!important;border-radius:4px;color:#fff!important;font-size:12px;padding:4px 8px}.mention-tag{align-items:center;border-radius:9999px;display:inline-flex;font-size:.875em;font-weight:500;gap:4px;padding:2px 8px;vertical-align:middle;white-space:nowrap}.mention-tag-file{background-color:rgba(127,189,247,.12);color:#7fbdf7}.mention-tag-skill{background-color:rgba(155,111,222,.12);color:#9b6fde}.mention-tag-canvas{background-color:rgba(234,161,65,.12);color:#d4882e}.mention-tag>span[class*=i-ph]{flex-shrink:0}
@@ -30,3 +30,10 @@ export declare function isCitationUrl(url: string): boolean;
30
30
  export declare function parseCitationUrl(url: string): CitationRef | null;
31
31
  /** Cache key for resolution state: the locator addresses a spot inside the same document. */
32
32
  export declare function citationCacheKey(ref: Pick<CitationRef, 'namespace' | 'id'>): string;
33
+ /**
34
+ * Rewrites bare `cite://` tokens into `[id](cite://...)` markdown links so they render inline
35
+ * through the link node instead of splitting the surrounding paragraph into block segments.
36
+ * Square brackets that only wrap a token are dropped, since the citation chip already reads as
37
+ * a reference.
38
+ */
39
+ export declare function linkifyBareCitations(text: string): string;
@@ -54,3 +54,27 @@ function parsePositiveInt(value) {
54
54
  export function citationCacheKey(ref) {
55
55
  return `${ref.namespace}/${ref.id}`;
56
56
  }
57
+ const BARE_CITE_REWRITE_REGEX = /\[[ \t]*(cite:\/\/[^\s"'<>`\[\]()*~]+)[ \t]*\](?!\()|(?<!\]\()(?<!<)(?<!\]:[ \t]*)(cite:\/\/[^\s"'<>`\[\]()*~]+)(?!\]\()/gi;
58
+ const MARKDOWN_CODE_REGEX = /(```|~~~)[\s\S]*?(?:\1|$)|``+[^`]*``+|`[^`]*`/g;
59
+ export function linkifyBareCitations(text) {
60
+ if (!text.toLowerCase().includes(CITE_SCHEME))
61
+ return text;
62
+ let result = "";
63
+ let lastEnd = 0;
64
+ for (const match of text.matchAll(MARKDOWN_CODE_REGEX)) {
65
+ result += linkifyProse(text.slice(lastEnd, match.index)) + match[0];
66
+ lastEnd = match.index + match[0].length;
67
+ }
68
+ return result + linkifyProse(text.slice(lastEnd));
69
+ }
70
+ function linkifyProse(text) {
71
+ return text.replace(BARE_CITE_REWRITE_REGEX, (match, bracketed, bare) => {
72
+ const token = bracketed ?? bare ?? "";
73
+ const url = token.replace(/[.,:;!?]+$/, "");
74
+ const trailing = token.slice(url.length);
75
+ const citation = parseCitationUrl(url);
76
+ if (!citation)
77
+ return match;
78
+ return `[${citation.id}](${url})${trailing}`;
79
+ });
80
+ }
@@ -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,
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { parseCitationUrl } from "../../citations/citation-links";
2
+ import { isCitationUrl, parseCitationUrl } from "../../citations/citation-links";
3
3
  import ChatUrlCitation from "../../citations/components/chat-url-citation.vue";
4
4
  import { parseUrlCitation } from "../../citations/url-citation";
5
5
  import { useEmbedConfig } from "../../embed/composables/embed-config";
@@ -22,6 +22,13 @@ const detectedCitation = computed(() => {
22
22
  return null;
23
23
  return parseCitationUrl(props.node.href);
24
24
  });
25
+ const citationLabel = computed(() => {
26
+ const citation = detectedCitation.value;
27
+ const label = linkText.value.trim();
28
+ if (!citation || !label || label === citation.id || isCitationUrl(label))
29
+ return void 0;
30
+ return label;
31
+ });
25
32
  const detectedUrlCitation = computed(() => parseUrlCitation(props.node.href, linkText.value, props.node.title));
26
33
  </script>
27
34
 
@@ -37,7 +44,7 @@ const detectedUrlCitation = computed(() => parseUrlCitation(props.node.href, lin
37
44
  <ChatCitation
38
45
  v-else-if="detectedCitation"
39
46
  :citation="detectedCitation"
40
- :label="linkText"
47
+ :label="citationLabel"
41
48
  />
42
49
  <ChatUrlCitation
43
50
  v-else-if="detectedUrlCitation"
@@ -49,6 +56,10 @@ const detectedUrlCitation = computed(() => parseUrlCitation(props.node.href, lin
49
56
  :title="node.title || void 0"
50
57
  target="_blank"
51
58
  rel="noopener noreferrer"
52
- class="text-blue-600 hover:text-blue-800 underline"
53
- >{{ linkText }}</a>
59
+ class="chat-inline-link"
60
+ >{{ linkText }}<span class="chat-inline-link__icon" i-ph-arrow-up-right-bold aria-hidden="true" /></a>
54
61
  </template>
62
+
63
+ <style scoped>
64
+ .chat-inline-link{color:inherit;text-decoration:underline;text-decoration-color:#a3a3a3;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:3px;transition:text-decoration-color .15s ease}.chat-inline-link:hover{text-decoration-color:#525252}.chat-inline-link__icon{display:inline-block;font-size:.7em;margin-left:.2em;vertical-align:.05em}
65
+ </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;
@@ -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,8 +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";
5
+ import { createChatDebugCitationsConfig } from "../debug/chat-debug-citations";
6
+ import { createReasoningCache, resolveReasoningDisplayMessages } from "../message-reasoning";
4
7
  import { createChatDebugScenarios } from "../debug/chat-debug-scenarios";
5
- import { useChatDebugDiagnostics } from "../debug/use-chat-debug-diagnostics";
8
+ import { useChatDebugDiagnostics, useChatDebugWindowListenerProbe } from "../debug/use-chat-debug-diagnostics";
6
9
  import { formatChatDebugTime as formatTime, useChatDebugPlayback } from "../debug/use-chat-debug-playback";
7
10
  const scenarios = createChatDebugScenarios();
8
11
  const viewportWidth = ref("default");
@@ -12,7 +15,8 @@ const rootRef = ref(null);
12
15
  const stageRef = ref(null);
13
16
  provideEmbedConfig({
14
17
  workspaceId: "chat-debug-workbench",
15
- hideSidebar: true
18
+ hideSidebar: true,
19
+ citations: createChatDebugCitationsConfig()
16
20
  }, { syncGlobal: false });
17
21
  const {
18
22
  selectedScenario,
@@ -30,7 +34,8 @@ const {
30
34
  togglePlayback,
31
35
  jumpToEnd,
32
36
  stepEvent,
33
- selectSpeed
37
+ selectSpeed,
38
+ applyManualEvent
34
39
  } = useChatDebugPlayback(scenarios, {
35
40
  onFrame: handlePlaybackFrame,
36
41
  onReset: handlePlaybackReset
@@ -45,11 +50,47 @@ const { diagnostics, reset: resetDiagnostics, recordFrame } = useChatDebugDiagno
45
50
  speed,
46
51
  seekRevision
47
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
+ }
48
87
  function handlePlaybackFrame(frameDeltaMs) {
49
88
  recordFrame(frameDeltaMs);
50
89
  }
51
90
  function handlePlaybackReset() {
52
91
  resetDiagnostics();
92
+ switchCount.value = 0;
93
+ railResetKey.value += 1;
53
94
  }
54
95
  const messageCharacters = computed(() => playbackState.value.messages.reduce(
55
96
  (total, message) => total + message.content.length,
@@ -187,24 +228,35 @@ function eventToneClass(tone) {
187
228
  </div>
188
229
  </div>
189
230
 
190
- <div ref="stageRef" class="stage-viewport" min-h-0 grow flex justify-center px-24px pb-24px>
191
- <div
192
- class="chat-under-test"
193
- :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="[
194
245
  `chat-under-test--${viewportWidth}`,
195
246
  showBounds && 'chat-under-test--bounded'
196
247
  ]"
197
- >
198
- <TelaChatBody :key="conversationVariant" :variant="conversationVariant" h-full>
199
- <ChatMessageList
200
- :messages="playbackState.messages"
201
- :loading="playbackState.loading"
202
- :streaming-active-message-id="playbackState.streamingActiveMessageId"
203
- :preparing-steps="playbackState.preparingSteps"
204
- :variant="conversationVariant"
205
- @retry="stepEvent(-1)"
206
- />
207
- </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>
208
260
  </div>
209
261
  </div>
210
262
  </main>
@@ -346,6 +398,63 @@ function eventToneClass(tone) {
346
398
  </dl>
347
399
  </section>
348
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
+
349
458
  <section mt-28px>
350
459
  <div flex items-center justify-between gap-12px mb-14px>
351
460
  <div body-10-regular text-tertiary uppercase tracking-0.08em>
@@ -3,6 +3,7 @@ import { useChatAuth } from "#chat-auth";
3
3
  import { useEmbedConfig } from "../../embed/composables/embed-config";
4
4
  import { useWorkspaceMembers } from "../../workspace/composables/workspace-members";
5
5
  import { isPromptTooLongFailure } from "../../types/schemas/chat/messages.js";
6
+ import { linkifyBareCitations } from "../../citations/citation-links";
6
7
  import { parseContentWithFiles } from "../../files/content-detection";
7
8
  import ChatVaultLink from "../../files/components/chat-vault-link.vue";
8
9
  const props = defineProps({
@@ -56,7 +57,12 @@ function formatTime(date) {
56
57
  const d = date instanceof Date ? date : new Date(date);
57
58
  return d.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
58
59
  }
59
- const displayContent = computed(() => props.message.content ?? "");
60
+ const displayContent = computed(() => {
61
+ const content = props.message.content ?? "";
62
+ if (props.message.role !== "assistant" || !embedConfig?.citations.value)
63
+ return content;
64
+ return linkifyBareCitations(content);
65
+ });
60
66
  const userContentHtml = computed(() => {
61
67
  if (props.message.role !== "user")
62
68
  return "";
@@ -93,7 +99,7 @@ const contentSegments = computed(() => {
93
99
  const content = displayContent.value;
94
100
  if (!content)
95
101
  return [];
96
- return parseContentWithFiles(content, { detectCitations: Boolean(embedConfig?.citations.value) });
102
+ return parseContentWithFiles(content);
97
103
  });
98
104
  const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type !== "text"));
99
105
  </script>
@@ -227,7 +233,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
227
233
  </div>
228
234
  </template>
229
235
  <template v-else>
230
- <div v-if="hasDetectedFiles" body-16-regular text-black-900>
236
+ <div v-if="hasDetectedFiles" body-16-regular text-primary>
231
237
  <template v-for="(segment, index) in contentSegments" :key="index">
232
238
  <ClientOnly v-if="segment.type === 'text'">
233
239
  <AppMarkdownRender
@@ -266,7 +272,7 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
266
272
  :smooth-streaming="isPending ? 'auto' : false"
267
273
  :typewriter="isPending"
268
274
  mode="chat"
269
- text-black-900
275
+ text-primary
270
276
  />
271
277
  </ClientOnly>
272
278
  <div
@@ -0,0 +1,8 @@
1
+ import type { CitationRef } from '../../citations/citation-links.js';
2
+ import type { ChatCitationsConfig, CitationMeta } from '../../citations/types.js';
3
+ /**
4
+ * Metadata derived from the reference itself, so any `cite://` a scenario emits resolves
5
+ * without a backend or a per-document fixture table.
6
+ */
7
+ export declare function chatDebugCitationMeta(ref: CitationRef): CitationMeta;
8
+ export declare function createChatDebugCitationsConfig(): ChatCitationsConfig;