14 Commits

Author SHA1 Message Date
zoeissleeping 2b9f8d64d7 bug fixes, and performance improvements, and drawer 2026-09-27 18:44:51 -05:00
zoeissleeping d48d8163a7 feat: remove search rerank 2026-09-27 09:45:19 -05:00
zoeissleeping bb5795e00d feat: custom tool renderers 2026-09-25 19:49:02 -05:00
zoeissleeping 3f81f96bda feat: expand model icons, guard uploads, and handle seach env better 2026-09-10 04:25:37 -05:00
zoeissleeping 4b99921023 feat(markdown): Migrate to comark for markdown rendering 2026-08-11 03:18:28 -05:00
zoeissleeping c9e48687ef fix: Dramatically better auto scroll 2026-08-04 02:38:36 -05:00
zoeissleeping a836082de8 feat: per-agent tools toggle and better python scratchpad
Add ToolSelector for enabling sandboxed python per agent, drop unsafe
filesystem/bash tools, bundle fetchUrl with web search, and return
Monty's last expression value so agents need not print.
2026-07-29 14:18:37 -05:00
zoeissleeping 5aebd30808 fix: copy tool calls, generations, and timestamps when forking topics
Forked topics dropped toolCall/generation relations and rewrote part
lastUpdatedAt, which broke tool rendering, token stats, and reasoning durations.
2026-07-27 17:37:40 -05:00
zoeissleeping 166052012d fix: preserve nested hard-break content and use timestamptz
remarkSplitBlocks was splicing after-break paragraphs onto the root
tree, which dropped content inside list items and other nested parents.
Insert the split sibling into the actual parent instead.

Also store timestamps as timestamptz so Drizzle no longer treats naive
timestamp values as UTC and shifts displayed times by the server offset.
2026-07-19 14:03:14 -05:00
zoeissleeping 28ef0c8a07 feat: add export to topics dropdown 2026-07-16 14:29:07 -05:00
zoeissleeping 886f385835 feat: add timestamp and forking 2026-07-15 15:49:38 -05:00
zoeissleeping 8ccaa824dd feat: file upload retry, secure file tokens, UI polish
- Add HMAC-based file token auth for secure AI model file access
- Add file upload retry with exponential backoff (max 3 retries)
- File endpoint now requires session auth or signed token
- Support assistant role messages in chat input
- Optimistic UI for attachments on message send
- Verify topic ownership before allowing messages
- Switch web scraping to Firecrawl API
- Agent profile page layout fixes (proper flex overflow)
- Add quick switcher (Ctrl+K) to sidenav
- Clean up longcat.ts and stale comments
2026-06-06 00:16:39 -05:00
zoeissleeping 47009b1f0a refactor: un-hardcode file urls 2026-05-12 10:28:33 -05:00
zoeissleeping 9550d44220 merge: drizzle rewrite into main
Complete migration from Triplit to PostgreSQL + Drizzle ORM with all subsequent feature work and fixes.
2026-05-11 17:44:22 -05:00
108 changed files with 14324 additions and 2084 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ app/ # Nuxt 4 application (SSR)
├── layouts/ # Auth and default layouts ├── layouts/ # Auth and default layouts
├── middleware/ # Global auth middleware ├── middleware/ # Global auth middleware
├── pages/ # File-based routing (/, /auth/*, /agent/*) ├── pages/ # File-based routing (/, /auth/*, /agent/*)
├── plugins/ # Auth plugins (client/server), remark markdown ├── plugins/ # Auth plugins (client/server)
├── types/ # TypeScript interfaces ├── types/ # TypeScript interfaces
└── utils/ # Crypto, model-mapping, search utilities └── utils/ # Crypto, model-mapping, search utilities
+1
View File
@@ -4,6 +4,7 @@
- [x] Make dropdowns a singleton component (work in progress) - [x] Make dropdowns a singleton component (work in progress)
- [ ] Make the sidebar better :kekdoggo:. It's annoying to manage across routes - [ ] Make the sidebar better :kekdoggo:. It's annoying to manage across routes
- [x] Make topics manually renameable - [x] Make topics manually renameable
- [ ] Surface auto-rename failures through the future toast/error UI; add actionable retry and bounded bulk naming for unnamed/provisional topics
- [ ] Improve latex rendering, make single line equations work _well_, and likely make it configurable - [ ] Improve latex rendering, make single line equations work _well_, and likely make it configurable
- [x] Implement appearence settings - [x] Implement appearence settings
- [x] Accent colors - [x] Accent colors
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import '~/assets/css/reset.css'; import '~/assets/css/reset.css';
import '~/assets/css/base.css'; import '~/assets/css/base.css';
import 'katex/dist/katex.min.css';
const { user } = useAuth(); const { user } = useAuth();
const { accent, neutral, hinting, refresh: refreshSettings } = await useUserSettings(); const { accent, neutral, hinting, refresh: refreshSettings } = await useUserSettings();
@@ -24,7 +25,7 @@ watchEffect(() => {
}); });
if (import.meta.client) { if (import.meta.client) {
// force shiki into browser rendering only // Force Shiki into browser rendering only.
window.sessionStorage.setItem('mdc-shiki-highlighter', 'browser'); window.sessionStorage.setItem('mdc-shiki-highlighter', 'browser');
} }
</script> </script>
+10 -5
View File
@@ -81,6 +81,7 @@
--color-border: color-mix(in srgb, rgba(255, 255, 255, 0.1), var(--color-accent) var(--accent-hinting)); --color-border: color-mix(in srgb, rgba(255, 255, 255, 0.1), var(--color-accent) var(--accent-hinting));
--color-border-active: color-mix(in srgb, rgba(255, 255, 255, 0.16), var(--color-accent) var(--accent-hinting)); --color-border-active: color-mix(in srgb, rgba(255, 255, 255, 0.16), var(--color-accent) var(--accent-hinting));
} }
:root.light { :root.light {
@@ -107,6 +108,7 @@
--color-border: color-mix(in srgb, rgba(0, 0, 0, 0.08), var(--color-accent) var(--accent-hinting)); --color-border: color-mix(in srgb, rgba(0, 0, 0, 0.08), var(--color-accent) var(--accent-hinting));
--color-border-active: color-mix(in srgb, rgba(0, 0, 0, 0.14), var(--color-accent) var(--accent-hinting)); --color-border-active: color-mix(in srgb, rgba(0, 0, 0, 0.14), var(--color-accent) var(--accent-hinting));
} }
/* :root.dark { /* :root.dark {
@@ -316,16 +318,19 @@ button.accent:hover {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='18' height='18'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='18' height='18'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0'/%3E%3C/svg%3E");
} }
.reasoning-container.middle { .reasoning-container.top {
mask-image: linear-gradient(#000, #000, transparent 0, #000 12%, #000 88%, transparent) -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 calc(100% - 24px), transparent 100%);
mask-image: linear-gradient(to bottom, #000 0%, #000 calc(100% - 24px), transparent 100%);
} }
.reasoning-container.top { .reasoning-container.middle {
mask-image: linear-gradient(#000, transparent, #000 0, #000 12%, #000 88%, transparent) -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 24px, #000 calc(100% - 24px), transparent 100%);
mask-image: linear-gradient(to bottom, transparent 0%, #000 24px, #000 calc(100% - 24px), transparent 100%);
} }
.reasoning-container.bottom { .reasoning-container.bottom {
mask-image: linear-gradient(transparent, #000, transparent 0, #000 12%, #000 88%, #000) -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 24px, #000 100%);
mask-image: linear-gradient(to bottom, transparent 0%, #000 24px, #000 100%);
} }
/* Markdown renderer */ /* Markdown renderer */
+25 -5
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import ImageViewer from '~/components/ImageViewer.vue'; import ImageViewer from '~/components/ImageViewer.vue';
import { resolveFileUrl } from '~~/utils/url';
const props = defineProps<{ const props = defineProps<{
file: { file: {
@@ -9,14 +10,21 @@ const props = defineProps<{
status?: 'uploading' | 'uploaded' | 'error'; status?: 'uploading' | 'uploaded' | 'error';
url: string; url: string;
progress?: number; progress?: number;
retryCount?: number;
}; };
}>(); }>();
const emit = defineEmits<{
retry: [];
}>();
const isImage = computed(() => props.file.mimeType.startsWith('image/')); const isImage = computed(() => props.file.mimeType.startsWith('image/'));
const isVideo = computed(() => props.file.mimeType.startsWith('video/')); const isVideo = computed(() => props.file.mimeType.startsWith('video/'));
const isAudio = computed(() => props.file.mimeType.startsWith('audio/')); const isAudio = computed(() => props.file.mimeType.startsWith('audio/'));
const isPdf = computed(() => props.file.mimeType === 'application/pdf'); const isPdf = computed(() => props.file.mimeType === 'application/pdf');
const resolvedUrl = computed(() => resolveFileUrl(props.file.url));
const imageViewerOpen = ref(false); const imageViewerOpen = ref(false);
const imageRef = ref<HTMLImageElement | null>(null); const imageRef = ref<HTMLImageElement | null>(null);
const imageViewerOriginRect = ref<DOMRect | null>(null); const imageViewerOriginRect = ref<DOMRect | null>(null);
@@ -45,16 +53,16 @@ function openImageViewer() {
<template> <template>
<div class="relative h-full w-fit flex"> <div class="relative h-full w-fit flex">
<div v-if="isImage"> <div v-if="isImage">
<img ref="imageRef" :src="props.file.url" <img ref="imageRef" :src="resolvedUrl"
class="rounded-lg h-full w-full max-h-36 max-w-64 object-cover cursor-zoom-in hover:opacity-90 transition-opacity" class="rounded-lg h-full w-full max-h-36 max-w-64 object-cover cursor-zoom-in hover:opacity-90 transition-opacity"
@click="openImageViewer" /> @click="openImageViewer" />
<ImageViewer v-if="imageViewerOpen" :src="props.file.url" :alt="props.file.name" <ImageViewer v-if="imageViewerOpen" :src="resolvedUrl" :alt="props.file.name"
:origin-rect="imageViewerOriginRect" :origin-element="imageRef" :origin-rect="imageViewerOriginRect" :origin-element="imageRef"
@close="imageViewerOpen = false" /> @close="imageViewerOpen = false" />
</div> </div>
<video v-else-if="isVideo" controls :src="props.file.url" <video v-else-if="isVideo" controls :src="resolvedUrl"
class="rounded-lg h-full w-full max-h-36 max-w-64 object-cover" /> class="rounded-lg h-full w-full max-h-36 max-w-64 object-cover" />
<audio v-else-if="isAudio" controls :src="props.file.url" class="rounded-lg h-full w-full max-h-36 max-w-64" /> <audio v-else-if="isAudio" controls :src="resolvedUrl" class="rounded-lg h-full w-full max-h-36 max-w-64" />
<div v-else class="flex items-center gap-2 p-2 rounded-lg bg-[var(--color-hover)] min-w-40 max-w-48"> <div v-else class="flex items-center gap-2 p-2 rounded-lg bg-[var(--color-hover)] min-w-40 max-w-48">
<span :class="[fileIcon, 'text-6 text-[var(--color-accent)]']"></span> <span :class="[fileIcon, 'text-6 text-[var(--color-accent)]']"></span>
@@ -68,8 +76,20 @@ function openImageViewer() {
class="absolute inset-0 bg-black/50 rounded-lg flex items-center justify-center"> class="absolute inset-0 bg-black/50 rounded-lg flex items-center justify-center">
<div class="text-center"> <div class="text-center">
<span class="i-svg-spinners-90-ring-with-bg text-6 text-white block mb-1"></span> <span class="i-svg-spinners-90-ring-with-bg text-6 text-white block mb-1"></span>
<span class="text-xs text-white">{{ file.progress || 0 }}%</span> <span class="text-xs text-white">
{{ file.retryCount ? `Retry ${file.retryCount}/3` : `${file.progress || 0}%` }}
</span>
</div> </div>
</div> </div>
<div v-else-if="file.status === 'error'"
class="absolute inset-0 bg-black/60 rounded-lg flex flex-col items-center justify-center gap-1">
<span class="i-mynaui-danger-triangle text-6 text-red-400"></span>
<span class="text-xs text-red-300">Upload failed</span>
<button @click.stop="emit('retry')"
class="mt-1 flex items-center gap-1 px-2 py-0.5 rounded-md bg-white/10 hover:bg-white/20 text-xs text-white transition-colors">
<span class="i-mynaui-refresh text-3"></span>
Retry
</button>
</div>
</div> </div>
</template> </template>
+8 -2
View File
@@ -8,11 +8,13 @@ const props = defineProps<{
mimeType: string; mimeType: string;
status: 'uploading' | 'uploaded' | 'error'; status: 'uploading' | 'uploaded' | 'error';
url: string; url: string;
retryCount?: number;
} }
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
retry: [];
}>(); }>();
const handleDelete = async () => { const handleDelete = async () => {
@@ -21,12 +23,16 @@ const handleDelete = async () => {
</script> </script>
<template> <template>
<div class="relative h-full w-fit flex"> <div class="relative h-full w-fit flex flex-shrink-0">
<Display :file="props.file" /> <Display :file="props.file" @retry="emit('retry')" />
<button @click="handleDelete" <button @click="handleDelete"
class="absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 flex items-center justify-center w-4 h-4 rounded-full bg-[var(--bg-base)] border border-[var(--color-border)] text-xs text-[#ff3b3b]"> class="absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 flex items-center justify-center w-4 h-4 rounded-full bg-[var(--bg-base)] border border-[var(--color-border)] text-xs text-[#ff3b3b]">
<span class="i-mynaui-x text-3"></span> <span class="i-mynaui-x text-3"></span>
</button> </button>
<div v-if="file.status === 'error'"
class="absolute bottom-0 right-0 translate-x-1/4 translate-y-1/4 flex items-center justify-center w-4 h-4 rounded-full bg-red-500">
<span class="i-mynaui-danger-triangle text-2.5 text-white"></span>
</div>
</div> </div>
</template> </template>
+244 -80
View File
@@ -1,14 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import type { BaseMessage } from '~/composables/useChat'; import type { BaseMessage } from '~/composables/useChat';
import { onMounted, ref, watch, onUnmounted, nextTick, type Ref } from 'vue'; import { computed, onMounted, ref, watch, onUnmounted, nextTick, type Ref } from 'vue';
import type { ModelWithProvider, ProviderWithModels } from '~/composables/useModels'; import type { ModelWithProvider, ProviderWithModels } from '~/composables/useModels';
import type { Agent } from '~/composables/useAgents'; import type { Agent } from '~/composables/useAgents';
import type FileSelector from './FileSelector.vue'; import type FileSelector from './FileSelector.vue';
import { getComposerHeight } from '~/utils/composer-size';
const { allModels } = await useModels(); const { allModels } = await useModels();
const { updateAgent, patchAgentLocally } = await useAgents(); const { updateAgent, patchAgentLocally } = await useAgents();
const inputHeight: Ref<string> = ref('auto'); const isDrawerOpen = useCookie<boolean>("chat:tools-drawer", { default: () => false });
watch(isDrawerOpen, () => {
emit("resize");
});
const inputHeight: Ref<string> = ref('48px');
const fileSelectorRef = ref<InstanceType<typeof FileSelector> | null>(null); const fileSelectorRef = ref<InstanceType<typeof FileSelector> | null>(null);
const inputRef = ref<HTMLTextAreaElement | null>(null); const inputRef = ref<HTMLTextAreaElement | null>(null);
let tempInput = ''; let tempInput = '';
@@ -19,6 +25,7 @@ const files = ref<{
status: 'uploading' | 'uploaded' | 'error'; status: 'uploading' | 'uploaded' | 'error';
url: string; url: string;
progress: number; progress: number;
retryCount: number;
}[]>([]); }[]>([]);
const inputValue = defineModel<BaseMessage>({ required: false, default: { content: '', fileIds: [] } }); const inputValue = defineModel<BaseMessage>({ required: false, default: { content: '', fileIds: [] } });
const textAreaValue = ref(''); const textAreaValue = ref('');
@@ -31,45 +38,72 @@ watch(files, (newFiles) => {
const emit = defineEmits<{ const emit = defineEmits<{
submit: [value: BaseMessage, model: ModelWithProvider | null]; submit: [value: BaseMessage, model: ModelWithProvider | null];
addMessage: [value: BaseMessage, role: 'user' | 'assistant', model: ModelWithProvider | null];
cancel: []; cancel: [];
resize: []; resize: [];
}>(); }>();
const props = defineProps<{ const props = defineProps<{
loading?: boolean; loading?: boolean;
allowManualRole?: boolean;
agent: Readonly<Agent> | null; agent: Readonly<Agent> | null;
providers?: ProviderWithModels[]; providers?: ProviderWithModels[];
}>(); }>();
const searchConfig = ref({ const searchConfig = ref({
enabled: props.agent?.config?.search?.enabled ?? false, enabled: props.agent?.config?.search?.enabled ?? false,
maxResults: props.agent?.config?.search?.maxResults ?? 10, });
rerank: props.agent?.config?.search?.rerank ?? false,
const toolsConfig = ref({
python: props.agent?.config?.tools?.python ?? false,
}); });
watch(() => props.agent?.config?.search, (val) => { watch(() => props.agent?.config?.search, (val) => {
searchConfig.value = { searchConfig.value = {
enabled: val?.enabled ?? false, enabled: val?.enabled ?? false,
maxResults: val?.maxResults ?? 10,
rerank: val?.rerank ?? false,
}; };
}, { deep: true }); }, { deep: true });
const saveSearchConfig = async () => { watch(() => props.agent?.config?.tools, (val) => {
toolsConfig.value = {
python: val?.python ?? false,
};
}, { deep: true });
const saveAgentConfig = async (partial: {
search?: typeof searchConfig.value;
tools?: typeof toolsConfig.value;
}) => {
if (!props.agent) return; if (!props.agent) return;
const currentConfig = props.agent.config ?? {}; const currentConfig = props.agent.config ?? {};
const newConfig = { const newConfig = {
...currentConfig, ...currentConfig,
search: { ...(partial.search !== undefined ? {
enabled: searchConfig.value.enabled, search: {
maxResults: searchConfig.value.maxResults, enabled: partial.search.enabled,
rerank: searchConfig.value.rerank, },
}, } : {}),
...(partial.tools !== undefined ? {
tools: {
python: partial.tools.python,
},
} : {}),
}; };
patchAgentLocally(props.agent.id, { config: newConfig }); patchAgentLocally(props.agent.id, { config: newConfig });
updateAgent(props.agent.id, { config: newConfig }); updateAgent(props.agent.id, { config: newConfig });
}; };
const saveSearchConfig = async () => {
await saveAgentConfig({ search: searchConfig.value });
};
const saveToolsConfig = async () => {
await saveAgentConfig({ tools: toolsConfig.value });
};
const isUploading = computed(() => files.value.some((f) => f.status === 'uploading'));
const hasFailedUploads = computed(() => files.value.some((f) => f.status === 'error'));
const selectedModel = ref<ModelWithProvider | null>(null); const selectedModel = ref<ModelWithProvider | null>(null);
const handlePaste = async (event: ClipboardEvent) => { const handlePaste = async (event: ClipboardEvent) => {
@@ -109,11 +143,18 @@ const initializeModel = () => {
const updateAgentDefaultModel = async (modelId: string) => { const updateAgentDefaultModel = async (modelId: string) => {
if (!props.agent) return; if (!props.agent) return;
try { try {
const currentModelId = props.agent.defaultModelId;
await $fetch(`/api/agent/${props.agent.id}`, { await $fetch(`/api/agent/${props.agent.id}`, {
method: 'PATCH', method: 'PATCH',
body: { body: {
defaultModelId: modelId, defaultModelId: modelId,
}, },
onRequest: () => {
patchAgentLocally(props.agent!.id, { defaultModelId: modelId });
},
onResponseError: () => {
patchAgentLocally(props.agent!.id, { defaultModelId: currentModelId });
},
}); });
} catch (error) { } catch (error) {
console.error('Failed to update agent default model:', error); console.error('Failed to update agent default model:', error);
@@ -138,13 +179,45 @@ watch(() => props.agent?.defaultModelId, (newModelId) => {
}) })
const handleSubmit = () => { const handleSubmit = () => {
if (isUploading.value || hasFailedUploads.value) {
return;
}
if (props.loading) { if (props.loading) {
emit('cancel'); emit('cancel');
return; return;
} }
if (inputValue.value.content.trim() || inputValue.value.fileIds.length > 0) { if (inputValue.value.content.trim() || inputValue.value.fileIds.length > 0) {
emit('submit', structuredClone(toRaw(inputValue.value)), selectedModel.value); const message: BaseMessage = {
content: inputValue.value.content,
fileIds: inputValue.value.fileIds,
files: files.value.map(f => ({
id: f.id,
name: f.name,
mimeType: f.mimeType,
url: f.url,
})),
};
emit('submit', message, selectedModel.value);
files.value = [];
textAreaValue.value = '';
}
};
const handleAddMessage = (role: 'user' | 'assistant') => {
if (inputValue.value.content.trim() || inputValue.value.fileIds.length > 0) {
const message: BaseMessage = {
content: inputValue.value.content,
fileIds: inputValue.value.fileIds,
files: files.value.map(f => ({
id: f.id,
name: f.name,
mimeType: f.mimeType,
url: f.url,
})),
};
emit('addMessage', message, role, selectedModel.value);
files.value = []; files.value = [];
textAreaValue.value = ''; textAreaValue.value = '';
} }
@@ -236,47 +309,57 @@ const handleWindowKeyDown = async (event: KeyboardEvent) => {
}; };
const mirrorRef = ref<HTMLDivElement | null>(null); const mirrorRef = ref<HTMLDivElement | null>(null);
const MIRROR_PROPERTIES = [
'box-sizing', 'font-family', 'font-size', 'font-weight', 'font-style',
'line-height', 'letter-spacing', 'word-spacing', 'text-align', 'text-indent',
'text-transform', 'tab-size', 'padding-top', 'padding-bottom', 'padding-left',
'padding-right', 'border-top-width', 'border-bottom-width', 'border-left-width',
'border-right-width', 'border-style', 'white-space', 'overflow-wrap', 'word-break',
] as const;
let sizingRaf: number | undefined;
let sizingObserver: ResizeObserver | undefined;
let typographyObserver: MutationObserver | undefined;
let sizingDirty = true;
let mirrorWidth = '';
let lineHeight = 24;
let padding = 0;
let borders = 0;
let borderBox = true;
const resizeTextArea = () => { const resizeTextArea = () => {
const textarea = inputRef.value; if (import.meta.server || sizingRaf !== undefined) return;
const mirror = mirrorRef.value; sizingRaf = requestAnimationFrame(() => {
if (!textarea || !mirror) return; sizingRaf = undefined;
const textarea = inputRef.value;
const mirror = mirrorRef.value;
if (!textarea || !mirror) return;
emit('resize'); if (sizingDirty) {
nextTick().then(() => { // Finish style reads before changing the isolated measurement box.
mirror.textContent = (textarea.value || '\u200b') + '\n'; const styles = getComputedStyle(textarea);
const values = MIRROR_PROPERTIES.map(property => styles.getPropertyValue(property));
mirrorWidth = styles.width;
lineHeight = Number.parseFloat(styles.lineHeight) || Number.parseFloat(styles.fontSize) * 1.2;
padding = Number.parseFloat(styles.paddingTop) + Number.parseFloat(styles.paddingBottom);
borders = Number.parseFloat(styles.borderTopWidth) + Number.parseFloat(styles.borderBottomWidth);
borderBox = styles.boxSizing === 'border-box';
MIRROR_PROPERTIES.forEach((property, index) => mirror.style.setProperty(property, values[index]!));
mirror.style.width = mirrorWidth;
sizingDirty = false;
}
const textareaStyles = getComputedStyle(textarea); // Preserve the final empty line without adding an extra line to ordinary text.
mirror.style.width = textareaStyles.width; mirror.textContent = textarea.value + '\u200b';
mirror.style.fontFamily = textareaStyles.fontFamily; const height = `${getComposerHeight(mirror.scrollHeight, lineHeight, padding, borders, borderBox)}px`;
mirror.style.fontSize = textareaStyles.fontSize; if (height === inputHeight.value) return;
mirror.style.lineHeight = textareaStyles.lineHeight;
mirror.style.letterSpacing = textareaStyles.letterSpacing;
mirror.style.wordSpacing = textareaStyles.wordSpacing;
mirror.style.textAlign = textareaStyles.textAlign;
mirror.style.paddingTop = textareaStyles.paddingTop;
mirror.style.paddingBottom = textareaStyles.paddingBottom;
mirror.style.paddingLeft = textareaStyles.paddingLeft;
mirror.style.paddingRight = textareaStyles.paddingRight;
mirror.style.borderTopWidth = textareaStyles.borderTopWidth;
mirror.style.borderBottomWidth = textareaStyles.borderBottomWidth;
mirror.style.borderLeftWidth = textareaStyles.borderLeftWidth;
mirror.style.borderRightWidth = textareaStyles.borderRightWidth;
mirror.style.whiteSpace = 'pre-wrap';
mirror.style.wordWrap = 'break-word';
mirror.style.overflow = 'hidden';
const lineHeight = 24; // Capture bottom-follow intent before applying the new height.
const minLines = 2; emit('resize');
const maxLines = 10; inputHeight.value = height;
const minHeight = minLines * lineHeight;
const maxHeight = maxLines * lineHeight;
const height = Math.min(Math.max(mirror.scrollHeight, minHeight), maxHeight);
inputHeight.value = `${height}px`;
}); });
}; };
watch(textAreaValue, resizeTextArea, { immediate: true }); watch(textAreaValue, resizeTextArea, { flush: 'post' });
let hasCommandKey = false; let hasCommandKey = false;
if (import.meta.server) { if (import.meta.server) {
@@ -290,42 +373,53 @@ onBeforeMount(() => {
tempInput = (document.getElementById('chat') as HTMLInputElement)?.value ?? ''; tempInput = (document.getElementById('chat') as HTMLInputElement)?.value ?? '';
}); });
let resizeRafId: number | undefined;
const handleWindowResize = () => { const handleWindowResize = () => {
if (resizeRafId) return; sizingDirty = true;
resizeRafId = requestAnimationFrame(() => { resizeTextArea();
resizeRafId = undefined; };
resizeTextArea();
});
}
onMounted(() => { onMounted(() => {
textAreaValue.value = tempInput; textAreaValue.value = tempInput;
document.addEventListener('keydown', handleWindowKeyDown); document.addEventListener('keydown', handleWindowKeyDown);
window.addEventListener('resize', handleWindowResize); window.addEventListener('resize', handleWindowResize);
inputRef.value?.addEventListener('paste', handlePaste); inputRef.value?.addEventListener('paste', handlePaste);
let observedWidth: number | undefined;
sizingObserver = new ResizeObserver(([entry]) => {
if (!entry || entry.contentRect.width === observedWidth) return;
observedWidth = entry.contentRect.width;
handleWindowResize();
});
if (inputRef.value) sizingObserver.observe(inputRef.value);
typographyObserver = new MutationObserver(handleWindowResize);
typographyObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style'] });
document.fonts.addEventListener('loadingdone', handleWindowResize);
handleWindowResize();
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('keydown', handleWindowKeyDown); document.removeEventListener('keydown', handleWindowKeyDown);
window.removeEventListener('resize', handleWindowResize); window.removeEventListener('resize', handleWindowResize);
if (resizeRafId !== undefined) cancelAnimationFrame(resizeRafId); if (sizingRaf !== undefined) cancelAnimationFrame(sizingRaf);
sizingObserver?.disconnect();
typographyObserver?.disconnect();
document.fonts.removeEventListener('loadingdone', handleWindowResize);
inputRef.value?.removeEventListener('paste', handlePaste); inputRef.value?.removeEventListener('paste', handlePaste);
}); });
</script> </script>
<template> <template>
<div :class="['w-full flex max-h-full', $attrs.class]"> <div :class="['w-full flex flex-col max-h-full', $attrs.class]">
<div ref="mirrorRef" aria-hidden="true" <div ref="mirrorRef" data-chat-measurement aria-hidden="true"
class="absolute top-0 left-0 pointer-events-none invisible overflow-hidden h-0"></div> class="fixed top-0 left-0 pointer-events-none invisible overflow-hidden h-0 [contain:layout_style_paint]"></div>
<div class="relative w-full flex flex-shrink-1 flex-col gap-3 p-2 rounded-2xl border transition-border ease-in-out duration-300 bg-[var(--bg-container)] <div class="relative z-10 w-full flex flex-shrink-1 flex-col gap-3 p-2 rounded-2xl border transition-border ease-in-out duration-300 bg-[var(--bg-container)]
border-[var(--color-border)] focus-within:border-[var(--color-border-active)]"> border-[var(--color-border)] focus-within:border-[var(--color-border-active)]">
<div v-if="files.length > 0" class="flex-1 flex gap-2 pt-2 px-2 pb-1 overflow-x-auto flex-wrap"> <div v-if="files.length > 0"
class="flex-shrink-0 flex flex-nowrap gap-2 pt-2 px-2 pb-1 overflow-x-auto overflow-y-hidden [scrollbar-width:thin]">
<!-- TODO: show attachment previews --> <!-- TODO: show attachment previews -->
<AttachmentPreview v-for="file in files" @delete="files = files.filter((f) => f.id !== file.id)" <AttachmentPreview v-for="file in files" @delete="files = files.filter((f) => f.id !== file.id)"
:key="file.id" :file="file" /> @retry="fileSelectorRef?.retryFile(file.id)" :key="file.id" :file="file"></AttachmentPreview>
</div> </div>
<div class="flex-1 min-w-0 max-h-full"> <div class="min-w-0 max-h-full">
<!-- Grammarly literally breaks everything, go fuck yourself --> <!-- Grammarly literally breaks everything, go fuck yourself -->
<!-- It is absolutely paramount that the closing tag for the textare has ZERO whitespace between the end of the textarea opening tag, otherwise there will be hydration errors --> <!-- It is absolutely paramount that the closing tag for the textare has ZERO whitespace between the end of the textarea opening tag, otherwise there will be hydration errors -->
<textarea data-gramm="false" id="chat" v-model="textAreaValue" ref="inputRef" <textarea data-gramm="false" id="chat" v-model="textAreaValue" ref="inputRef"
@@ -336,27 +430,97 @@ onUnmounted(() => {
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<!-- TODO: since we dont want to model selector dropdown to potentially overflow, it has max-width: 100%, so, we need to maake the trigger large enough to fit the entire width of the dropdown --> <!-- TODO: since we dont want to model selector dropdown to potentially overflow, it has max-width: 100%, so, we need to maake the trigger large enough to fit the entire width of the dropdown -->
<div class="flex flex-1 gap-1 min-w-0"> <div class="flex flex-1 items-center gap-1 min-w-0">
<button v-if="selectedModel?.capabilities.includes('tools')"
type="button"
@click="isDrawerOpen = !isDrawerOpen"
class="sm:hidden relative flex items-center justify-center h-8.5 w-8.5 shrink-0 @hover:bg-[var(--color-hover)] rounded-lg transition-colors duration-200"
:aria-label="isDrawerOpen ? 'Close tools drawer' : 'Open tools drawer'"
:aria-expanded="isDrawerOpen">
<span class="i-mynaui-chevron-down text-5 text-[var(--text-secondary)] transition-transform duration-200"
:class="{ 'rotate-180': isDrawerOpen }"></span>
<span v-if="!isDrawerOpen && (searchConfig.enabled || toolsConfig.python)"
class="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-[var(--color-accent)]"></span>
</button>
<ModelSelector v-if="providers !== undefined" :add-hotkey="true" v-model="selectedModel" <ModelSelector v-if="providers !== undefined" :add-hotkey="true" v-model="selectedModel"
:providers="providers" /> :providers="providers"></ModelSelector>
<SearchSelector v-if="selectedModel?.capabilities.includes('tools')" :enabled="searchConfig.enabled" <div v-if="selectedModel?.capabilities.includes('tools')" class="hidden sm:flex items-center gap-1">
:max-results="searchConfig.maxResults" :rerank="searchConfig.rerank" <SearchSelector :enabled="searchConfig.enabled"
@update:enabled="(v: boolean) => { searchConfig.enabled = v; saveSearchConfig() }" @update:enabled="(v: boolean) => { searchConfig.enabled = v; saveSearchConfig(); }"></SearchSelector>
@update:max-results="(v: number) => { searchConfig.maxResults = v; saveSearchConfig() }" <ToolSelector :python="toolsConfig.python"
@update:rerank="(v: boolean) => { searchConfig.rerank = v; saveSearchConfig() }" /> @update:python="(v: boolean) => { toolsConfig.python = v; saveToolsConfig(); }"></ToolSelector>
<FileSelector ref="fileSelectorRef" :selected-model="selectedModel" v-model="files" /> </div>
</div> </div>
<button aria-label="Send message" @click="handleSubmit" <div class="flex items-center gap-1 shrink-0">
:disabled="(!inputValue.content.trim() && files.length === 0) && !loading" :class="[ <FileSelector ref="fileSelectorRef" :selected-model="selectedModel" v-model="files"></FileSelector>
'h-8 w-8 rounded-xl transition-all duration-200 flex items-center justify-center disabled:cursor-not-allowed disabled:bg-transparent', <div v-if="allowManualRole" class="flex items-center">
(inputValue.content.trim() || files.length > 0) && !loading <button aria-label="Send message" @click="handleSubmit"
? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] @hover:bg-[var(--color-accent-hover)]' :disabled="((!inputValue.content.trim() && files.length === 0) && !loading) || isUploading || hasFailedUploads"
: 'text-[var(--text-dim)]', :class="[
loading && 'bg-[var(--color-hover)] @hover:bg-[var(--color-active)]', 'h-8 w-8 rounded-l-lg rounded-r-none transition-all duration-200 flex items-center justify-center disabled:cursor-not-allowed disabled:bg-transparent',
]"> (inputValue.content.trim() || files.length > 0) && !loading && !isUploading && !hasFailedUploads
<span v-if="loading" class="text-6.5 i-mynaui-stop-solid"></span> ? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] @hover:bg-[var(--color-accent-hover)]'
<span v-else class="text-5 i-mynaui-send-solid"></span> : 'text-[var(--text-dim)]',
</button> loading && 'bg-[var(--color-hover)] @hover:bg-[var(--color-active)]',
]">
<span v-if="loading" class="text-6.5 i-mynaui-stop-solid"></span>
<span v-else class="text-5 i-mynaui-send-solid"></span>
</button>
<Dropdown placement="bottom-end">
<template #default="{ setRef, isOpen, toggle }">
<button :ref="setRef" aria-label="Message options" @click="toggle"
:disabled="(!inputValue.content.trim() && files.length === 0) || loading || isUploading || hasFailedUploads"
:class="[
'h-8 w-6 rounded-r-lg rounded-l-none transition-all duration-200 flex items-center justify-center disabled:cursor-not-allowed border-l border-[var(--text-dim)] disabled:border-transparent',
(inputValue.content.trim() || files.length > 0) && !loading && !isUploading && !hasFailedUploads
? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] @hover:bg-[var(--color-accent-hover)] disabled:bg-transparent'
: 'text-[var(--text-dim)]',
loading && 'bg-[var(--color-hover)] @hover:bg-[var(--color-active)]',
]">
<span class="text-4.5 i-mynaui-chevron-down" :class="{ 'rotate-180': isOpen }"></span>
</button>
</template>
<template #dropdown="{ close }">
<button
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm text-[var(--text-primary)] @hover:bg-[var(--color-hover)] transition-colors whitespace-nowrap"
@click="handleAddMessage('user'); close()">
<span class="text-4 i-mynaui-user"></span>
Add as User
</button>
<button
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm text-[var(--text-primary)] @hover:bg-[var(--color-hover)] transition-colors whitespace-nowrap"
@click="handleAddMessage('assistant'); close()">
<span class="text-4 i-mynaui-sparkles"></span>
Add as Assistant
</button>
</template>
</Dropdown>
</div>
<button v-else aria-label="Send message" @click="handleSubmit"
:disabled="((!inputValue.content.trim() && files.length === 0) && !loading) || isUploading || hasFailedUploads"
:class="[
'h-8 w-8 rounded-xl transition-all duration-200 flex items-center justify-center disabled:cursor-not-allowed disabled:bg-transparent',
(inputValue.content.trim() || files.length > 0) && !loading && !isUploading && !hasFailedUploads
? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] @hover:bg-[var(--color-accent-hover)]'
: 'text-[var(--text-dim)]',
loading && 'bg-[var(--color-hover)] @hover:bg-[var(--color-active)]',
]">
<span v-if="loading" class="text-6.5 i-mynaui-stop-solid"></span>
<span v-else class="text-5 i-mynaui-send-solid"></span>
</button>
</div>
</div>
</div>
<div v-if="selectedModel?.capabilities.includes('tools')"
class="sm:hidden relative z-0 mx-auto w-[calc(100%-4px)] transition-[height] duration-250 ease-[cubic-bezier(0.4,_0,_0.2,_1)]"
:class="isDrawerOpen ? 'h-[44px]' : 'h-0'">
<div class="absolute -top-4 left-0 right-0 pt-5 px-3 pb-1.5 rounded-b-2xl border-x border-b border-[var(--color-border)] bg-[var(--bg-surface)] flex items-center gap-1.5 transition-[transform,opacity] duration-250 ease-[cubic-bezier(0.4,_0,_0.2,_1)]"
:class="isDrawerOpen ? 'translate-y-0 opacity-100' : '-translate-y-[44px] opacity-0 pointer-events-none'">
<SearchSelector :enabled="searchConfig.enabled"
@update:enabled="(v: boolean) => { searchConfig.enabled = v; saveSearchConfig(); }"></SearchSelector>
<ToolSelector :python="toolsConfig.python"
@update:python="(v: boolean) => { toolsConfig.python = v; saveToolsConfig(); }"></ToolSelector>
</div> </div>
</div> </div>
</div> </div>
+115 -50
View File
@@ -1,6 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
const MAX_RETRIES = 3;
const RETRY_BASE_DELAY_MS = 1000;
const props = defineProps<{ const props = defineProps<{
selectedModel?: ModelWithProvider | null; selectedModel?: ModelWithProvider | null;
}>(); }>();
@@ -15,10 +18,12 @@ const files = defineModel<{
status: 'uploading' | 'uploaded' | 'error'; status: 'uploading' | 'uploaded' | 'error';
url: string; url: string;
progress: number; progress: number;
retryCount: number;
}[]>({ required: false, default: [] }); }[]>({ required: false, default: [] });
const rawFiles = ref<File[]>([]); const rawFiles = ref<File[]>([]);
const activeUploads = ref<Map<string, XMLHttpRequest>>(new Map()); const activeUploads = ref<Map<string, XMLHttpRequest>>(new Map());
const pendingFiles = ref<Map<string, File>>(new Map());
const uploadWithProgress = (file: File, url: string, id: string) => { const uploadWithProgress = (file: File, url: string, id: string) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -39,12 +44,16 @@ const uploadWithProgress = (file: File, url: string, id: string) => {
xhr.onload = () => { xhr.onload = () => {
activeUploads.value.delete(id); activeUploads.value.delete(id);
resolve(xhr); if (xhr.status >= 400) {
reject(new Error(`Upload failed with status ${xhr.status}`));
} else {
resolve(xhr);
}
}; };
xhr.onerror = () => { xhr.onerror = () => {
activeUploads.value.delete(id); activeUploads.value.delete(id);
reject(xhr); reject(new Error('Network error during upload'));
}; };
xhr.onabort = () => { xhr.onabort = () => {
@@ -57,68 +66,122 @@ const uploadWithProgress = (file: File, url: string, id: string) => {
}); });
}; };
const uploadFile = async (file: File) => { const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
try {
const id = nanoid();
const fileName = file.name || `${id}.png`;
const fileType = file.type || 'application/octet-stream';
files.value.push({ const uploadFileAttempt = async (file: File, id: string, fileName: string, fileType: string) => {
const { url: uploadUrl, assetUrl } = await $fetch('/api/upload/presigned', {
method: 'POST',
body: {
file: {
name: fileName,
mimeType: fileType,
}
},
}) as { url: string; assetUrl: string };
await uploadWithProgress(file, uploadUrl, id);
await $fetch('/api/file', {
method: 'POST',
body: {
id, id,
name: fileName, name: fileName,
mimeType: fileType, mimeType: fileType,
status: 'uploading', size: file.size,
url: URL.createObjectURL(file), url: assetUrl,
progress: 0 },
}); });
};
const { url: uploadUrl, assetUrl } = await $fetch('/api/upload/presigned', { const uploadFile = async (file: File) => {
method: 'POST', const id = nanoid();
body: { const fileName = file.name || `${id}.png`;
file: { const fileType = file.type || 'application/octet-stream';
name: fileName,
mimeType: fileType,
}
},
}) as { url: string; assetUrl: string };
await uploadWithProgress(file, uploadUrl, id); files.value.push({
id,
name: fileName,
mimeType: fileType,
status: 'uploading',
url: URL.createObjectURL(file),
progress: 0,
retryCount: 0,
});
pendingFiles.value.set(id, file);
await $fetch('/api/file', { await attemptUpload(file, id, fileName, fileType);
method: 'POST', };
body: {
id, const attemptUpload = async (file: File, id: string, fileName: string, fileType: string, attempt = 0) => {
name: fileName, try {
mimeType: fileType, await uploadFileAttempt(file, id, fileName, fileType);
size: file.size,
url: assetUrl,
},
})
files.value = files.value.map(f => { files.value = files.value.map(f => {
if (f.name === file.name) { if (f.id === id) {
return { return { ...f, status: 'uploaded' as const, progress: 100 };
...f,
status: 'uploaded',
progress: 100
};
} }
return f; return f;
}); });
pendingFiles.value.delete(id);
} catch (error) { } catch (error) {
files.value = files.value.map(f => { if (error instanceof Error && error.message === 'Upload aborted') {
if (f.name === file.name) { pendingFiles.value.delete(id);
return { return;
...f, }
status: 'error'
};
}
return f;
});
}
}
defineExpose({ uploadFile }); const nextAttempt = attempt + 1;
if (nextAttempt < MAX_RETRIES) {
files.value = files.value.map(f => {
if (f.id === id) {
return { ...f, retryCount: nextAttempt, progress: 0 };
}
return f;
});
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
await sleep(delay);
const stillExists = files.value.some(f => f.id === id);
if (!stillExists) return;
files.value = files.value.map(f => {
if (f.id === id) {
return { ...f, status: 'uploading' as const };
}
return f;
});
await attemptUpload(file, id, fileName, fileType, nextAttempt);
} else {
files.value = files.value.map(f => {
if (f.id === id) {
return { ...f, status: 'error' as const };
}
return f;
});
}
}
};
const retryFile = async (id: string) => {
const file = pendingFiles.value.get(id);
if (!file) return;
const entry = files.value.find(f => f.id === id);
if (!entry) return;
files.value = files.value.map(f => {
if (f.id === id) {
return { ...f, status: 'uploading' as const, progress: 0, retryCount: 0 };
}
return f;
});
await attemptUpload(file, id, entry.name, entry.mimeType);
};
defineExpose({ uploadFile, retryFile });
watch(rawFiles, async (newFiles, oldFiles) => { watch(rawFiles, async (newFiles, oldFiles) => {
const diff = newFiles.filter(f => const diff = newFiles.filter(f =>
@@ -146,6 +209,8 @@ watch(files, async (newFiles, oldFiles) => {
activeUploads.value.delete(removedFile.id); activeUploads.value.delete(removedFile.id);
} }
pendingFiles.value.delete(removedFile.id);
if (removedFile.url.startsWith('blob:')) { if (removedFile.url.startsWith('blob:')) {
URL.revokeObjectURL(removedFile.url); URL.revokeObjectURL(removedFile.url);
} }
+29
View File
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { useId } from "vue";
withDefaults(defineProps<{
size?: string | number;
color?: boolean;
avatar?: boolean;
}>(), {
size: 24,
});
const titleId = useId();
const BACKGROUND_COLOR = "#a3e6d9";
const AVATAR_SCALE = 0.7;
</script>
<template>
<div class="inline-flex items-center justify-center"
:style="{ width: `${size}px`, height: `${size}px`, backgroundColor: avatar ? BACKGROUND_COLOR : undefined, borderRadius: avatar ? '0.375rem' : undefined }">
<!-- Artwork from https://github.com/lobehub/lobe-icons/pull/396 (MIT; see LICENSE). -->
<svg xmlns="http://www.w3.org/2000/svg" :width="size" :height="size"
viewBox="0 0 24 24" :fill="avatar ? '#000' : 'currentColor'" fill-rule="evenodd"
role="img" :aria-labelledby="titleId" style="flex: none; line-height: 1;"
:style="{ transform: avatar ? `scale(${AVATAR_SCALE})` : undefined }">
<title :id="titleId">Dots Studio</title>
<path d="M8.429 23.18a67 67 0 0 1-.423-2.074c0-.024 2.298-.042 5.106-.042 4.836 0 5.104.005 5.077.098a19 19 0 0 0-.148.714c-.066.34-.158.787-.204.996l-.084.378h-4.651c-3.671 0-4.656-.015-4.673-.07m1.89-6.37-6.351-1.1-.054-.195c-.05-.177-.025-.247.286-.836 1.975-3.727 5.2-7.438 8.824-10.153 1.898-1.422 4.002-2.647 6.013-3.502.733-.311.744-.314.897-.218l.17.106c.01.005-.728 3.81-1.64 8.457-.91 4.645-1.655 8.47-1.655 8.5s-.032.052-.07.048-2.928-.502-6.42-1.107m4.799-1.675c.293-1.376 2.09-10.679 2.067-10.701-.017-.017-.354.172-.75.418-2.186 1.362-3.772 2.637-5.599 4.501a27.4 27.4 0 0 0-3.302 4.03c-.367.545-.419.648-.331.668.25.056 7.628 1.337 7.737 1.343.101.006.13-.037.178-.26z"></path>
</svg>
</div>
</template>
+31
View File
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { useId } from "vue";
withDefaults(defineProps<{
size?: string | number;
color?: boolean;
avatar?: boolean;
}>(), {
size: 24,
});
const titleId = useId();
const BACKGROUND_COLOR = "#fff";
const AVATAR_SCALE = 0.75;
</script>
<template>
<div class="inline-flex items-center justify-center"
:style="{ width: `${size}px`, height: `${size}px`, backgroundColor: avatar ? BACKGROUND_COLOR : undefined, borderRadius: avatar ? '0.375rem' : undefined }">
<svg xmlns="http://www.w3.org/2000/svg" :width="size" :height="size"
viewBox="0 0 24 24" :fill="avatar || color ? '#000' : 'currentColor'" role="img" :aria-labelledby="titleId"
style="flex: none; line-height: 1;"
:style="{ transform: avatar ? `scale(${AVATAR_SCALE})` : undefined }">
<title :id="titleId">Nex AGI</title>
<g transform="matrix(.02777778 0 0 .02777778 -2.222222 -2.222222)">
<path d="M634.153 529.915C634.153 536.056 631.023 541.773 625.849 545.081C620.674 548.388 614.17 548.83 608.597 546.251L367.982 434.957L367.983 749.839C367.983 755.934 364.899 761.615 359.787 764.935L137.804 909.096C132.269 912.69 125.211 912.968 119.411 909.819C113.611 906.67 110 900.599 110 894V156.359C110 150.25 113.099 144.558 118.229 141.242C123.361 137.926 129.823 137.438 135.394 139.948L623.547 359.872C630.002 362.78 634.153 369.203 634.153 376.284V529.915Z"></path>
<path d="M389.847 493.085C389.847 486.944 392.977 481.227 398.151 477.919C403.326 474.612 409.83 474.17 415.403 476.749L656.018 588.043L656.017 273.161C656.017 267.066 659.101 261.385 664.213 258.065L886.196 113.904C891.731 110.31 898.789 110.032 904.589 113.181C910.389 116.33 914 122.401 914 129L914 866.641C914 872.75 910.901 878.442 905.771 881.758C900.639 885.074 894.177 885.562 888.606 883.052L400.453 663.128C393.998 660.22 389.847 653.797 389.847 646.716V493.085Z"></path>
</g>
</svg>
</div>
</template>
+3 -4
View File
@@ -16,17 +16,16 @@ const [a, b, c, d] = useFillIds(TITLE, 4);
<template> <template>
<div class="inline-flex items-center justify-center" <div class="inline-flex items-center justify-center"
:style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']"> :style="[`width: ${size}px; height: ${size}px;`, avatar ? `background-color: ${BACKGROUND_COLOR}; border-radius: 0.375rem;` : '']">
<!-- TODO: scale to 24px -->
<svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']" <svg :style="[`width: ${size}px; height: ${size}px; flex: none; line-height: 1;`, avatar ? `transform: scale(${AVATAR_SCALE});` : '']"
style="flex: none; line-height: 1;" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> style="flex: none; line-height: 1;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>{{ TITLE }}</title> <title>{{ TITLE }}</title>
<mask :id="d!.id" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="32"> <mask :id="d!.id" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="32">
<rect width="32" height="32" :fill="a!.fill" style="" /> <rect width="32" height="32" :fill="a!.fill" style="" />
<rect width="32" height="32" :fill="b!.fill" style="" /> <rect width="32" height="32" :fill="b!.fill" style="" />
<rect width="32" height="32" :fill="c!.fill" style="" /> <rect width="32" height="32" :fill="c!.fill" style="" />
</mask> </mask>
<g :mask="d!.fill"> <g transform="scale(0.75)">
<path <path :mask="d!.fill"
d="M8.98976 30.3815C6.02045 28.9329 3.60852 26.6447 2.01266 23.7643C0.453682 20.9498 -0.219858 17.775 0.0629571 14.5827C0.126988 13.8674 0.757216 13.3392 1.47289 13.4024C2.18722 13.466 2.71622 14.0968 2.65309 14.8126C2.41613 17.4865 2.98128 20.1467 4.28757 22.5053C5.41219 24.536 7.023 26.2153 8.9891 27.4192L15.1043 14.8784C12.7106 14.0812 10.7169 14.4282 10.5375 14.4631C10.5112 14.4692 10.4858 14.4735 10.4599 14.4786C9.86386 14.5784 9.28735 14.2527 9.05527 13.7111C8.73499 13.1131 7.77341 11.6515 6.60959 11.0837C5.44577 10.516 3.65213 10.6991 3.07471 10.8246C2.58789 10.931 2.08122 10.7494 1.77371 10.358C1.4662 9.9665 1.40726 9.43259 1.62554 8.98496C5.48998 1.05529 15.087 -2.24965 23.0153 1.61816C30.9437 5.48598 34.2453 15.0756 30.3875 23.003C30.3836 23.0111 30.3796 23.0192 30.3752 23.0282C26.504 30.949 16.9145 34.2476 8.98976 30.3815ZM17.44 16.0179L11.3253 28.5578C17.4968 30.8631 24.5196 28.3019 27.731 22.4803C27.2796 21.7817 26.4875 20.78 25.5709 20.3328C24.3864 19.7549 22.6509 19.9442 22.0547 20.0695C21.9566 20.0928 21.8588 20.1041 21.7604 20.105C21.5842 20.1059 21.4054 20.071 21.2339 19.9962C21.056 19.9183 20.8952 19.8009 20.7654 19.6497C20.6912 19.5623 20.6299 19.4667 20.5806 19.3648C20.5422 19.2882 19.5939 17.4481 17.4391 16.0175L17.44 16.0179ZM7.74737 8.74582C8.74313 9.2316 9.5548 10.0037 10.1608 10.7321C12.0348 7.86545 14.578 5.70023 16.5425 4.3042C17.2726 3.78579 18.0284 3.30111 18.7563 2.88291C13.6429 1.80923 8.28375 3.83577 5.1764 8.10245C5.99889 8.15876 6.90528 8.33501 7.74737 8.74582ZM24.6415 5.75507C24.7602 6.58628 24.8441 7.47954 24.8847 8.37503C24.9943 10.7762 24.8555 14.1022 23.7588 17.3369C24.6735 17.3614 25.7372 17.5232 26.7105 17.998C27.5795 18.4219 28.3081 19.0633 28.8814 19.7035C30.3453 14.6184 28.6441 9.13194 24.6411 5.75597L24.6415 5.75507ZM17.4489 13.0336C19.2472 13.9109 20.5471 15.078 21.4282 16.0842C22.8614 11.427 22.2366 6.21527 21.7149 4.28506C19.873 5.06221 15.3815 7.77795 12.5957 11.7753C13.9322 11.8499 15.6515 12.1568 17.4489 13.0336Z" d="M8.98976 30.3815C6.02045 28.9329 3.60852 26.6447 2.01266 23.7643C0.453682 20.9498 -0.219858 17.775 0.0629571 14.5827C0.126988 13.8674 0.757216 13.3392 1.47289 13.4024C2.18722 13.466 2.71622 14.0968 2.65309 14.8126C2.41613 17.4865 2.98128 20.1467 4.28757 22.5053C5.41219 24.536 7.023 26.2153 8.9891 27.4192L15.1043 14.8784C12.7106 14.0812 10.7169 14.4282 10.5375 14.4631C10.5112 14.4692 10.4858 14.4735 10.4599 14.4786C9.86386 14.5784 9.28735 14.2527 9.05527 13.7111C8.73499 13.1131 7.77341 11.6515 6.60959 11.0837C5.44577 10.516 3.65213 10.6991 3.07471 10.8246C2.58789 10.931 2.08122 10.7494 1.77371 10.358C1.4662 9.9665 1.40726 9.43259 1.62554 8.98496C5.48998 1.05529 15.087 -2.24965 23.0153 1.61816C30.9437 5.48598 34.2453 15.0756 30.3875 23.003C30.3836 23.0111 30.3796 23.0192 30.3752 23.0282C26.504 30.949 16.9145 34.2476 8.98976 30.3815ZM17.44 16.0179L11.3253 28.5578C17.4968 30.8631 24.5196 28.3019 27.731 22.4803C27.2796 21.7817 26.4875 20.78 25.5709 20.3328C24.3864 19.7549 22.6509 19.9442 22.0547 20.0695C21.9566 20.0928 21.8588 20.1041 21.7604 20.105C21.5842 20.1059 21.4054 20.071 21.2339 19.9962C21.056 19.9183 20.8952 19.8009 20.7654 19.6497C20.6912 19.5623 20.6299 19.4667 20.5806 19.3648C20.5422 19.2882 19.5939 17.4481 17.4391 16.0175L17.44 16.0179ZM7.74737 8.74582C8.74313 9.2316 9.5548 10.0037 10.1608 10.7321C12.0348 7.86545 14.578 5.70023 16.5425 4.3042C17.2726 3.78579 18.0284 3.30111 18.7563 2.88291C13.6429 1.80923 8.28375 3.83577 5.1764 8.10245C5.99889 8.15876 6.90528 8.33501 7.74737 8.74582ZM24.6415 5.75507C24.7602 6.58628 24.8441 7.47954 24.8847 8.37503C24.9943 10.7762 24.8555 14.1022 23.7588 17.3369C24.6735 17.3614 25.7372 17.5232 26.7105 17.998C27.5795 18.4219 28.3081 19.0633 28.8814 19.7035C30.3453 14.6184 28.6441 9.13194 24.6411 5.75597L24.6415 5.75507ZM17.4489 13.0336C19.2472 13.9109 20.5471 15.078 21.4282 16.0842C22.8614 11.427 22.2366 6.21527 21.7149 4.28506C19.873 5.06221 15.3815 7.77795 12.5957 11.7753C13.9322 11.8499 15.6515 12.1568 17.4489 13.0336Z"
:fill="!avatar && color ? '#4137FF' : 'currentColor'" style="fill-opacity:1;" /> :fill="!avatar && color ? '#4137FF' : 'currentColor'" style="fill-opacity:1;" />
</g> </g>
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { useId } from "vue";
withDefaults(defineProps<{
size?: string | number;
color?: boolean;
avatar?: boolean;
}>(), {
size: 24,
});
const titleId = useId();
</script>
<template>
<div class="inline-flex items-center justify-center"
:style="{ width: `${size}px`, height: `${size}px`, borderRadius: avatar ? '0.375rem' : undefined, overflow: avatar ? 'hidden' : undefined }">
<svg xmlns="http://www.w3.org/2000/svg" :width="size" :height="size"
viewBox="0 0 24 24" role="img" :aria-labelledby="titleId"
style="flex: none; line-height: 1;">
<title :id="titleId">Thinking Machines Lab</title>
<rect width="24" height="24" rx="2.6666666666666665"
:fill="color ? '#e6e7e8' : 'currentColor'" :fill-opacity="color ? 1 : 0.15"></rect>
<rect x="4" y="4" width="16" height="16" rx="1.3333333333333333"
:fill="color ? '#31373d' : 'currentColor'"></rect>
</svg>
</div>
</template>
+15
View File
@@ -0,0 +1,15 @@
<script setup lang="ts">
import { renderMath } from 'comark/plugins/math';
const props = defineProps<{
content: string;
display: boolean;
}>();
const html = computed(() => renderMath(props.content, props.display, { throwOnError: false }));
</script>
<template>
<span v-if="!display" class="math-inline" v-html="html"></span>
<div v-else class="math-block" v-html="html"></div>
</template>
+76 -82
View File
@@ -1,103 +1,97 @@
<script setup lang="ts"> <script setup lang="ts">
import { h, Text, computed } from 'vue'; import type { MarkdownAstNode } from '~/utils/markdown';
import { agentParser, userParser, getNodeAttributes, getNodeChildren, getNodeTag, splitParagraphBreaks, textContent, toVueAttributes } from '~/utils/markdown';
import { createTextVNode, h, shallowRef, watch } from 'vue';
import type { SearchSource } from '~/utils/tool-rendering';
import SourceCitation from './SourceCitation.vue';
import MarkdownMath from './Math.vue';
import MarkdownShikiHighlight from './ShikiHighlight.vue'; import MarkdownShikiHighlight from './ShikiHighlight.vue';
const props = defineProps<{ const props = defineProps<{
content: string; content: string;
finished: boolean; finished: boolean;
id: string; id: string;
sources?: ReadonlyMap<string, SearchSource>;
citations?: boolean;
}>(); }>();
const { $remark } = useNuxtApp(); const parser = props.citations ? agentParser : userParser;
const parse = async (content: string, finished: boolean) => {
const tree = await parser(content, { streaming: !finished });
// Copy-on-write: br-free trees return by reference with no allocation.
return splitParagraphBreaks(tree);
};
// this function effectively removes lazy concetenation from the markdown // Parsed trees are replaced, never mutated. Avoid proxying every AST node.
// normally in markdown, if you have two lines that are separated by only const parsed = shallowRef(await parse(props.content, props.finished));
// one new line. E.g.: let parseVersion = 0;
//
// This is some markdown text but I split
// it into two lines so it's easier to read in the editor
//
// they are *usually* concatenated into one line, however,
// since we are a web editor, with line wrapping, that behavior
// in undesirable, so pre preserve newlines by just maiking
// every new line two newlines (as long as we arent in a codeblock)
// function preprocessMarkdown(doc: string) {
// const lines = doc.split('\n');
// let inCode = false;
// let result: string[] = [];
// for (let i = 0; i < lines.length; i++) { watch(
// const fullLine = lines[i]!; [() => props.content, () => props.finished],
async ([content, finished]) => {
// const match = fullLine.match(/^( {0,3})(.*)/); const version = ++parseVersion;
// const content = match?.[2] || ''; const tree = await parse(content, finished);
if (version === parseVersion) {
// if (content.startsWith('```')) { parsed.value = tree;
// if (!inCode) {
// inCode = true;
// } else {
// inCode = false;
// }
// result.push(fullLine);
// continue;
// }
// if (inCode) {
// result.push(fullLine);
// } else {
// if (content.trim().length === 0) {
// result.push('');
// } else {
// if (result.length > 0 && result[result.length - 1] !== '') {
// result.push('');
// }
// result.push(fullLine);
// }
// }
// }
// return result.join('\n');
// }
const ast = computed(() => {
const mdast = $remark.parse(props.content);
return $remark.runSync(mdast);
});
const renderNode = (node: any, index: number): any => {
if (node.type === 'text' || node.type === 'raw') return h(Text, node.value);
if (node.type === 'element') {
if (node.tagName === 'code') {
const isBlock = node.position?.start.line !== node.position?.end.line;
if (isBlock && node.children?.[0]?.type === 'text') {
return h(MarkdownShikiHighlight, {
key: `code-${index}`,
code: node.children[0].value,
language: node.properties?.className?.[0]?.replace('language-', '') || 'text'
});
}
} }
},
);
const children = node.children?.map((child: any, i: number) => renderNode(child, i)) || []; const renderNode = (node: MarkdownAstNode, path: string): any => {
if (typeof node === 'string') {
return h( return createTextVNode(node);
node.tagName,
{ ...node.properties, key: `${node.tagName}-${index}` },
children
);
} }
return null;
if (!Array.isArray(node)) {
return null;
}
if (node[0] === null) {
return null;
}
const tag = getNodeTag(node);
const attributes = getNodeAttributes(node);
if (tag === 'source-cite') {
const ids = typeof attributes.ids === "string" ? attributes.ids.split(",") : [];
return h(SourceCitation, { key: path, ids, sources: props.sources });
}
if (tag === 'pre') {
const language = typeof attributes.language === 'string' ? attributes.language : 'text';
return h(MarkdownShikiHighlight, {
key: path,
code: textContent(node),
language,
});
}
if (tag === 'math') {
return h(MarkdownMath, {
key: path,
content: typeof attributes.content === 'string' ? attributes.content : textContent(node),
display: typeof attributes.class === 'string' && attributes.class.includes('block'),
});
}
const children = getNodeChildren(node)
.map((child, index) => renderNode(child, `${path}.${index}`))
.filter((child) => child !== null);
return h(
tag,
{ ...toVueAttributes(attributes), key: path },
children,
);
}; };
const render = () => { const render = () => {
const children = ast.value?.children?.flatMap(renderNode) || []; const children = parsed.value.nodes
.map((node, index) => renderNode(node, `${props.id}.${index}`))
.filter((child) => child !== null);
return h('div', { class: 'prose-wrapper' }, [ return h('article', { class: 'prose-wrapper markdown-body' }, children);
h('article', { class: 'markdown-body' }, children) };
]);
}
</script> </script>
<template> <template>
@@ -3,8 +3,6 @@ import { type Grammar } from 'shiki';
import { hashSync } from '~/utils/hash'; import { hashSync } from '~/utils/hash';
const props = defineProps<{ code: string; language: string }>(); const props = defineProps<{ code: string; language: string }>();
const start = Date.now();
const MAX_LENGTH = 15000; const MAX_LENGTH = 15000;
const renderId = `${useId()}-${hashSync(props.code + props.language)}`; const renderId = `${useId()}-${hashSync(props.code + props.language)}`;
@@ -76,8 +74,6 @@ onUnmounted(() => {
if (copyTimeout) clearTimeout(copyTimeout); if (copyTimeout) clearTimeout(copyTimeout);
clear(); clear();
}); });
console.log("shiki codeblock rendered in", Date.now() - start);
</script> </script>
<template> <template>
+123
View File
@@ -0,0 +1,123 @@
<script setup lang="ts">
import type { SearchSource } from "~/utils/tool-rendering";
import { autoUpdate, flip, offset, shift, useFloating } from "@floating-ui/vue";
import { computed, onBeforeUnmount, ref, useId } from "vue";
import { getFaviconUrl } from "~/utils/tool-rendering";
const props = defineProps<{
ids: string[];
sources?: ReadonlyMap<string, SearchSource>;
}>();
const entries = computed(() => [...new Set(props.ids)].map(id => ({ id, source: props.sources?.get(id) })));
const firstSource = computed(() => entries.value.find(entry => entry.source)?.source);
const label = computed(() => {
const first = firstSource.value?.hostname ?? "Source unavailable";
return entries.value.length > 1 ? `${first} +${entries.value.length - 1}` : first;
});
const popupId = useId();
const trigger = ref<HTMLElement | null>(null);
const preview = ref<HTMLElement | null>(null);
const open = ref(false);
const failedFavicons = ref(new Set<string>());
const { floatingStyles, placement } = useFloating(trigger, preview, {
open,
strategy: "fixed",
placement: "top-start",
middleware: [offset(8), flip(), shift({ padding: 12 })],
whileElementsMounted: autoUpdate,
transform: false,
});
const transformOrigin = computed(() =>
placement.value.startsWith("top")
? "transform-origin-bottom-center"
: "transform-origin-top-center"
);
let closeTimer: ReturnType<typeof setTimeout> | undefined;
let pointerInside = false;
const cancelClose = () => {
clearTimeout(closeTimer);
closeTimer = undefined;
};
const showPreview = () => {
cancelClose();
open.value = true;
};
const scheduleClose = () => {
cancelClose();
// Allow the pointer to cross the gap between the badge and the preview.
closeTimer = setTimeout(() => {
closeTimer = undefined;
const focused = document.activeElement;
if (!pointerInside && !trigger.value?.contains(focused) && !preview.value?.contains(focused)) {
open.value = false;
}
}, 150);
};
const onPointerEnter = (event: PointerEvent) => {
if (event.pointerType === "touch") {
return;
}
pointerInside = true;
showPreview();
};
const onPointerLeave = (event: PointerEvent) => {
if (event.pointerType === "touch") {
return;
}
pointerInside = false;
scheduleClose();
};
const dismissPreview = () => {
cancelClose();
if (preview.value?.contains(document.activeElement)) {
trigger.value?.focus();
}
open.value = false;
};
onBeforeUnmount(cancelClose);
</script>
<template>
<a ref="trigger" :href="firstSource?.url" target="_blank" rel="noopener noreferrer"
:tabindex="firstSource ? undefined : 0" :aria-expanded="open" :aria-controls="open ? popupId : undefined"
@pointerenter="onPointerEnter" @pointerleave="onPointerLeave" @focusin="showPreview" @focusout="scheduleClose"
@keydown.esc.stop.prevent="dismissPreview"
class="inline-flex items-center gap-1 max-w-64 mx-1 px-2 border border-[var(--color-border)] rounded-full bg-[var(--bg-container)] hover:bg-[var(--color-hover)] transition-colors text-[var(--text-secondary)] text-xs align-baseline cursor-pointer focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-[var(--text-secondary)] focus-visible:outline-offset-2 decoration-none">
<img v-if="firstSource && !failedFavicons.has(firstSource.hostname)" :src="getFaviconUrl(firstSource.hostname)"
alt="" loading="lazy" class="rounded-[4px] w-[0.9rem] h-[0.9rem] m-0"
@error="failedFavicons.add(firstSource.hostname)">
{{ label }}
</a>
<Transition enter-active-class="transition-[opacity,transform] duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
enter-from-class="opacity-0 scale-95 translate-y-1" enter-to-class="opacity-100 scale-100 translate-y-0"
leave-active-class="transition-[opacity,transform] duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
leave-from-class="opacity-100 scale-100 translate-y-0" leave-to-class="opacity-0 scale-95 translate-y-1">
<span v-if="open" :id="popupId" ref="preview" @pointerenter="onPointerEnter" @pointerleave="onPointerLeave"
@focusin="showPreview" @focusout="scheduleClose" @keydown.esc.stop.prevent="dismissPreview"
class="z-2 inset-auto m-0 p-[0.85rem] w-[min(22rem,calc(100vw_-_24px))] max-h-[min(24rem,70vh)] overflow-y-auto border border-[var(--color-border)] rounded-xl bg-[var(--bg-surface)] text-[var(--text-primary)] shadow-md text-sm leading-normal"
:class="transformOrigin" :style="floatingStyles">
<span v-for="(entry, index) in entries" :key="entry.id"
class="flex flex-col gap-[0.4rem] [overflow-wrap:anywhere]"
:class="{ 'mt-[0.8rem] pt-[0.8rem] border-t border-[var(--color-border)]': index > 0 }">
<template v-if="entry.source">
<span class="text-xs text-[var(--text-secondary)]">{{ entry.source.hostname }} · Search
excerpt</span>
<a :href="entry.source.url" target="_blank" rel="noopener noreferrer">{{ entry.source.title }}</a>
<span v-if="entry.source.snippet" class="whitespace-pre-wrap text-[var(--text-secondary)]">{{
entry.source.snippet }}</span>
</template>
<span v-else>Source unavailable ({{ entry.id }})</span>
</span>
</span>
</Transition>
</template>
+5 -38
View File
@@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { MessagePart } from '~/composables/useChat'; import type { MessagePart } from '~/composables/useChat';
import ReasoningBody from './ReasoningBody.vue';
const props = defineProps<{ const props = defineProps<{
part: Readonly<MessagePart>; part: Readonly<MessagePart>;
}>(); }>();
const reasoningOpen = ref(!props.part.finished); const reasoningOpen = ref(!props.part.finished);
const bodyRef = ref<InstanceType<typeof ReasoningBody> | null>(null);
watch( watch(
() => props.part.finished, () => props.part.finished,
@@ -14,44 +16,14 @@ watch(
}, },
); );
const containerRef: Ref<HTMLDivElement | null> = ref(null);
const scrollState = ref('middle');
const { scrollToBottom } = useAutoScroll(containerRef);
const handleScroll = () => {
if (!containerRef.value) return;
const container = containerRef.value;
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
const topThreshold = 0.02 * clientHeight;
if (scrollHeight <= clientHeight) {
// the container does not have enough content to scroll
scrollState.value = '';
return;
}
if (scrollTop <= topThreshold) {
scrollState.value = 'top';
} else if (scrollTop + 100 >= scrollHeight - clientHeight) {
scrollState.value = 'bottom';
} else {
scrollState.value = 'middle';
}
};
const toggleReasoning = async () => { const toggleReasoning = async () => {
if (!props.part.finished) return; if (!props.part.finished) return;
reasoningOpen.value = !reasoningOpen.value; reasoningOpen.value = !reasoningOpen.value;
if (!reasoningOpen.value) return; if (!reasoningOpen.value) return;
await nextTick(); await nextTick();
scrollToBottom('instant'); bodyRef.value?.scrollToBottom('instant');
handleScroll(); bodyRef.value?.handleScroll();
}; };
const formatDuration = (ms: number) => { const formatDuration = (ms: number) => {
@@ -91,11 +63,6 @@ const formatDuration = (ms: number) => {
<span class="i-mynaui-chevron-down w-4 h-4 text-[var(--text-secondary)]" <span class="i-mynaui-chevron-down w-4 h-4 text-[var(--text-secondary)]"
:class="reasoningOpen ? '' : '-rotate-90'"></span> :class="reasoningOpen ? '' : '-rotate-90'"></span>
</button> </button>
<div v-if="reasoningOpen" ref="containerRef" @scroll="handleScroll" <ReasoningBody v-if="reasoningOpen" ref="bodyRef" :part="part" />
:class="['reasoning-container max-h-[min(40vh,320px)] overflow-y-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]', scrollState]">
<div class="p-2">
<MarkdownRenderer :finished="part.finished" :id="part.id" :content="part.content!" />
</div>
</div>
</div> </div>
</template> </template>
@@ -0,0 +1,65 @@
<script setup lang="ts">
import type { MessagePart } from '~/composables/useChat';
import { nextTick, onMounted, ref, watch, type Ref } from 'vue';
const props = defineProps<{
part: Readonly<MessagePart>;
}>();
const containerRef: Ref<HTMLDivElement | null> = ref(null);
const scrollState = ref('');
const { scrollToBottom } = useAutoScroll(containerRef);
const handleScroll = () => {
if (!containerRef.value) {
return;
}
const container = containerRef.value;
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
if (scrollHeight <= clientHeight) {
scrollState.value = '';
return;
}
const threshold = Math.max(6, 0.02 * clientHeight);
if (scrollTop <= threshold) {
scrollState.value = 'top';
} else if (scrollTop >= scrollHeight - clientHeight - threshold) {
scrollState.value = 'bottom';
} else {
scrollState.value = 'middle';
}
};
onMounted(() => {
nextTick(() => {
handleScroll();
});
});
watch(
() => props.part.content,
() => {
nextTick(() => {
handleScroll();
});
},
);
defineExpose({ scrollToBottom, handleScroll });
</script>
<template>
<div ref="containerRef" @scroll="handleScroll"
:class="['reasoning-container max-h-[min(40vh,320px)] overflow-y-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]', scrollState]">
<div class="p-2">
<MarkdownRenderer :finished="props.part.finished" :id="props.part.id" :content="props.part.content!" />
</div>
</div>
</template>
-11
View File
@@ -1,11 +0,0 @@
<script lang="ts" setup>
import type { MessagePart } from '~/composables/useChat';
const props = defineProps<{
part: Readonly<MessagePart>;
}>();
</script>
<template>
<MarkdownRenderer :finished="part.finished" :id="part.id" :content="part.content!" />
</template>
@@ -0,0 +1,103 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useNuxtApp } from "#app";
const props = defineProps<{
code: string;
lang?: string;
}>();
const { $shiki } = useNuxtApp();
const html = ref("");
const targetLang = computed(() => props.lang || "json");
const lineNumberWidth = computed(() => {
const lines = props.code ? props.code.split("\n").length : 1;
return lines.toString().length;
});
watch(
[() => props.code, targetLang],
async ([newCode, newLang]) => {
if (!newCode) {
html.value = "";
return;
}
let resolvedLang = newLang;
try {
if ($shiki?.getLanguage) {
await $shiki.getLanguage(resolvedLang);
}
} catch {
resolvedLang = "text";
}
try {
if ($shiki?.codeToHtml) {
html.value = await $shiki.codeToHtml(newCode, {
lang: resolvedLang,
themes: {
dark: "vitesse-dark",
light: "vitesse-light",
},
});
} else {
html.value = "";
}
} catch {
html.value = "";
}
},
{ immediate: true },
);
</script>
<template>
<div
class="tool-code-container overflow-auto max-h-80 scrollbar-thin text-xs font-mono"
:style="`--line-number-width: ${lineNumberWidth}ch;`"
>
<div v-if="html" class="tool-code-highlight" v-html="html"></div>
<pre v-else class="tool-code-fallback m-0 p-3.5 text-[var(--text-secondary)] whitespace-pre-wrap break-words leading-relaxed">{{ code }}</pre>
</div>
</template>
<style scoped>
.tool-code-container {
background-color: var(--shiki-dark-bg, #121019);
}
:deep(.tool-code-highlight > pre) {
margin: 0;
padding: 0.875rem 1rem;
font-size: 0.75rem;
line-height: 1.625;
scrollbar-width: thin;
overflow-x: auto;
counter-reset: lines;
min-height: 100%;
box-sizing: border-box;
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
:deep(.tool-code-highlight > pre > code .line::before) {
counter-increment: lines;
content: counter(lines);
width: var(--line-number-width, 2ch);
margin-right: 1.25rem;
display: inline-block;
text-align: right;
color: rgba(255, 255, 255, 0.25);
user-select: none;
}
:deep(html.light .tool-code-container) {
background-color: #ffffff;
}
:deep(html.light .tool-code-highlight > pre > code .line::before) {
color: rgba(0, 0, 0, 0.25);
}
</style>
+7 -6
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ToolCall } from '~/composables/useChat'; import type { ToolCall } from '~/composables/useChat';
import { formatToolValue, parseToolValue } from '~/utils/tool-rendering';
const props = defineProps<{ const props = defineProps<{
toolCall: Readonly<ToolCall>; toolCall: Readonly<ToolCall>;
@@ -28,21 +29,21 @@ const input: ComputedRef<string> = computed(() => {
case 'input': case 'input':
if (props.toolCall.input === null || props.toolCall.input === undefined) return ''; if (props.toolCall.input === null || props.toolCall.input === undefined) return '';
if (props.toolCall.input!.type === 'json') { if (props.toolCall.input!.type === 'json') {
return JSON.stringify(JSON.parse(props.toolCall.input!.value), null, 2); return formatToolValue(props.toolCall.input);
} }
return props.toolCall.input!.value; return props.toolCall.input!.value;
case 'output': case 'output':
if (props.toolCall.output === null || props.toolCall.output === undefined) return ''; if (props.toolCall.output === null || props.toolCall.output === undefined) return '';
if (props.toolCall.output!.type === 'json') { if (props.toolCall.output!.type === 'json') {
return JSON.stringify(JSON.parse(props.toolCall.output!.value), null, 2); return formatToolValue(props.toolCall.output);
} }
return props.toolCall.output!.value; return props.toolCall.output!.value;
case 'error': case 'error':
if (props.toolCall.error === null || props.toolCall.error === undefined) return ''; if (props.toolCall.error === null || props.toolCall.error === undefined) return '';
if (props.toolCall.error!.type === 'json') { if (props.toolCall.error!.type === 'json') {
return JSON.stringify(JSON.parse(props.toolCall.error!.value), null, 2); return formatToolValue(props.toolCall.error);
} }
return props.toolCall.error!.value; return props.toolCall.error!.value;
@@ -55,7 +56,7 @@ const input: ComputedRef<string> = computed(() => {
traceObj.input = traceObj.input.value; traceObj.input = traceObj.input.value;
break; break;
case 'json': case 'json':
traceObj.input = JSON.parse(traceObj.input.value); traceObj.input = parseToolValue(props.toolCall.input);
break; break;
} }
@@ -64,7 +65,7 @@ const input: ComputedRef<string> = computed(() => {
traceObj.output = traceObj.output.value; traceObj.output = traceObj.output.value;
break; break;
case 'json': case 'json':
traceObj.output = JSON.parse(traceObj.output.value); traceObj.output = parseToolValue(props.toolCall.output);
break; break;
} }
@@ -73,7 +74,7 @@ const input: ComputedRef<string> = computed(() => {
traceObj.error = traceObj.error.value; traceObj.error = traceObj.error.value;
break; break;
case 'json': case 'json':
traceObj.error = JSON.parse(traceObj.error.value); traceObj.error = parseToolValue(props.toolCall.error);
break; break;
} }
@@ -0,0 +1,324 @@
<script setup lang="ts">
import type { MessagePart, ToolCall } from "~/composables/useChat";
import { computed, onUnmounted, ref, useId, watch } from "vue";
import CodeViewer from "./CodeViewer.vue";
import {
formatDuration,
formatToolValue,
getToolDisplayInfo,
parseToolValue,
} from "~/utils/tool-rendering";
export interface CustomToolTab {
id: string;
label: string;
count?: string;
copyContent?: () => string;
}
const props = defineProps<{
toolCall: Readonly<ToolCall>;
part?: MessagePart;
title?: string;
subtitle?: string;
icon?: string;
customTab?: CustomToolTab;
duration?: string;
}>();
const emit = defineEmits<{
retry: [];
}>();
const isOpen = ref(false);
const contentId = useId();
const copied = ref(false);
let copyTimeout: ReturnType<typeof setTimeout> | null = null;
const displayInfo = computed(() => getToolDisplayInfo(props.toolCall));
const displayTitle = computed(() => props.title || displayInfo.value.verb);
const displaySubtitle = computed(() => props.subtitle !== undefined ? props.subtitle : displayInfo.value.target);
const displayIcon = computed(() => props.icon || displayInfo.value.icon);
const calculatedDuration = computed(() => {
if (props.duration) {
return props.duration;
}
if (props.toolCall.status === "pending") {
return null;
}
const start = props.part?.createdAt || props.toolCall.createdAt;
const end = props.part?.lastUpdatedAt;
return formatDuration(start, end);
});
interface TabItem {
id: string;
label: string;
}
const availableTabs = computed<TabItem[]>(() => {
const tabs: TabItem[] = [];
if (props.customTab) {
tabs.push({ id: props.customTab.id, label: props.customTab.label });
}
if (props.toolCall.status === "failed" || props.toolCall.error) {
tabs.push({ id: "error", label: "Error" });
}
if (props.toolCall.input) {
tabs.push({ id: "input", label: "Input" });
}
if (props.toolCall.output) {
tabs.push({ id: "output", label: "Output" });
}
tabs.push({ id: "trace", label: "Trace" });
return tabs;
});
const resolveDefaultTab = (): string => {
if (props.toolCall.status === "failed" || props.toolCall.error) {
return "error";
}
if (props.customTab && props.toolCall.status === "completed") {
return props.customTab.id;
}
if (props.toolCall.output) {
return "output";
}
if (props.toolCall.input) {
return "input";
}
return "trace";
};
const activeTab = ref(resolveDefaultTab());
watch(
() => props.toolCall.status,
(newStatus) => {
if (newStatus === "failed") {
activeTab.value = "error";
} else if (newStatus === "completed" && props.customTab) {
activeTab.value = props.customTab.id;
}
},
);
const formattedError = computed(() => {
if (!props.toolCall.error) {
return "";
}
const parsed = parseToolValue(props.toolCall.error);
if (typeof parsed === "string") {
return parsed;
}
return JSON.stringify(parsed, null, 2);
});
const currentTabCode = computed(() => {
switch (activeTab.value) {
case "input":
return formatToolValue(props.toolCall.input);
case "output":
return formatToolValue(props.toolCall.output);
case "error":
return formattedError.value;
case "trace": {
const traceObj = {
id: props.toolCall.id,
toolName: props.toolCall.toolName,
status: props.toolCall.status,
input: parseToolValue(props.toolCall.input),
output: parseToolValue(props.toolCall.output),
error: parseToolValue(props.toolCall.error),
createdAt: props.toolCall.createdAt,
};
return JSON.stringify(traceObj, null, 2);
}
default:
return "";
}
});
const currentTabLang = computed(() => {
if (activeTab.value === "trace") {
return "json";
}
if (activeTab.value === "input") {
return props.toolCall.input?.type === "json" ? "json" : "text";
}
if (activeTab.value === "output") {
return props.toolCall.output?.type === "json" ? "json" : "text";
}
return "text";
});
const footerInfo = computed(() => {
if (props.customTab && activeTab.value === props.customTab.id) {
return props.customTab.count || "";
}
const lines = currentTabCode.value ? currentTabCode.value.split("\n").length : 0;
const lineLabel = lines === 1 ? "line" : "lines";
const lang = currentTabLang.value;
return `${lines} ${lineLabel} · ${lang}`;
});
const canRetry = computed(() => props.toolCall.status === "failed");
const copyActiveTab = async () => {
let textToCopy = "";
if (props.customTab && activeTab.value === props.customTab.id && props.customTab.copyContent) {
textToCopy = props.customTab.copyContent();
} else {
textToCopy = currentTabCode.value;
}
if (!textToCopy) {
return;
}
try {
await navigator.clipboard.writeText(textToCopy);
copied.value = true;
if (copyTimeout) {
clearTimeout(copyTimeout);
}
copyTimeout = setTimeout(() => {
copied.value = false;
}, 2000);
} catch (err) {
console.error("Failed to copy tool call content:", err);
}
};
const toggle = () => {
isOpen.value = !isOpen.value;
};
onUnmounted(() => {
if (copyTimeout) {
clearTimeout(copyTimeout);
}
});
</script>
<template>
<div class="tool-disclosure flex flex-col min-w-0 text-[var(--text-tertiary)]">
<button
type="button"
class="w-full @hover:bg-[var(--color-hover)] rounded-lg p-1 flex justify-between items-center transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] cursor-pointer select-none text-left"
:aria-expanded="isOpen"
:aria-controls="contentId"
@click="toggle"
>
<span class="flex items-center gap-1.5 min-w-0 overflow-hidden">
<span class="w-6 h-6 flex-shrink-0 rounded-lg overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center">
<span
:class="[
'w-3 h-3 text-3 transition-colors',
displayIcon,
toolCall.status === 'failed' ? 'text-red-400' : 'text-[var(--text-secondary)]'
]"
aria-hidden="true"
></span>
</span>
<span class="text-sm truncate">
<span :class="toolCall.status === 'failed' ? 'text-red-400' : 'text-[var(--text-secondary)]'">
{{ displayTitle }}
</span>
<span v-if="displaySubtitle" class="text-[var(--text-tertiary)] ml-1.5 font-normal truncate">
{{ displaySubtitle }}
</span>
</span>
</span>
<span class="flex items-center gap-2 shrink-0 ml-2">
<slot name="summary"></slot>
<span
v-if="toolCall.status === 'pending'"
class="i-svg-spinners-180-ring-with-bg text-3 text-[var(--color-accent)]"
aria-hidden="true"
></span>
<span v-else-if="toolCall.status === 'failed'" class="text-xs text-red-400 font-medium">Failed</span>
<span v-else-if="toolCall.status === 'cancelled'" class="text-xs text-[var(--text-tertiary)]">Cancelled</span>
<span v-else-if="calculatedDuration" class="text-xs text-[var(--text-tertiary)] font-mono">{{ calculatedDuration }}</span>
<span
class="i-mynaui-chevron-down w-4 h-4 text-[var(--text-secondary)] transition-transform duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
:class="isOpen ? '' : '-rotate-90'"
aria-hidden="true"
></span>
</span>
</button>
<div
v-show="isOpen"
:id="contentId"
class="mt-1 border border-[var(--color-border)] rounded-xl bg-[var(--bg-surface)] overflow-hidden text-xs"
>
<div class="flex items-center gap-1 px-3 pt-2.5 pb-2 overflow-x-auto scrollbar-none border-b border-[var(--color-border)]/40">
<button
v-for="tab in availableTabs"
:key="tab.id"
type="button"
class="px-2.5 py-1 rounded-md text-xs font-medium transition-colors cursor-pointer shrink-0"
:class="[
activeTab === tab.id
? (tab.id === 'error'
? 'bg-red-500/15 text-red-300'
: 'bg-[var(--color-hover)] text-[var(--text-primary)]')
: 'text-[var(--text-tertiary)] @hover:text-[var(--text-secondary)] @hover:bg-[var(--color-hover)]/50'
]"
@click="activeTab = tab.id"
>
{{ tab.label }}
</button>
</div>
<div class="min-h-0">
<div v-if="customTab && activeTab === customTab.id">
<slot :name="customTab.id">
<slot></slot>
</slot>
</div>
<div v-else-if="activeTab === 'error'">
<CodeViewer :code="formattedError" lang="json"></CodeViewer>
</div>
<div v-else-if="['input', 'output', 'trace'].includes(activeTab)">
<CodeViewer :code="currentTabCode" :lang="currentTabLang"></CodeViewer>
</div>
</div>
<div class="flex items-center justify-between px-3 py-2 border-t border-[var(--color-border)]/40 text-xs text-[var(--text-tertiary)] bg-[var(--color-hover)]/20">
<div>
<span class="font-mono text-[11px] text-[var(--text-tertiary)]">{{ footerInfo }}</span>
</div>
<div class="flex items-center gap-2">
<slot name="actions"></slot>
<button
v-if="canRetry"
type="button"
class="flex items-center gap-1.5 px-2 py-1 rounded text-[var(--text-tertiary)] @hover:text-red-400 @hover:bg-[var(--color-hover)] transition-colors cursor-pointer text-xs"
@click="emit('retry')"
>
<span class="i-mynaui-refresh w-3.5 h-3.5" aria-hidden="true"></span>
<span>Retry</span>
</button>
<button
type="button"
class="flex items-center gap-1.5 px-2 py-1 rounded text-[var(--text-tertiary)] @hover:text-[var(--text-primary)] @hover:bg-[var(--color-hover)] transition-colors cursor-pointer text-xs"
@click="copyActiveTab"
>
<span :class="copied ? 'i-mynaui-check text-emerald-400' : 'i-mynaui-copy'" class="w-3.5 h-3.5" aria-hidden="true"></span>
<span>{{ copied ? 'Copied' : 'Copy' }}</span>
</button>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { MessagePart, ToolCall } from "~/composables/useChat";
import Disclosure from "./Disclosure.vue";
defineProps<{
toolCall: Readonly<ToolCall>;
part?: MessagePart;
}>();
const emit = defineEmits<{
retry: [];
}>();
</script>
<template>
<Disclosure :tool-call="toolCall" :part="part" @retry="emit('retry')"></Disclosure>
</template>
@@ -0,0 +1,200 @@
<script setup lang="ts">
import type { MessagePart, ToolCall } from "~/composables/useChat";
import { computed, ref } from "vue";
import Disclosure from "./Disclosure.vue";
import {
domainInitial,
getDomainColor,
getFaviconUrl,
parseToolValue,
readSearchSources,
readSearchWarnings,
} from "~/utils/tool-rendering";
const props = defineProps<{
toolCall: Readonly<ToolCall>;
part?: MessagePart;
}>();
const emit = defineEmits<{
retry: [];
}>();
const output = computed(() => parseToolValue(props.toolCall.output));
const sources = computed(() => readSearchSources(output.value));
const warnings = computed(() => readSearchWarnings(output.value));
const warningSummary = computed(() => warnings.value.map(warning => `${warning.engine}: ${warning.reason}`).join("; "));
const query = computed(() => {
const input = parseToolValue(props.toolCall.input);
if (typeof input === "object" && input !== null && "query" in input && typeof input.query === "string") {
return input.query;
}
return "";
});
const sitesCount = computed(() => {
return sources.value?.length ?? 0;
});
const topDomains = computed(() => {
return [...new Set((sources.value ?? []).map(s => s.hostname))].slice(0, 4);
});
const failedFavicons = ref(new Set<string>());
const onFaviconError = (domain: string) => {
failedFavicons.value.add(domain);
};
const getDomainBadgeStyle = (idx: number) => {
const zIndex = topDomains.value.length - idx;
if (idx === 0) {
return { zIndex };
}
return {
zIndex,
maskImage: "radial-gradient(circle 12.5px at -4px 50%, transparent 12.5px, #000 13px)",
WebkitMaskImage: "radial-gradient(circle 12.5px at -4px 50%, transparent 12.5px, #000 13px)",
};
};
const title = computed(() => {
if (props.toolCall.status === "pending") {
return "Searching the web…";
}
if (props.toolCall.status === "failed") {
return "Search failed";
}
if (props.toolCall.status === "cancelled") {
return "Search cancelled";
}
const count = sitesCount.value;
if (count > 0) {
return `Searched ${count} ${count === 1 ? "site" : "sites"}`;
}
return "Searched the web";
});
const formattedQuery = computed(() => {
return query.value ? `"${query.value}"` : "";
});
const customTab = computed(() => {
const count = sources.value?.length ?? 0;
return {
id: "sources",
label: "Sources",
count: `${count} ${count === 1 ? "source" : "sources"}`,
copyContent: () => {
if (!sources.value?.length) {
return "";
}
return sources.value.map(s => `${s.title}\n${s.url}`).join("\n\n");
},
};
});
</script>
<template>
<Disclosure
:tool-call="toolCall"
:part="part"
:title="title"
:subtitle="formattedQuery"
icon="i-mynaui-search"
:custom-tab="customTab"
@retry="emit('retry')"
>
<template #summary>
<Tooltip
v-if="warnings.length"
class="flex items-center text-amber-500 mr-2"
:text="`Some search engines failed: ${warningSummary}`"
>
<span
class="i-mynaui-danger-triangle w-4 h-4"
role="img"
:aria-label="`Some search engines failed: ${warningSummary}`"
></span>
</Tooltip>
<div v-if="topDomains.length && toolCall.status !== 'pending'" class="flex items-center mr-1" aria-hidden="true">
<span
v-for="(domain, idx) in topDomains"
:key="domain"
class="relative w-[22px] h-[22px] rounded-full overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center shrink-0"
:class="idx > 0 ? '-ml-[7px]' : ''"
:style="getDomainBadgeStyle(idx)"
:title="domain"
>
<img
v-if="!failedFavicons.has(domain)"
:src="getFaviconUrl(domain)"
:alt="domain"
class="w-[15px] h-[15px] rounded-full object-contain"
loading="lazy"
@error="onFaviconError(domain)"
>
<span
v-else
class="w-full h-full flex items-center justify-center text-[10px] font-bold text-white"
:style="{ backgroundColor: getDomainColor(domain) }"
>
{{ domainInitial(domain) }}
</span>
</span>
</div>
</template>
<template #sources>
<div class="flex flex-col gap-2 max-h-80 overflow-y-auto scrollbar-thin p-3">
<p v-if="warnings.length" class="flex items-start gap-2 text-xs text-amber-500 m-0">
<span class="i-mynaui-danger-triangle w-4 h-4 shrink-0" aria-hidden="true"></span>
<span>Some search engines failed: {{ warningSummary }}</span>
</p>
<a
v-for="source in sources"
:key="source.url"
:href="source.url"
target="_blank"
rel="noopener noreferrer"
class="flex flex-col gap-1.5 p-3 rounded-xl bg-[var(--bg-container)] @hover:bg-[var(--color-hover)] transition-colors cursor-pointer text-left no-underline border border-transparent @hover:border-[var(--color-border)]"
>
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-2.5 min-w-0">
<span class="w-6 h-6 rounded-lg overflow-hidden flex items-center justify-center shrink-0 bg-[var(--bg-surface)]">
<img
v-if="!failedFavicons.has(source.hostname)"
:src="getFaviconUrl(source.hostname)"
:alt="source.hostname"
class="w-4 h-4 rounded-md object-contain"
loading="lazy"
@error="onFaviconError(source.hostname)"
>
<span
v-else
class="w-full h-full flex items-center justify-center text-xs font-bold text-white"
:style="{ backgroundColor: getDomainColor(source.hostname) }"
>
{{ domainInitial(source.hostname) }}
</span>
</span>
<span class="text-sm font-medium text-[var(--text-primary)] truncate">
{{ source.title }}
</span>
</div>
<span class="text-[11px] font-mono text-[var(--text-dim)] truncate pl-0.5">
{{ source.url }}
</span>
</div>
<p v-if="source.snippet" class="text-xs text-[var(--text-secondary)] leading-relaxed m-0 line-clamp-2">
{{ source.snippet }}
</p>
</a>
<p v-if="!sources?.length" class="text-xs text-[var(--text-tertiary)] m-0">
{{ toolCall.status === "pending" ? "Waiting for results…" : "No usable sources returned." }}
</p>
</div>
</template>
</Disclosure>
</template>
+16 -32
View File
@@ -1,44 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ToolCall } from '~/composables/useChat'; import type { MessagePart, ToolCall } from "~/composables/useChat";
import Debug from './Debug.vue'; import { computed } from "vue";
import Generic from "./Generic.vue";
import { resolveToolRenderer } from "./renderers";
const props = defineProps<{ const props = defineProps<{
toolCall: Readonly<ToolCall>; toolCall: Readonly<ToolCall>;
part?: MessagePart;
}>(); }>();
const deubgToolCallOpen = ref(false); const emit = defineEmits<{
retry: [];
}>();
const toggleDebugToolCall = () => { const renderer = computed(() => resolveToolRenderer(props.toolCall.toolName));
deubgToolCallOpen.value = !deubgToolCallOpen.value;
};
</script> </script>
<template> <template>
<div class="flex flex-col gap-2"> <div class="flex flex-col min-w-0">
<div <NuxtErrorBoundary :key="toolCall.id + ':' + toolCall.toolName">
class="select-none w-full @hover:bg-[var(--color-hover)] group rounded-lg p-1 flex justify-between items-center text-[--text-tertiary] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"> <component :is="renderer" :tool-call="toolCall" :part="part" @retry="emit('retry')"></component>
<div class="flex items-center justify-between w-full"> <template #error>
<div class="flex items-center gap-1"> <Generic :tool-call="toolCall" :part="part" @retry="emit('retry')"></Generic>
<div </template>
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center"> </NuxtErrorBoundary>
<span class="w-3 h-3" :class="{
'i-svg-spinners-180-ring-with-bg': toolCall.status === 'pending',
'i-mynaui-x-solid text-[#ff3b3b]': toolCall.status === 'failed',
'i-mynaui-tool': toolCall.status !== 'pending' && toolCall.status !== 'failed',
}"></span>
</div>
{{ toolCall.toolName }}
</div>
<div
class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<button @click="toggleDebugToolCall"
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden @hover:bg-[var(--color-hover)] flex items-center justify-center transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<span class="i-mynaui-search w-3 h-3 text-[var(--text-secondary)]"></span>
</button>
</div>
</div>
</div>
<Debug v-if="deubgToolCallOpen" :toolCall="toolCall" />
</div> </div>
</template> </template>
@@ -0,0 +1,15 @@
import type { Component } from "vue";
import Generic from "./Generic.vue";
import Search from "./Search.vue";
// Renderers receive a readonly toolCall prop and own their presentation only.
const renderers: Readonly<Record<string, Component>> = Object.freeze({
search: Search,
});
export const resolveToolRenderer = (toolName: string): Component => {
if (Object.hasOwn(renderers, toolName)) {
return renderers[toolName] ?? Generic;
}
return Generic;
};
+21 -9
View File
@@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { collectCitationSources } from '~/utils/citations';
import type { SearchSource } from '~/utils/tool-rendering';
import Error from './Error.vue'; import Error from './Error.vue';
import Reasoning from './Reasoning.vue'; import Reasoning from './Reasoning.vue';
import Text from './Text.vue';
import Tool from './Tool/index.vue'; import Tool from './Tool/index.vue';
import type { MessageEntity } from '~/composables/useChat'; import type { MessageEntity } from '~/composables/useChat';
@@ -9,6 +10,14 @@ const props = defineProps<{
message: MessageEntity message: MessageEntity
}>(); }>();
const EMPTY_SOURCES: ReadonlyMap<string, SearchSource> = new Map();
// Citation markers are rare; avoid JSON-parsing every tool output when none exist.
const sources = computed(() => {
const parts = props.message.parts ?? [];
const hasCitations = parts.some(part => part.type === 'text' && part.content?.includes('[[cite:') === true);
return hasCitations ? collectCitationSources(parts) : EMPTY_SOURCES;
});
const triggerRef = ref<HTMLElement | null>(null); const triggerRef = ref<HTMLElement | null>(null);
const { open, close, isOpen } = useTokenDropdown(); const { open, close, isOpen } = useTokenDropdown();
@@ -25,14 +34,17 @@ const toggleTokenDropdown = () => {
<template> <template>
<div class="flex flex-col w-full gap-2"> <div class="flex flex-col w-full gap-2">
<div v-for="part in message.parts?.filter(p => p.content?.trim() !== '' || p.toolCall)" :key="part.id"> <template v-for="part in message.parts" :key="part.id">
<Reasoning v-if="part.type === 'reasoning'" :part="part" /> <template v-if="part.content?.trim() !== '' || part.toolCall">
<Text v-else-if="part.type === 'text'" :part="part" /> <Reasoning v-if="part.type === 'reasoning'" :part="part"></Reasoning>
<Tool v-else-if="part.type === 'tool-call'" :toolCall="part.toolCall!" /> <MarkdownRenderer v-else-if="part.type === 'text'" :finished="part.finished" :id="part.id"
<div v-else> :content="part.content!" :sources="sources" :citations="true"></MarkdownRenderer>
Unhandled part type: {{ part.type }} {{ part }} <Tool v-else-if="part.type === 'tool-call'" :tool-call="part.toolCall!" :part="part"></Tool>
</div> <div v-else>
</div> Unhandled part type: {{ part.type }} {{ part }}
</div>
</template>
</template>
<span class="flex items-center gap-1" <span class="flex items-center gap-1"
v-if="message.generation?.status === 'pending' && (message.parts || []).length === 0"> v-if="message.generation?.status === 'pending' && (message.parts || []).length === 0">
+13 -8
View File
@@ -7,14 +7,19 @@ defineProps<{
</script> </script>
<template> <template>
<div class="flex flex-col gap-2 max-w-full bg-[var(--bg-container)] py-2 px-3 rounded-xl"> <div class="flex flex-col items-end gap-1 max-w-full">
<MarkdownRenderer v-if="message.content" :finished="true" :content="message.content" :id="message.id" /> <time class="text-[10px] text-[var(--text-secondary)] opacity-60 select-none pr-1">
<div v-if="message.attachments && message.attachments.length > 0" class="flex flex-col gap-2"> {{ new Date(message.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }}
<div v-for="attachment in message.attachments" :key="attachment.id" </time>
class="flex flex-wrap items-center gap-2"> <div class="flex flex-col gap-2 max-w-full bg-[var(--bg-container)] py-2 px-3 rounded-xl">
<div <MarkdownRenderer v-if="message.content" :finished="true" :content="message.content" :id="message.id" />
class="flex-shrink-0 rounded-lg overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center"> <div v-if="message.attachments && message.attachments.length > 0" class="flex flex-col gap-2">
<AttachmentDisplay :file="attachment.file" /> <div v-for="attachment in message.attachments" :key="attachment.id"
class="flex flex-wrap items-center gap-2">
<div
class="flex-shrink-0 rounded-lg overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center">
<AttachmentDisplay :file="attachment.file" />
</div>
</div> </div>
</div> </div>
</div> </div>
+112 -75
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Message } from '~/composables/useChat'; import type { Message } from '~/composables/useChat';
import { getActiveMessage, getCurrentChildIndex, getMessageCount, getNextActiveChildId } from '~~/utils/message';
const { openDialog } = useDialog(); const { openDialog } = useDialog();
@@ -12,6 +13,7 @@ const emit = defineEmits<{
delete: []; delete: [];
edit: [value: string]; edit: [value: string];
patch: [updates: Partial<Message>]; patch: [updates: Partial<Message>];
fork: [];
}>(); }>();
let reqAbortController: AbortController | null = null; let reqAbortController: AbortController | null = null;
@@ -34,7 +36,9 @@ watch(() => message.activeChildId, async (newValue) => {
}); });
watch(() => message.children.length, (newCount, oldCount) => { watch(() => message.children.length, (newCount, oldCount) => {
if (newCount === oldCount) return; if (newCount === oldCount) {
return;
}
if (newCount === 0) { if (newCount === 0) {
emit('patch', { activeChildId: null }); emit('patch', { activeChildId: null });
@@ -42,7 +46,7 @@ watch(() => message.children.length, (newCount, oldCount) => {
} }
if (oldCount > newCount) { if (oldCount > newCount) {
const activeChild = message.children.find(c => c.id === message.activeChildId); const activeChild = message.children.find(c => c?.id === message.activeChildId);
if (!activeChild) { if (!activeChild) {
emit('patch', { activeChildId: message.children[newCount - 1]?.id ?? null }); emit('patch', { activeChildId: message.children[newCount - 1]?.id ?? null });
} }
@@ -55,19 +59,17 @@ watch(() => message.children.length, (newCount, oldCount) => {
} }
}); });
const activeMessage = computed(() => { watch(() => message.deleted, (isDeleted) => {
if (message.activeChildId && message.children.length > 0) { if (isDeleted && message.children.length > 0) {
const child = message.children.find(c => c.id === message.activeChildId); const activeChild = message.children.find(c => c?.id === message.activeChildId);
if (child) return child; if (!activeChild) {
emit('patch', { activeChildId: message.children[0]?.id ?? null });
}
} }
if (message.deleted === true && message.children.length > 0) {
return message.children[0];
}
return message;
}); });
const activeMessage = computed(() => getActiveMessage(message));
const copied = ref(false); const copied = ref(false);
const copyMessage = async () => { const copyMessage = async () => {
if (activeMessage.value!.role === 'user') { if (activeMessage.value!.role === 'user') {
@@ -126,91 +128,126 @@ const handleEdit = async () => {
}); });
}; };
const forkMessage = () => {
emit('fork');
};
const deleteMessage = () => { const deleteMessage = () => {
emit('delete'); emit('delete');
}; };
const messageCount = computed(() => { const messageCount = computed(() => getMessageCount(message));
if (message.deleted === true) {
return message.children.length;
}
return message.children.length + 1; const currentChildIndex = computed(() => getCurrentChildIndex(message));
});
const currentChildIndex = computed(() => {
if (!message.activeChildId) return 0;
const idx = message.children.findIndex(c => c.id === message.activeChildId);
return idx >= 0 ? idx + 1 : 0;
});
const navigateChild = (direction: -1 | 1) => { const navigateChild = (direction: -1 | 1) => {
const allItems = message.deleted === true const nextActiveChildId = getNextActiveChildId(message, direction);
? message.children emit('patch', { activeChildId: nextActiveChildId });
: [null, ...message.children]; };
const currentIdx = allItems.findIndex(item => const { show: showTooltip, hide: hideTooltip } = useTooltip();
item === null ? !message.activeChildId : item.id === message.activeChildId
);
const newIdx = Math.max(0, Math.min(allItems.length - 1, currentIdx + direction)); let hasCursor: boolean | null = null;
const newItem = allItems[newIdx]; const checkHasCursor = () => {
if (hasCursor === null && typeof window !== 'undefined') {
hasCursor = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
}
return hasCursor ?? false;
};
emit('patch', { activeChildId: newItem?.id ?? null }); const handleTooltipHover = (e: MouseEvent) => {
if (!checkHasCursor()) {
return;
}
const target = (e.target as HTMLElement).closest('[data-hotkey], [data-tooltip]') as HTMLElement | null;
if (!target) {
return;
}
const hotkey = target.dataset.hotkey ? target.dataset.hotkey.split(',') : undefined;
const text = target.dataset.tooltip || undefined;
showTooltip(target, hotkey, text);
};
const handleTooltipLeave = () => {
hideTooltip();
};
const handleAction = (e: MouseEvent) => {
const trigger = (e.target as HTMLElement).closest('[data-action]') as HTMLElement | null;
if (!trigger) {
return;
}
switch (trigger.dataset.action) {
case 'prev-child':
navigateChild(-1);
break;
case 'next-child':
navigateChild(1);
break;
case 'regenerate':
regenerateMessage();
break;
case 'edit':
handleEdit();
break;
case 'copy':
copyMessage();
break;
case 'fork':
forkMessage();
break;
case 'delete':
deleteMessage();
break;
}
}; };
</script> </script>
<template> <template>
<div v-if="activeMessage" class="max-w-full mb-4 group flex flex-col"> <div v-if="activeMessage" class="max-w-full mb-4 group flex flex-col">
<div :class="[message.role === 'user' ? 'pl-9 flex justify-end' : '']"> <MessageUser v-if="message.role === 'user'" class="pl-9 self-end" :message="activeMessage!"></MessageUser>
<MessageUser v-if="message.role === 'user'" :message="activeMessage!" /> <MessageAgent v-else :message="activeMessage"></MessageAgent>
<MessageAgent v-else :message="activeMessage" /> <div class="flex justify-between items-center" @click="handleAction">
</div> <div v-if="messageCount > 1" class="flex gap-1"
<div class="flex justify-between items-center"> @mouseover="handleTooltipHover" @mouseout="handleTooltipLeave">
<div> <button data-action="prev-child" :data-hotkey="currentChildIndex > 0 ? 'alt,[' : undefined"
<div v-if="messageCount > 1" class="flex gap-1"> class="@hover:bg-[var(--color-hover)] rounded transition duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<Tooltip :inert="currentChildIndex === 0" :hotkey="['alt', '[']"> <span class="i-mynaui-chevron-left w-4 h-4 text-[var(--text-secondary)] pointer-events-none"
<button @click="navigateChild(-1)" :class="currentChildIndex === 0 ? 'opacity-0' : ''"></span>
class="@hover:bg-[var(--color-hover)] rounded transition duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"> </button>
<span class="i-mynaui-chevron-left w-4 h-4 text-[var(--text-secondary)]" <span class="text-xs text-[var(--text-secondary)]">
:class="currentChildIndex === 0 ? 'opacity-0' : ''"></span> {{ currentChildIndex + 1 }} / {{ messageCount }}
</button> </span>
</Tooltip> <button data-action="next-child" :data-hotkey="currentChildIndex + 1 < messageCount ? 'alt,]' : undefined"
<span class="text-xs text-[var(--text-secondary)]"> class="@hover:bg-[var(--color-hover)] rounded transition duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
{{ currentChildIndex + 1 }} / {{ messageCount }} <span class="i-mynaui-chevron-right w-4 h-4 text-[var(--text-secondary)] pointer-events-none"
</span> :class="currentChildIndex + 1 === messageCount ? 'opacity-0' : ''"></span>
<Tooltip :inert="currentChildIndex + 1 === messageCount" :hotkey="['alt', ']']"> </button>
<button @click="navigateChild(1)"
class="@hover:bg-[var(--color-hover)] rounded transition duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<span class="i-mynaui-chevron-right w-4 h-4 text-[var(--text-secondary)]"
:class="currentChildIndex + 1 === messageCount ? 'opacity-0' : ''"></span>
</button>
</Tooltip>
</div>
</div> </div>
<div v-show="activeMessage.generation?.status !== 'pending'" <div v-show="activeMessage.generation?.status !== 'pending'"
class="self-end mt-1 w-fit bg-[var(--bg-container)] text-[var(--text-secondary)] gap-px flex items-center rounded-md overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"> @mouseover="handleTooltipHover" @mouseout="handleTooltipLeave"
<Tooltip :hotkey="['ctrl', 'shift', 'enter']"> class="ml-auto self-end mt-1 w-fit bg-[var(--bg-container)] text-[var(--text-secondary)] gap-px flex items-center rounded-md overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<button @click="regenerateMessage" <button data-action="regenerate" data-hotkey="ctrl,shift,enter"
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
<span class="i-mynaui-refresh text-4.5"></span>
</button>
</Tooltip>
<button v-if="message.role === 'user'" @click="handleEdit"
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]"> class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
<span class="i-mynaui-pencil text-4.5"></span> <span class="i-mynaui-refresh text-4.5 pointer-events-none"></span>
</button> </button>
<button @click="copyMessage" :class="{ 'text-emerald-500': copied }" <button v-if="message.role === 'user'" data-action="edit"
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]"> class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
<span :class="copied ? 'i-mynaui-check text-emerald-500' : 'i-mynaui-copy text-4.5'"></span> <span class="i-mynaui-pencil text-4.5 pointer-events-none"></span>
</button>
<button data-action="copy" :class="{ 'text-emerald-500': copied }"
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
<span :class="copied ? 'i-mynaui-check text-emerald-500' : 'i-mynaui-copy text-4.5'" class="pointer-events-none"></span>
</button>
<button data-action="fork"
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
<span class="i-mynaui-git-branch text-4.5 pointer-events-none"></span>
</button>
<button data-action="delete" data-hotkey="ctrl,shift,backspace"
class="flex justify-center items-center w-7 h-6 text-red-500 @hover:bg-[var(--color-hover)]">
<span class="i-mynaui-trash text-5 pointer-events-none"></span>
</button> </button>
<Tooltip :hotkey="['ctrl', 'shift', 'backspace']">
<button @click="deleteMessage"
class="flex justify-center items-center w-7 h-6 text-red-500 @hover:bg-[var(--color-hover)]">
<span class="i-mynaui-trash text-5"></span>
</button>
</Tooltip>
</div> </div>
</div> </div>
</div> </div>
+4 -83
View File
@@ -1,39 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { DialogType } from '~/composables/useDialog'; defineProps<{ enabled: boolean }>();
const { openDialog } = await useDialog();
const props = defineProps<{
enabled: boolean;
maxResults: number;
rerank: boolean;
}>();
const emit = defineEmits<{ const emit = defineEmits<{
'update:enabled': [value: boolean]; 'update:enabled': [value: boolean];
'update:maxResults': [value: number];
'update:rerank': [value: boolean];
}>(); }>();
const adjustMaxResults = (delta: number) => {
const next = Math.min(50, Math.max(1, props.maxResults + delta));
emit('update:maxResults', next);
};
const { settings } = await useUserSettings();
const systemAssistantsRerank = computed(() => {
return settings.value.systemAssistants?.rerank ?? null;
});
const isRerankConfigured = computed(() => {
const sa = systemAssistantsRerank.value;
return sa?.enabled === true && sa?.modelId != null && sa.modelId !== '';
});
const openSettings = () => {
openDialog(DialogType.Settings, undefined, { page: 'systemAssistants' });
};
</script> </script>
<template> <template>
@@ -46,10 +16,8 @@ const openSettings = () => {
</button> </button>
</template> </template>
<template #dropdown="{ close }"> <template #dropdown>
<div class="flex p-1 gap-4"> <div class="flex flex-col gap-1 p-1">
<!-- Left: mode toggle group -->
<div class="flex flex-col gap-1">
<button @click="emit('update:enabled', false)" <button @click="emit('update:enabled', false)"
class="flex items-start gap-3 rounded-xl px-3 py-2.5 text-left transition-colors duration-150" class="flex items-start gap-3 rounded-xl px-3 py-2.5 text-left transition-colors duration-150"
:class="!enabled :class="!enabled
@@ -83,57 +51,10 @@ const openSettings = () => {
</span> </span>
<span class="text-xs leading-snug" <span class="text-xs leading-snug"
:class="enabled ? 'text-[var(--text-secondary)]' : 'text-[var(--text-dim)]'"> :class="enabled ? 'text-[var(--text-secondary)]' : 'text-[var(--text-dim)]'">
Search the web automatically when needed Search the web and fetch URLs when needed
</span> </span>
</div> </div>
</button> </button>
</div>
<!-- Right: settings (only when enabled) -->
<div v-if="enabled" class="flex flex-col gap-3 border-l border-[var(--color-border)] pl-4">
<!-- Max results stepper -->
<div class="flex flex-col gap-1.5">
<span class="text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Max results
</span>
<div class="flex items-center gap-2">
<button @click="adjustMaxResults(-1)"
class="h-7 w-7 flex items-center justify-center rounded-lg @hover:bg-[var(--color-hover)] text-[var(--text-secondary)] @hover:text-[var(--text-primary)] transition-colors duration-150">
<span class="text-sm i-mynaui-minus"></span>
</button>
<span class="w-8 text-center text-sm font-medium tabular-nums text-[var(--text-primary)]">
{{ maxResults }}
</span>
<button @click="adjustMaxResults(1)"
class="h-7 w-7 flex items-center justify-center rounded-lg @hover:bg-[var(--color-hover)] text-[var(--text-secondary)] @hover:text-[var(--text-primary)] transition-colors duration-150">
<span class="text-sm i-mynaui-plus-solid"></span>
</button>
</div>
</div>
<!-- Rerank toggle -->
<div class="flex flex-col gap-1.5">
<span class="text-xs font-medium text-[var(--color-tertiary)] uppercase tracking-wider">
Rerank
</span>
<div v-if="!isRerankConfigured"
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[var(--bg-container)] border border-[var(--color-border)]">
<button @click="() => { openSettings(); close() }"
class="flex items-center gap-2 text-xs text-[var(--text-dim)] hover:text-[var(--text-primary)] transition-colors duration-150">
<span class="i-mynaui-sparkles text-3.5"></span>
<span>Setup reranking</span>
<span class="i-mynaui-arrow-right text-3 ml-auto"></span>
</button>
</div>
<label v-else
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[var(--bg-container)] border border-[var(--color-border)] cursor-pointer @hover:border-[var(--color-hover)] transition-colors duration-150">
<input type="checkbox" :checked="props.rerank"
@change="emit('update:rerank', ($event.target as HTMLInputElement).checked)"
class="w-4 h-4 rounded border-[var(--color-border)] bg-transparent text-[var(--color-accent)] focus:ring-[var(--color-accent)] focus:ring-offset-0 cursor-pointer" />
<span class="text-xs text-[var(--text-secondary)]">Rerank results</span>
</label>
</div>
</div>
</div> </div>
</template> </template>
</Dropdown> </Dropdown>
+13 -17
View File
@@ -1,18 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ModelWithProvider } from '~/composables/useModels'; import type { ModelWithProvider } from '~/composables/useModels';
const props = defineProps<{
params?: string;
}>();
const { providers, allModels } = await useModels(); const { providers, allModels } = await useModels();
const { settings, updateSettings } = await useUserSettings(); const { settings, updateSettings } = await useUserSettings();
const toggle = async (key: string) => { const toggle = async () => {
const current = (settings.value.systemAssistants as any)[key]; const current = settings.value.systemAssistants.rename;
await updateSettings({ await updateSettings({
systemAssistants: { systemAssistants: {
[key]: { rename: {
...current, ...current,
enabled: !current.enabled enabled: !current.enabled
} }
@@ -20,11 +16,11 @@ const toggle = async (key: string) => {
}); });
}; };
const updateModel = async (key: string, model: ModelWithProvider | undefined | null) => { const updateModel = async (model: ModelWithProvider | undefined | null) => {
const current = (settings.value.systemAssistants as any)[key]; const current = settings.value.systemAssistants.rename;
await updateSettings({ await updateSettings({
systemAssistants: { systemAssistants: {
[key]: { rename: {
...current, ...current,
modelId: model?.id ?? null modelId: model?.id ?? null
} }
@@ -44,15 +40,15 @@ defineEmits(['navigate']);
<template> <template>
<div <div
class="flex flex-col gap-4 py-4 flex-grow overflow-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]"> class="flex flex-col gap-4 py-4 flex-grow overflow-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]">
<div class="flex flex-col" v-for="(systemAssistant, key) in settings.systemAssistants" :key="key"> <div class="flex flex-col">
<label :for="`slider-${key}`" class="flex justify-between gap-4 items-center"> <label for="slider-rename" class="flex justify-between gap-4 items-center">
<h4 class="case-capital">{{ key }}</h4> <h4 class="case-capital">Rename</h4>
<Slider :id="`slider-${key}`" :checked="systemAssistant?.enabled ?? false" <Slider id="slider-rename" :checked="settings.systemAssistants.rename?.enabled ?? false"
@click="toggle(String(key))" /> @click="toggle" />
</label> </label>
<div class="flex-1 justify-between gap-4 items-center"> <div class="flex-1 justify-between gap-4 items-center">
<ModelSelector :providers="providers" :model-value="getModel(systemAssistant?.modelId)" <ModelSelector :providers="providers" :model-value="getModel(settings.systemAssistants.rename?.modelId)"
@update:model-value="(model) => updateModel(String(key), model)" /> @update:model-value="updateModel" />
</div> </div>
</div> </div>
</div> </div>
+136 -11
View File
@@ -1,11 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import RowVirtualizerFixed from '~/components/RowVirtualizerFixed.vue'; import RowVirtualizerFixed from '~/components/RowVirtualizerFixed.vue';
import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue'; import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue';
import { DialogType } from '~/composables/useDialog';
import { exportTopicToJson, exportTopicToMarkdown } from '~~/utils/export';
const { openDialog } = useDialog();
const dropdownOpen = ref(false); const dropdownOpen = ref(false);
const dropdownTrigger = ref<HTMLElement | null>(null); const dropdownTrigger = ref<HTMLElement | null>(null);
const dropdownContent = ref(null); const dropdownContent = ref(null);
const activeMenuTopicId = ref<string | null>(null); const activeMenuTopicId = ref<string | null>(null);
const exportSubmenuOpen = ref(false);
const exportTrigger = ref<HTMLElement | null>(null);
const exportContent = ref(null);
const exportInProgress = ref(false);
const route = useRoute(); const route = useRoute();
const { getAgent, patchTopicLocally, deleteTopic: deleteAgentTopic } = await useAgents(); const { getAgent, patchTopicLocally, deleteTopic: deleteAgentTopic } = await useAgents();
@@ -32,16 +39,51 @@ const { floatingStyles, placement, middlewareData } = useFloating(dropdownTrigge
transform: false, transform: false,
}); });
const {
floatingStyles: exportFloatingStyles,
placement: exportPlacement,
middlewareData: exportMiddlewareData,
} = useFloating(exportTrigger, exportContent, {
placement: 'right-start',
whileElementsMounted: autoUpdate,
middleware: [offset(6), flip({ fallbackPlacements: ['left-start', 'right-end', 'left-end'] }), shift({ padding: 10 }), hide()],
transform: false,
});
const transformOrigin = computed(() => const transformOrigin = computed(() =>
placement.value.startsWith('top') placement.value.startsWith('top')
? 'transform-origin-bottom-center' ? 'transform-origin-bottom-center'
: 'transform-origin-top-center' : 'transform-origin-top-center'
); );
const exportTransformOrigin = computed(() => {
switch (exportPlacement.value.split('-')[0]) {
case 'left':
return 'transform-origin-right-center';
case 'right':
return 'transform-origin-left-center';
case 'top':
return 'transform-origin-bottom-center';
case 'bottom':
return 'transform-origin-top-center';
default:
return 'transform-origin-left-center';
}
});
const closeExportSubmenu = () => {
exportSubmenuOpen.value = false;
};
const closeDropdown = () => { const closeDropdown = () => {
dropdownOpen.value = false; dropdownOpen.value = false;
activeMenuTopicId.value = null; activeMenuTopicId.value = null;
dropdownTrigger.value = null; dropdownTrigger.value = null;
closeExportSubmenu();
};
const openExportSubmenu = () => {
exportSubmenuOpen.value = true;
}; };
// Computed to get the topic data for the currently open menu // Computed to get the topic data for the currently open menu
@@ -49,6 +91,35 @@ const menuTopic = computed(() =>
topics.value.find(t => t.id === activeMenuTopicId.value) topics.value.find(t => t.id === activeMenuTopicId.value)
); );
const handleExport = async (format: 'json' | 'markdown') => {
if (!menuTopic.value || exportInProgress.value) return;
exportInProgress.value = true;
try {
if (format === 'json') {
await exportTopicToJson(menuTopic.value.id);
} else {
await exportTopicToMarkdown(menuTopic.value.id);
}
} finally {
exportInProgress.value = false;
closeDropdown();
}
};
const handleDropdownClickOutside = (event?: Event) => {
const target = event?.target as Node | null;
if (target) {
if (exportContent.value && (exportContent.value as HTMLElement).contains(target)) {
return;
}
if (exportTrigger.value && exportTrigger.value.contains(target)) {
return;
}
}
closeDropdown();
};
const topicsOpen = ref(true); const topicsOpen = ref(true);
const autoRenameTopic = async (topicId: string) => { const autoRenameTopic = async (topicId: string) => {
@@ -56,10 +127,12 @@ const autoRenameTopic = async (topicId: string) => {
} }
const cancelAutoRename = async (topicId: string) => { const cancelAutoRename = async (topicId: string) => {
// TODO: make this more optimistic const result = await $fetch(`/api/topic/${topicId}/auto-rename/cancel`, {
await $fetch(`/api/topic/${topicId}/auto-rename/cancel`, { method: "POST",
method: 'POST',
}); });
if (result.ok) {
patchTopicLocally(topicId, { renaming: false });
}
} }
const renameTopicId = ref<string | null>(null); const renameTopicId = ref<string | null>(null);
@@ -133,6 +206,7 @@ const handleNavClick = (e: MouseEvent) => {
activeMenuTopicId.value = topicId; activeMenuTopicId.value = topicId;
dropdownTrigger.value = trigger; dropdownTrigger.value = trigger;
dropdownOpen.value = true; dropdownOpen.value = true;
closeExportSubmenu();
break; break;
} }
} }
@@ -151,6 +225,17 @@ onMounted(() => {
<SidenavItem :to="`/agent/${agentId}/profile`" name="Agent Info" icon="i-mynaui-info-square" <SidenavItem :to="`/agent/${agentId}/profile`" name="Agent Info" icon="i-mynaui-info-square"
:active="route.path.endsWith('/profile')" /> :active="route.path.endsWith('/profile')" />
<SidenavItem @click="openDialog(DialogType.QuickSwitcher)" name="Search" icon="i-mynaui-search">
<div class="flex items-center gap-1">
<span class="flex bg-[var(--bg-container)] px-1 rounded border border-[var(--color-border)]">
<kbd class="font-mono text-[10px] case-capital">ctrl</kbd>
</span>
<span class="flex bg-[var(--bg-container)] px-1 rounded border border-[var(--color-border)]">
<kbd class="font-mono text-[10px] case-capital">k</kbd>
</span>
</div>
</SidenavItem>
<!-- Topics Section --> <!-- Topics Section -->
<button @click="topicsOpen = !topicsOpen" <button @click="topicsOpen = !topicsOpen"
class="flex items-center justify-between gap-2 px-2 py-2 rounded-lg bg-transparent @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] transition-colors w-full text-left"> class="flex items-center justify-between gap-2 px-2 py-2 rounded-lg bg-transparent @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] transition-colors w-full text-left">
@@ -173,12 +258,13 @@ onMounted(() => {
v-model="newTopicName" @keydown.enter="saveRename" @keydown.escape="cancelRename" v-model="newTopicName" @keydown.enter="saveRename" @keydown.escape="cancelRename"
@blur="saveRename" @blur="saveRename"
class="flex-1 bg-transparent border-none outline-none md:text-sm font-medium text-[var(--text-primary)] px-0 min-w-0" /> class="flex-1 bg-transparent border-none outline-none md:text-sm font-medium text-[var(--text-primary)] px-0 min-w-0" />
<div v-else-if="topic.renaming" class="flex w-full"> <div v-else class="flex items-center gap-2 min-w-0">
<span class="i-svg-spinners-3-dots-fade text-7 md:text-6"></span> <span class="md:text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap">
{{ topic.name }}
</span>
<span v-if="topic.renaming" aria-label="Renaming"
class="i-svg-spinners-3-dots-fade shrink-0 text-4"></span>
</div> </div>
<span v-else class="md:text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap">
{{ topic.name }}
</span>
<div data-action="toggle-dropdown" <div data-action="toggle-dropdown"
class="text-[var(--text-secondary)] shrink-0 w-0 max-w-0 opacity-0 overflow-hidden p-1 max-md:w-auto max-md:max-w-none max-md:opacity-100 flex items-center justify-center rounded-md @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)]" class="text-[var(--text-secondary)] shrink-0 w-0 max-w-0 opacity-0 overflow-hidden p-1 max-md:w-auto max-md:max-w-none max-md:opacity-100 flex items-center justify-center rounded-md @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)]"
@@ -198,36 +284,49 @@ onMounted(() => {
leave-active-class="transition-[opacity,transform] duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]" leave-active-class="transition-[opacity,transform] duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
leave-from-class="opacity-100 scale-100 translate-y-0" leave-from-class="opacity-100 scale-100 translate-y-0"
leave-to-class="opacity-0 scale-95 translate-y-1"> leave-to-class="opacity-0 scale-95 translate-y-1">
<div v-if="dropdownOpen" ref="dropdownContent" :style="{ <div v-if="dropdownOpen" ref="dropdownContent" v-click-outside="handleDropdownClickOutside" :style="{
...floatingStyles, ...floatingStyles,
visibility: middlewareData.hide?.referenceHidden visibility: middlewareData.hide?.referenceHidden
? 'hidden' ? 'hidden'
: 'visible', : 'visible',
}" class="fixed z-20" :class="transformOrigin"> }" class="fixed z-20" :class="transformOrigin">
<div v-click-outside="closeDropdown" <div
class="bg-[var(--bg-surface)] border border-[var(--color-border)] rounded-xl p-1.5 shadow-xl flex flex-col gap-1 min-w-40"> class="bg-[var(--bg-surface)] border border-[var(--color-border)] rounded-xl p-1.5 shadow-xl flex flex-col gap-1 min-w-40">
<!-- Dynamic content based on menuTopic --> <!-- Dynamic content based on menuTopic -->
<template v-if="menuTopic"> <template v-if="menuTopic">
<button v-if="menuTopic.renaming" @click="cancelAutoRename(menuTopic.id); closeDropdown()" <button v-if="menuTopic.renaming" @click="cancelAutoRename(menuTopic.id); closeDropdown()"
@mouseenter="closeExportSubmenu"
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50"> class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50">
Cancel Auto Rename Cancel Auto Rename
</button> </button>
<button v-else @click="autoRenameTopic(menuTopic.id); closeDropdown()" <button v-else @click="autoRenameTopic(menuTopic.id); closeDropdown()"
@mouseenter="closeExportSubmenu"
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50"> class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50">
Auto Rename Auto Rename
</button> </button>
<button :disabled="menuTopic.renaming ?? false" <button :disabled="menuTopic.renaming ?? false"
@click="startRename(menuTopic.id, menuTopic.name); closeDropdown()" @click="startRename(menuTopic.id, menuTopic.name); closeDropdown()"
@mouseenter="closeExportSubmenu"
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50"> class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50">
Rename Rename
</button> </button>
<button ref="exportTrigger" @click.stop="exportSubmenuOpen = !exportSubmenuOpen"
@mouseenter="openExportSubmenu"
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50 flex items-center justify-between gap-3"
:class="{ 'bg-[var(--color-hover)]': exportSubmenuOpen }"
:disabled="exportInProgress">
<span>Export</span>
<span class="i-mynaui-chevron-right text-4 shrink-0"
:class="exportPlacement.startsWith('left') ? 'rotate-180' : ''"></span>
</button>
<div class="h-px bg-[var(--color-border)] my-1" /> <div class="h-px bg-[var(--color-border)] my-1" />
<button @click="deleteTopic(menuTopic.id); closeDropdown()" <button @click="deleteTopic(menuTopic.id); closeDropdown()"
@mouseenter="closeExportSubmenu"
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50 text-red-500"> class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50 text-red-500">
Delete Delete
</button> </button>
@@ -235,6 +334,32 @@ onMounted(() => {
</div> </div>
</div> </div>
</Transition> </Transition>
<Transition
enter-active-class="transition-[opacity,transform] duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
enter-from-class="opacity-0 scale-95" enter-to-class="opacity-100 scale-100"
leave-active-class="transition-[opacity,transform] duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
<div v-if="dropdownOpen && exportSubmenuOpen" ref="exportContent" :style="{
...exportFloatingStyles,
visibility: exportMiddlewareData.hide?.referenceHidden
? 'hidden'
: 'visible',
}" class="fixed z-30" :class="exportTransformOrigin"
@mouseenter="openExportSubmenu">
<div
class="bg-[var(--bg-surface)] border border-[var(--color-border)] rounded-xl p-1.5 shadow-xl flex flex-col gap-1 min-w-44">
<button :disabled="exportInProgress" @click="handleExport('json')"
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50 whitespace-nowrap">
Export to JSON
</button>
<button :disabled="exportInProgress" @click="handleExport('markdown')"
class="text-left px-3 py-1.5 text-sm rounded-lg @hover:bg-[var(--color-hover)] transition-colors disabled:opacity-50 whitespace-nowrap">
Export to Markdown
</button>
</div>
</div>
</Transition>
</Teleport> </Teleport>
</nav> </nav>
</template> </template>
+45
View File
@@ -0,0 +1,45 @@
<script setup lang="ts">
const props = defineProps<{
python: boolean;
}>();
const emit = defineEmits<{
'update:python': [value: boolean];
}>();
const anyEnabled = computed(() => props.python);
</script>
<template>
<Dropdown dropdownClass="text-sm" placement="top">
<template #default="{ toggle, setRef }">
<button :ref="setRef" @click="toggle"
class="flex items-center justify-center h-8.5 w-8.5 @hover:bg-[var(--color-hover)] rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<span class="pointer-events-none i-mynaui-archive text-5 transition-colors duration-200"
:class="anyEnabled ? 'text-[var(--color-accent)]' : 'text-[var(--text-secondary)]'"></span>
</button>
</template>
<template #dropdown>
<div class="flex flex-col gap-1 p-1 min-w-48">
<label
class="flex items-start gap-3 rounded-xl px-3 py-2.5 text-left cursor-pointer transition-colors duration-150 @hover:bg-[var(--color-hover)]"
:class="python ? 'bg-[var(--color-active)]' : ''">
<input type="checkbox" :checked="python"
@change="emit('update:python', ($event.target as HTMLInputElement).checked)"
class="mt-0.5 w-4 h-4 rounded border-[var(--color-border)] bg-transparent text-[var(--color-accent)] focus:ring-[var(--color-accent)] focus:ring-offset-0 cursor-pointer shrink-0" />
<div class="flex flex-col gap-0.5">
<span class="text-sm font-medium"
:class="python ? 'text-[var(--text-primary)]' : 'text-[var(--text-secondary)]'">
Python
</span>
<span class="text-xs leading-snug"
:class="python ? 'text-[var(--text-secondary)]' : 'text-[var(--text-dim)]'">
Run code in a sandbox
</span>
</div>
</label>
</div>
</template>
</Dropdown>
</template>
+10 -7
View File
@@ -1,20 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps<{ const props = defineProps<{
hotkey?: string[]; hotkey?: string[];
text?: string;
}>(); }>();
const { show, hide } = useTooltip(); const { show, hide } = useTooltip();
const triggerRef = ref<HTMLElement | null>(null); const triggerRef = ref<HTMLElement | null>(null);
const hasCursor = ref(false); let hasCursor: boolean | null = null;
const checkHasCursor = () => {
onMounted(() => { if (hasCursor === null && typeof window !== 'undefined') {
hasCursor.value = window.matchMedia('(hover: hover) and (pointer: fine)').matches; hasCursor = window.matchMedia('(hover: hover) and (pointer: fine)').matches;
}); }
return hasCursor ?? false;
};
const onMouseEnter = () => { const onMouseEnter = () => {
if (triggerRef.value && hasCursor.value) { if (triggerRef.value && checkHasCursor()) {
show(triggerRef.value, props.hotkey); show(triggerRef.value, props.hotkey, props.text);
} }
}; };
</script> </script>
+3 -2
View File
@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue'; import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue';
const { activeElement, activeHotkey, isVisible } = useTooltip(); const { activeElement, activeHotkey, activeText, isVisible } = useTooltip();
const tooltipRef = ref<HTMLElement | null>(null); const tooltipRef = ref<HTMLElement | null>(null);
@@ -61,7 +61,8 @@ const transformOrigin = computed(() => placement.value.includes('top') ? 'transf
} }
]" :class="transformOrigin" ]" :class="transformOrigin"
class="pointer-events-none fixed z-35 flex flex-col bg-[var(--bg-surface)] rounded-lg shadow-xl px-1.5 py-1 text-xs"> class="pointer-events-none fixed z-35 flex flex-col bg-[var(--bg-surface)] rounded-lg shadow-xl px-1.5 py-1 text-xs">
<div class="flex gap-1 items-center"> <span v-if="activeText" class="max-w-xs whitespace-normal">{{ activeText }}</span>
<div v-if="displayHotkey?.length" class="flex gap-1 items-center">
<span v-for="key in displayHotkey" :key="key" <span v-for="key in displayHotkey" :key="key"
class="bg-[var(--bg-container)] px-1 rounded border border-[var(--color-border)]"> class="bg-[var(--bg-container)] px-1 rounded border border-[var(--color-border)]">
<kbd class="font-mono text-[10px] case-capital">{{ key }}</kbd> <kbd class="font-mono text-[10px] case-capital">{{ key }}</kbd>
+39 -2
View File
@@ -1,4 +1,6 @@
import type { BaseMessage } from "~/composables/useChat";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { provisionalTopicTitle } from "~~/utils/topic-title";
import { attempt } from "~~/types/result"; import { attempt } from "~~/types/result";
import * as schema from '~~/drizzle/schema'; import * as schema from '~~/drizzle/schema';
@@ -153,11 +155,11 @@ export const useAgents = async () => {
// TODO: since topics are contained within each agent struct, all topic // TODO: since topics are contained within each agent struct, all topic
// actions must be done through the agents composable // actions must be done through the agents composable
const createTopic = async (agentId: string) => { const createTopic = async (agentId: string, message?: BaseMessage) => {
const topicId = nanoid(); const topicId = nanoid();
const topic = { const topic = {
id: topicId, id: topicId,
name: 'New Topic', name: provisionalTopicTitle(message),
agentId, agentId,
}; };
@@ -198,11 +200,46 @@ export const useAgents = async () => {
}); });
} }
const forkTopic = async (topicId: string, messageId: string): Promise<string | null> => {
try {
const result = await $fetch<{ ok: boolean; topicId: string; name: string }>(`/api/topic/${topicId}/fork`, {
method: 'POST',
body: { messageId },
});
if (result.ok) {
const agent = agents.value.find(a => a.topics.some(t => t.id === topicId));
if (agent) {
const newTopic: Topic = {
id: result.topicId,
name: result.name,
agentId: agent.id,
userId: agent.userId,
renaming: false,
renameAttemptId: null,
renameExpiresAt: null,
titleSource: "manual",
createdAt: new Date(),
};
agents.value = agents.value.map(a =>
a.id === agent.id ? { ...a, topics: [newTopic, ...a.topics] } as AgentWithTopics : a
);
}
return result.topicId;
}
return null;
} catch (error) {
console.error('Failed to fork topic:', error);
return null;
}
};
return { return {
agents, agents,
refresh, refresh,
createAgent, createAgent,
createTopic, createTopic,
forkTopic,
getAgent, getAgent,
patchAgentLocally, patchAgentLocally,
patchTopicLocally, patchTopicLocally,
+263 -48
View File
@@ -3,80 +3,295 @@ import { ref, watch, onUnmounted, type Ref } from 'vue';
export function useAutoScroll(elementRef: Ref<HTMLElement | null>, options: { export function useAutoScroll(elementRef: Ref<HTMLElement | null>, options: {
threshold?: number; threshold?: number;
} = {}) { } = {}) {
const { threshold = 80 } = options; const { threshold = 30 } = options;
const isUserScrollingUp = ref(false); /** User intentionally left the bottom — stop following new content */
const unhooked = ref(false);
/** Back-compat alias for UI that keys off "user scrolled away" */
const isUserScrollingUp = unhooked;
const shouldAutoScroll = ref(true); const shouldAutoScroll = ref(true);
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => { let isProgrammatic = false;
let lastScrollTop = 0;
let mutationRaf: number | null = null;
let anchorRaf: number | null = null;
let anchorDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
let observer: MutationObserver | null = null;
let resizeObserver: ResizeObserver | null = null;
let lastWidth = 0;
let anchor: { element: Element; offsetFromTop: number } | null = null;
const isAtBottom = (el: HTMLElement) =>
el.scrollHeight - el.scrollTop - el.clientHeight <= threshold;
const setUnhooked = (value: boolean) => {
unhooked.value = value;
shouldAutoScroll.value = !value;
if (!value) anchor = null;
};
const scrollToBottom = (behavior: ScrollBehavior = 'instant') => {
const el = elementRef.value; const el = elementRef.value;
if (!el) return; if (!el) {
return;
}
const bottom = Math.max(0, el.scrollHeight - el.clientHeight);
if (Math.abs(el.scrollTop - bottom) < 0.5) {
setUnhooked(false);
lastScrollTop = el.scrollTop;
return;
}
isProgrammatic = true;
el.scrollTo({ el.scrollTo({
top: el.scrollHeight, top: bottom,
behavior, behavior,
}); });
shouldAutoScroll.value = true;
requestAnimationFrame(() => {
isProgrammatic = false;
if (elementRef.value && isAtBottom(elementRef.value)) {
setUnhooked(false);
lastScrollTop = elementRef.value.scrollTop;
}
});
};
const captureAnchor = (el: HTMLElement) => {
if (!unhooked.value) {
anchor = null;
return;
}
const rect = el.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const offsets = [4, 24, 48, 80, 120];
for (const y of offsets) {
const target = document.elementFromPoint(x, rect.top + y);
if (!target || target === el || !el.contains(target)) {
continue;
}
anchor = {
element: target,
offsetFromTop: target.getBoundingClientRect().top - rect.top,
};
return;
}
anchor = null;
};
const requestAnchorCapture = () => {
if (!unhooked.value || anchorRaf !== null) return;
anchorRaf = requestAnimationFrame(() => {
anchorRaf = null;
const el = elementRef.value;
if (el) captureAnchor(el);
});
};
const scheduleAnchorCapture = () => {
const el = elementRef.value;
// Native scrollend is the primary path; debounce is only a fallback.
if (!el || 'onscrollend' in el || !unhooked.value) return;
if (anchorDebounceTimeout !== null) {
clearTimeout(anchorDebounceTimeout);
}
anchorDebounceTimeout = setTimeout(() => {
anchorDebounceTimeout = null;
requestAnchorCapture();
}, 150);
};
const handleScrollEnd = () => {
if (anchorDebounceTimeout !== null) {
clearTimeout(anchorDebounceTimeout);
anchorDebounceTimeout = null;
}
requestAnchorCapture();
};
const restoreAnchor = (el: HTMLElement) => {
if (!anchor || !el.contains(anchor.element)) {
return;
}
const top = el.getBoundingClientRect().top;
const delta =
anchor.element.getBoundingClientRect().top - top - anchor.offsetFromTop;
if (Math.abs(delta) < 0.5) {
return;
}
isProgrammatic = true;
el.scrollTop += delta;
requestAnimationFrame(() => {
isProgrammatic = false;
});
};
const handleWheel = (event: WheelEvent) => {
isProgrammatic = false;
// Intent to leave bottom — unhook before content mutations can re-stick
if (event.deltaY < 0) {
setUnhooked(true);
}
}; };
const handleScroll = () => { const handleScroll = () => {
const el = elementRef.value; const el = elementRef.value;
if (!el) return; if (!el) {
return;
const { scrollTop, scrollHeight, clientHeight } = el;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
if (distanceFromBottom <= threshold) {
if (isUserScrollingUp.value) {
isUserScrollingUp.value = false;
shouldAutoScroll.value = true;
}
} else {
isUserScrollingUp.value = true;
shouldAutoScroll.value = false;
} }
const scrollingUp = el.scrollTop < lastScrollTop;
lastScrollTop = el.scrollTop;
if (isProgrammatic) {
if (isAtBottom(el)) {
setUnhooked(false);
}
return;
}
// If the user is scrolling up, they are deliberately navigating away from the bottom.
// Unhook immediately so we never fight the scroll or snap back down.
if (scrollingUp) {
setUnhooked(true);
scheduleAnchorCapture();
return;
}
// Only re-hook when scrolling down into the bottom threshold.
if (isAtBottom(el)) {
setUnhooked(false);
return;
}
scheduleAnchorCapture();
}; };
let observer: MutationObserver | null = null; const followIfHooked = () => {
let timeout: NodeJS.Timeout | null = null; if (unhooked.value) {
return;
watch(elementRef, (newEl, oldEl) => {
if (oldEl) {
oldEl.removeEventListener('scroll', handleScroll);
observer?.disconnect();
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
} }
scrollToBottom('instant');
};
if (newEl) { const handleMutation = (records: MutationRecord[]) => {
newEl.addEventListener('scroll', handleScroll, { passive: true }); if (unhooked.value || mutationRaf !== null) {
return;
}
// The isolated sizing mirror changes on every keystroke, but cannot
// change scroll height. Keep observing real composer content (files,
// tool controls, etc.) as well as message content.
if (records.every(record => {
const target = record.target instanceof Element ? record.target : record.target.parentElement;
return target?.closest('[data-chat-measurement]');
})) return;
mutationRaf = requestAnimationFrame(() => {
mutationRaf = null;
followIfHooked();
});
};
observer = new MutationObserver(() => { const attach = (el: HTMLElement) => {
// Only auto-scroll if user hasn't scrolled up and is near bottom el.style.overflowAnchor = 'none';
if (!isUserScrollingUp.value && shouldAutoScroll.value) { lastScrollTop = el.scrollTop;
lastWidth = el.clientWidth;
setUnhooked(false);
el.addEventListener('wheel', handleWheel, { passive: true });
el.addEventListener('scroll', handleScroll, { passive: true });
el.addEventListener('scrollend', handleScrollEnd, { passive: true });
observer = new MutationObserver(handleMutation);
observer.observe(el, {
childList: true,
subtree: true,
characterData: true,
});
resizeObserver = new ResizeObserver(() => {
const current = elementRef.value;
if (!current) {
return;
}
const width = current.clientWidth;
if (width === lastWidth) {
// Height-only growth (streaming layout): stay stuck if hooked
if (!unhooked.value) {
scrollToBottom('instant'); scrollToBottom('instant');
} }
}); return;
}
observer.observe(newEl, { lastWidth = width;
childList: true,
subtree: true, if (!unhooked.value) {
characterData: true, scrollToBottom('instant');
}); return;
}
if (!anchor) {
captureAnchor(current);
}
restoreAnchor(current);
});
resizeObserver.observe(el);
// Content already taller than the viewport on mount — catch up once
requestAnimationFrame(() => {
if (!unhooked.value) {
scrollToBottom('instant');
}
});
};
const detach = (el: HTMLElement | null) => {
if (el) {
el.removeEventListener('wheel', handleWheel);
el.removeEventListener('scroll', handleScroll);
el.removeEventListener('scrollend', handleScrollEnd);
} }
}); observer?.disconnect();
observer = null;
resizeObserver?.disconnect();
resizeObserver = null;
if (mutationRaf !== null) {
cancelAnimationFrame(mutationRaf);
mutationRaf = null;
}
if (anchorRaf !== null) {
cancelAnimationFrame(anchorRaf);
anchorRaf = null;
}
if (anchorDebounceTimeout !== null) {
clearTimeout(anchorDebounceTimeout);
anchorDebounceTimeout = null;
}
anchor = null;
};
watch(elementRef, (newEl, oldEl) => {
detach(oldEl ?? null);
if (newEl) {
attach(newEl);
}
}, { immediate: true });
onUnmounted(() => { onUnmounted(() => {
elementRef.value?.removeEventListener('scroll', handleScroll); detach(elementRef.value);
observer?.disconnect();
if (timeout) {
clearTimeout(timeout);
}
}); });
return { return {
scrollToBottom, scrollToBottom,
isUserScrollingUp, // expose for UI feedback (optional) isUserScrollingUp,
shouldAutoScroll,
isAtBottom: () => {
const el = elementRef.value;
return el ? isAtBottom(el) : true;
},
}; };
} }
+276 -105
View File
@@ -1,11 +1,20 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { toRaw } from 'vue';
import * as schema from '~~/drizzle/schema'; import * as schema from '~~/drizzle/schema';
import { Err, Ok, type Result } from '~~/types/result'; import { Err, Ok, type Result } from '~~/types/result';
import { buildMessageTree } from '~~/utils/message'; import { buildMessageTree } from '~~/utils/message';
import { appendTextDelta, serializeTopicSnapshot } from '~~/utils/topic-sync';
export type BaseMessage = { export type BaseMessage = {
content: string; content: string;
fileIds: string[]; fileIds: string[];
files?: {
id: string;
name: string;
mimeType: string;
url: string;
size?: number;
}[];
} }
export type ToolCall = typeof schema.toolCalls.$inferSelect export type ToolCall = typeof schema.toolCalls.$inferSelect
@@ -31,7 +40,7 @@ export enum ChatErrorType {
Unimplemented, Unimplemented,
} }
type Event = { type: string; payload: any; timestamp: number }; type Event = { type: string; payload: any; timestamp: number; replay?: boolean };
const TARGET_UPDATES_PER_SECOND = 24; const TARGET_UPDATES_PER_SECOND = 24;
@@ -39,6 +48,10 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
let data = useState<Topic & { messages: Message[] } | undefined>('useChat:data', () => undefined); let data = useState<Topic & { messages: Message[] } | undefined>('useChat:data', () => undefined);
let sse: EventSource | undefined; let sse: EventSource | undefined;
let disposed = false;
let connectionAttempt = 0;
let reconnectTimeout: ReturnType<typeof setTimeout> | undefined;
const { promise: connectionReady, resolve: markConnected } = Promise.withResolvers<void>();
const textDeltaBuffer: Map<string, Map<string, { const textDeltaBuffer: Map<string, Map<string, {
content: string; content: string;
@@ -57,7 +70,7 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
for (const [partId, content] of parts) { for (const [partId, content] of parts) {
const part = msg.parts?.find(p => p.id === partId); const part = msg.parts?.find(p => p.id === partId);
if (part) { if (part) {
part.content += content.content; part.content = content.content;
part.lastUpdatedAt = new Date(content.lastUpdatedAt); part.lastUpdatedAt = new Date(content.lastUpdatedAt);
} }
} }
@@ -86,6 +99,23 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
flushTimeout = setTimeout(flushTextDeltas, 1000 / TARGET_UPDATES_PER_SECOND); flushTimeout = setTimeout(flushTextDeltas, 1000 / TARGET_UPDATES_PER_SECOND);
}; };
const resetTextDeltas = () => {
if (flushTimeout !== undefined) clearTimeout(flushTimeout);
flushTimeout = undefined;
textDeltaBuffer.clear();
flushCbs.clear();
};
const scheduleReconnect = (forceSnapshot = false) => {
sse?.close();
sse = undefined;
if (disposed || reconnectTimeout !== undefined) return;
reconnectTimeout = setTimeout(() => {
reconnectTimeout = undefined;
void connectSSE(forceSnapshot);
}, 1500);
};
const processEvent = (event: Event) => { const processEvent = (event: Event) => {
if (!data.value) return; if (!data.value) return;
@@ -95,12 +125,17 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
case 'MESSAGE_CREATED': { case 'MESSAGE_CREATED': {
// Push the new message if it doesn't exist (prevents duplicates from HTTP vs SSE) // Push the new message if it doesn't exist (prevents duplicates from HTTP vs SSE)
if (!data.value.messages.find(m => m.id === payload.id)) { if (!data.value.messages.find(m => m.id === payload.id)) {
data.value.messages.push({ ...payload, parts: [] }); data.value.messages.push({ ...payload, parts: payload.parts ?? [], attachments: payload.attachments ?? [] });
} else { } else {
// replace the existing data with the new data // replace the existing data with the new data
const index = data.value.messages.findIndex(m => m.id === payload.id); const index = data.value.messages.findIndex(m => m.id === payload.id);
if (index !== -1) { if (index !== -1 && !event.replay) {
data.value.messages[index] = { ...payload, parts: [] }; const existing = data.value.messages[index]!;
data.value.messages[index] = {
...payload,
parts: payload.parts ?? [],
attachments: payload.attachments ?? existing.attachments ?? [],
};
} }
} }
break; break;
@@ -123,7 +158,9 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
const msg = data.value.messages.find(m => m.id === payload.messageId); const msg = data.value.messages.find(m => m.id === payload.messageId);
if (msg) { if (msg) {
if (!msg.parts) msg.parts = []; if (!msg.parts) msg.parts = [];
msg.parts.push(payload.part); if (!msg.parts.some(part => part.id === payload.part.id)) {
msg.parts.push(payload.part);
}
} }
break; break;
} }
@@ -131,25 +168,42 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
case 'text-delta': { case 'text-delta': {
const msg = data.value.messages.find(m => m.id === payload.messageId); const msg = data.value.messages.find(m => m.id === payload.messageId);
if (!msg) break; if (!msg) break;
const part = msg.parts?.find(part => part.id === payload.partId);
if (!part || part.finished) break;
if (!textDeltaBuffer.has(payload.messageId)) { if (!textDeltaBuffer.has(payload.messageId)) {
textDeltaBuffer.set(payload.messageId, new Map()); textDeltaBuffer.set(payload.messageId, new Map());
} }
const messageBuffer = textDeltaBuffer.get(payload.messageId)!; const messageBuffer = textDeltaBuffer.get(payload.messageId)!;
const existing = messageBuffer.get(payload.partId)?.content || ''; const existing = messageBuffer.get(payload.partId)?.content ?? part.content ?? '';
messageBuffer.set(payload.partId, { content: existing + payload.content, lastUpdatedAt: payload.lastUpdatedAt }); if (typeof payload.offset === 'number' && payload.offset > existing.length) {
scheduleReconnect(true);
break;
}
const content = appendTextDelta(existing, payload.content, payload.offset);
if (content === existing) break;
messageBuffer.set(payload.partId, { content, lastUpdatedAt: payload.lastUpdatedAt });
scheduleFlush(); scheduleFlush();
break; break;
} }
case 'text-delete': {
const msg = data.value.messages.find(m => m.id === payload.messageId);
if (msg?.parts) msg.parts = msg.parts.filter(part => part.id !== payload.partId);
textDeltaBuffer.get(payload.messageId)?.delete(payload.partId);
break;
}
case 'text-end': { case 'text-end': {
const msg = data.value.messages.find(m => m.id === payload.messageId); const msg = data.value.messages.find(m => m.id === payload.messageId);
const part = msg?.parts?.find(p => p.id === payload.partId); const part = msg?.parts?.find(p => p.id === payload.partId);
if (part) { if (part && part.type !== 'tool-call') {
nextFlush(() => { nextFlush(() => {
part.content = payload.content; part.content = payload.content;
part.lastUpdatedAt = payload.lastUpdatedAt; if (payload.lastUpdatedAt) {
part.lastUpdatedAt = payload.lastUpdatedAt;
}
part.finished = true; part.finished = true;
}); });
} }
@@ -159,8 +213,16 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
case 'tool-call-start': { case 'tool-call-start': {
const msg = data.value.messages.find(m => m.id === payload.messageId); const msg = data.value.messages.find(m => m.id === payload.messageId);
if (msg) { if (msg) {
if (!msg.parts) msg.parts = []; if (!msg.parts) {
msg.parts.push(payload.part); msg.parts = [];
}
const part = { ...payload.part };
if (!part.createdAt) {
part.createdAt = new Date().toISOString();
}
if (!msg.parts.some(existing => existing.id === part.id)) {
msg.parts.push(part);
}
} }
break; break;
} }
@@ -169,10 +231,22 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
const msg = data.value.messages.find(m => m.id === payload.messageId); const msg = data.value.messages.find(m => m.id === payload.messageId);
const part = msg?.parts?.find(p => p.toolCallId === payload.toolCallId); const part = msg?.parts?.find(p => p.toolCallId === payload.toolCallId);
if (part?.toolCall) { if (part?.toolCall) {
if (payload.input) part.toolCall.input = payload.input; if (payload.input) {
if (payload.output) part.toolCall.output = payload.output; part.toolCall.input = payload.input;
if (payload.error) part.toolCall.error = payload.error; }
if (payload.status) part.toolCall.status = payload.status; if (payload.output) {
part.toolCall.output = payload.output;
}
if (payload.error) {
part.toolCall.error = payload.error;
}
if (payload.status) {
part.toolCall.status = payload.status;
if (payload.status === 'completed' || payload.status === 'failed') {
part.lastUpdatedAt = payload.lastUpdatedAt || new Date().toISOString();
part.finished = true;
}
}
} }
break; break;
} }
@@ -202,55 +276,79 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
} }
} }
const connectSSE = async () => { const connectSSE = async (forceSnapshot = false) => {
const { promise, resolve } = Promise.withResolvers<void>(); if (disposed) return;
const attempt = ++connectionAttempt;
const id = unref(topicId); const id = unref(topicId);
const lastUpdate = data.value?.messages.at(-1)?.updatedAt || '0'; sse?.close();
const msgCount = data.value?.messages.length || 0; if (flushTimeout !== undefined) {
clearTimeout(flushTimeout);
if (sse && sse.readyState !== EventSource.CLOSED) { flushTextDeltas();
if (!(new URL(sse.url).pathname.startsWith(`/api/topic/${id}`))) { }
sse.close(); let offeredSnapshot: string | undefined;
} else { const query = new URLSearchParams();
return; if (!forceSnapshot && data.value?.id === id && globalThis.crypto?.subtle) {
try {
offeredSnapshot = serializeTopicSnapshot(toRaw(data.value));
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(offeredSnapshot));
query.set('snapshot', Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join(''));
} catch {
// Fingerprinting is an optimization; a full snapshot remains safe.
offeredSnapshot = undefined;
} }
} }
if (disposed || attempt !== connectionAttempt) return;
sse = new EventSource(`/api/topic/${id}?lastUpdate=${lastUpdate}&count=${msgCount}`); const source = new EventSource(`/api/topic/${id}?${query}`);
sse = source;
source.addEventListener('error', () => {
if (sse === source) scheduleReconnect();
});
sse.addEventListener("error", (e) => { source.addEventListener('message', (e) => {
console.error('sse error', e); if (disposed || sse !== source) return;
setTimeout(connectSSE, 1500);
})
sse.addEventListener("message", (e) => {
const raw = JSON.parse(e.data); const raw = JSON.parse(e.data);
const events: Event[] = Array.isArray(raw) ? raw : [raw]; const events: Event[] = Array.isArray(raw) ? raw : [raw];
for (const { type, payload, timestamp } of events) { for (const event of events) {
const { type, payload } = event;
if (!type || !payload) continue; if (!type || !payload) continue;
if (type === 'initial_state') { if (type === 'synced') {
data.value = payload; // Optimistic edits can occur while the handshake is in flight.
resolve() // Only retain the exact state that we offered to the server.
if (offeredSnapshot !== serializeTopicSnapshot(toRaw(data.value))) {
scheduleReconnect(true);
return;
}
offeredSnapshot = undefined;
markConnected();
continue; continue;
} }
processEvent({ type, payload, timestamp }); if (type === 'initial_state') {
resetTextDeltas();
data.value = payload;
offeredSnapshot = undefined;
markConnected();
continue;
}
processEvent(event);
} }
}); });
await promise; await connectionReady;
} };
if (connect) { if (connect) {
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (flushTimeout) { disposed = true;
clearTimeout(flushTimeout); connectionAttempt++;
flushTextDeltas(); if (reconnectTimeout !== undefined) clearTimeout(reconnectTimeout);
} resetTextDeltas();
markConnected();
if (sse) { if (sse) {
console.log('[SSE] Closing connection'); console.log('[SSE] Closing connection');
sse.close(); sse.close();
@@ -263,12 +361,6 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
const { data: ssrData } = await useFetch<Topic & { messages: Message[] }>(`/api/topic/${id}`); const { data: ssrData } = await useFetch<Topic & { messages: Message[] }>(`/api/topic/${id}`);
data.value = ssrData.value; data.value = ssrData.value;
} else { } else {
// if (data.value === undefined) {
// const id = unref(topicId);
// const { data: ssrData } = await useFetch<Topic & { messages: Message[] }>(`/api/topic/${id}`);
// data.value = ssrData.value;
// }
await connectSSE(); await connectSSE();
} }
} }
@@ -305,6 +397,7 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
const sendMessage = async ( const sendMessage = async (
baseMessage: BaseMessage, baseMessage: BaseMessage,
onRequest?: () => void, onRequest?: () => void,
role: 'user' | 'assistant' = 'user',
): Promise<Result<void, ChatErrorType>> => { ): Promise<Result<void, ChatErrorType>> => {
const { user } = useAuth(); const { user } = useAuth();
if (!user.value) return Err(ChatErrorType.NoUser); if (!user.value) return Err(ChatErrorType.NoUser);
@@ -312,34 +405,94 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
try { try {
const message = { const message = {
id: nanoid(), id: nanoid(),
role: 'user', role,
content: baseMessage.content,
fileIds: baseMessage.fileIds,
} }
await $fetch(`/api/topic/${unref(topicId)}/message`, { await $fetch(`/api/topic/${unref(topicId)}/message`, {
method: 'POST', method: 'POST',
body: { body: {
message message: {
...message,
content: baseMessage.content,
fileIds: baseMessage.fileIds,
}
}, },
onRequest() { onRequest() {
if (data.value) { if (data.value) {
data.value!.messages.push({ switch (role) {
topicId: unref(topicId), case 'user': {
userId: user.value!.id, const optimisticAttachments = baseMessage.files?.map(file => ({
// TODO id: nanoid(),
attachments: [], userId: user.value!.id,
parts: undefined, topicId: unref(topicId),
generation: null, messageId: message.id,
parentMessageId: null, fileId: file.id,
generationId: null, createdAt: new Date(),
activeChildId: null, file: {
deleted: null, id: file.id,
createdAt: new Date(), userId: user.value!.id,
updatedAt: new Date(), name: file.name,
children: [], mimeType: file.mimeType,
...message size: file.size ?? 0,
} as Message); url: file.url,
createdAt: new Date(),
},
})) ?? [];
data.value!.messages.push({
topicId: unref(topicId),
userId: user.value!.id,
attachments: optimisticAttachments,
parts: undefined,
generation: null,
parentMessageId: null,
generationId: null,
activeChildId: null,
children: [],
deleted: false,
content: baseMessage.content,
createdAt: new Date(),
updatedAt: new Date(),
...message
});
break;
}
case 'assistant': {
data.value!.messages.push({
topicId: unref(topicId),
userId: user.value!.id,
// TODO
attachments: [],
generation: null,
parentMessageId: null,
generationId: null,
activeChildId: null,
children: [],
deleted: false,
content: null,
createdAt: new Date(),
updatedAt: new Date(),
parts: [
{
id: 'bogus-id',
userId: user.value!.id,
topicId: unref(topicId),
messageId: message.id,
type: 'text',
content: baseMessage.content,
finished: true,
createdAt: new Date(),
lastUpdatedAt: new Date(),
providerOptions: null,
toolCallId: null,
toolCall: null,
}
],
...message
});
break;
}
}
} }
onRequest?.(); onRequest?.();
}, },
@@ -368,31 +521,11 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
return Err(ChatErrorType.NoAgent); return Err(ChatErrorType.NoAgent);
} }
const { settings } = await useUserSettings();
let rerank = undefined;
if (agent.config?.search?.rerank && settings.value.systemAssistants.rerank.enabled) {
const rerankModel = await useModels().then(m => m.allModels.value.find(m => m.id === settings.value.systemAssistants.rerank.modelId));
if (rerankModel) {
const rerankProviderApiKeyRes = await getProviderAPIKey(rerankModel.provider);
if (rerankProviderApiKeyRes.ok === false) {
return rerankProviderApiKeyRes;
}
const rerankProviderApiKey = rerankProviderApiKeyRes.data;
rerank = {
modelId: rerankModel.id,
providerApiKey: rerankProviderApiKey,
};
}
}
await $fetch(`/api/topic/${unref(topicId)}/chat`, { await $fetch(`/api/topic/${unref(topicId)}/chat`, {
method: 'POST', method: 'POST',
body: { body: {
modelId: model.id, modelId: model.id,
providerApiKey, providerApiKey,
rerank,
parentMessageId, parentMessageId,
}, },
}); });
@@ -463,38 +596,76 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
} }
const deleteMessagesFocusedDescendant = async (messageId: string): Promise<Result<void, Error>> => { const deleteMessagesFocusedDescendant = async (messageId: string): Promise<Result<void, Error>> => {
if (!data.value) return Err(new Error('No data')); if (!data.value) {
return Err(new Error("No data"));
}
const msg = data.value.messages.find(m => m.id === messageId); const msg = data.value.messages.find((m) => {
if (!msg) return Err(new Error('Message not found')); return m.id === messageId;
});
if (!msg) {
return Err(new Error("Message not found"));
}
const previousMessages = data.value!.messages; const previousMessages = data.value.messages;
const res = await $fetch(`/api/messages/${messageId}`, { const res = await $fetch(`/api/messages/${messageId}`, {
method: 'DELETE', method: "DELETE",
onRequest() { onRequest() {
if (!msg.activeChildId && (msg.parentMessageId === null || msg.parentMessageId === undefined)) { if (!data.value) {
msg.deleted = true;
return; return;
} }
const parent = data.value!.messages.find(m => m.id === msg.parentMessageId); if (msg.parentMessageId) {
data.value!.messages = data.value!.messages.filter(m => m.id !== msg.id); const parent = data.value.messages.find((m) => {
if (parent?.deleted) { return m.id === msg.parentMessageId;
data.value!.messages = data.value!.messages.filter(m => m.id !== parent.id); });
data.value.messages = data.value.messages.filter((m) => {
return m.id !== msg.id;
});
if (parent) {
const remainingChildren = data.value.messages.filter((m) => {
return m.parentMessageId === parent.id;
});
if (parent.deleted && remainingChildren.length === 0) {
data.value.messages = data.value.messages.filter((m) => {
return m.id !== parent.id;
});
} else if (parent.activeChildId === msg.id) {
parent.activeChildId = remainingChildren.length > 0
? (parent.deleted ? remainingChildren[0]!.id : remainingChildren[remainingChildren.length - 1]!.id)
: null;
}
}
return;
} }
const children = data.value.messages.filter((m) => {
return m.parentMessageId === msg.id;
});
if (children.length === 0) {
data.value.messages = data.value.messages.filter((m) => {
return m.id !== msg.id;
});
return;
}
msg.deleted = true;
msg.activeChildId = children[0]?.id ?? null;
}, },
onResponseError() { onResponseError() {
data.value!.messages = previousMessages; if (data.value) {
data.value.messages = previousMessages;
}
}, },
}); });
if (!res.ok) { if (!res.ok) {
return Err(new Error('Failed to delete message')); return Err(new Error("Failed to delete message"));
} }
return Ok(undefined); return Ok(undefined);
} };
return { return {
topic, topic,
+3 -2
View File
@@ -1,5 +1,6 @@
import { attempt } from "~~/types/result"; import { attempt } from "~~/types/result";
import * as schema from '~~/drizzle/schema'; import * as schema from '~~/drizzle/schema';
import { resolveFileUrl } from '~~/utils/url';
export type ResourceFile = typeof schema.files.$inferSelect; export type ResourceFile = typeof schema.files.$inferSelect;
@@ -118,12 +119,12 @@ export const useResources = async () => {
}; };
const copyLink = async (file: ResourceFile) => { const copyLink = async (file: ResourceFile) => {
const url = file.url; const url = resolveFileUrl(file.url);
await navigator.clipboard.writeText(url); await navigator.clipboard.writeText(url);
}; };
const downloadFile = (file: ResourceFile) => { const downloadFile = (file: ResourceFile) => {
const url = file.url; const url = resolveFileUrl(file.url);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = file.name; a.download = file.name;
-2
View File
@@ -8,8 +8,6 @@ export const useTokenDropdown = () => {
const triggerElement = useState<HTMLElement | null>('token-dropdown:trigger', () => null); const triggerElement = useState<HTMLElement | null>('token-dropdown:trigger', () => null);
const open = (generation: Generation, trigger: HTMLElement) => { const open = (generation: Generation, trigger: HTMLElement) => {
console.log("open", generation, trigger);
activeGeneration.value = generation; activeGeneration.value = generation;
triggerElement.value = trigger; triggerElement.value = trigger;
isOpen.value = true; isOpen.value = true;
+5 -1
View File
@@ -3,12 +3,13 @@ const persistTimeout = 150;
export const useTooltip = () => { export const useTooltip = () => {
const activeElement = useState<HTMLElement | null>('tooltip:activeElement', () => null); const activeElement = useState<HTMLElement | null>('tooltip:activeElement', () => null);
const activeText = useState<string | null>('tooltip:activeText', () => null);
const activeHotkey = useState<string[] | null>('tooltip:activeHotkey', () => null); const activeHotkey = useState<string[] | null>('tooltip:activeHotkey', () => null);
const isVisible = useState<boolean>('tooltip:isVisible', () => false); const isVisible = useState<boolean>('tooltip:isVisible', () => false);
const dwellTimeoutId = useState<NodeJS.Timeout | null>('tooltip:dwellTimeoutId', () => null); const dwellTimeoutId = useState<NodeJS.Timeout | null>('tooltip:dwellTimeoutId', () => null);
const closeTimeoutId = useState<NodeJS.Timeout | null>('tooltip:closeTimeout', () => null); const closeTimeoutId = useState<NodeJS.Timeout | null>('tooltip:closeTimeout', () => null);
const show = (el: HTMLElement, hotkey?: string[]) => { const show = (el: HTMLElement, hotkey?: string[], text?: string) => {
if (closeTimeoutId.value) { if (closeTimeoutId.value) {
clearTimeout(closeTimeoutId.value); clearTimeout(closeTimeoutId.value);
} }
@@ -16,10 +17,12 @@ export const useTooltip = () => {
if (isVisible.value) { if (isVisible.value) {
activeElement.value = el; activeElement.value = el;
activeHotkey.value = hotkey || null; activeHotkey.value = hotkey || null;
activeText.value = text || null;
} else { } else {
dwellTimeoutId.value = setTimeout(() => { dwellTimeoutId.value = setTimeout(() => {
activeElement.value = el; activeElement.value = el;
activeHotkey.value = hotkey || null; activeHotkey.value = hotkey || null;
activeText.value = text || null;
isVisible.value = true; isVisible.value = true;
}, dwellTimeout); }, dwellTimeout);
} }
@@ -44,6 +47,7 @@ export const useTooltip = () => {
return { return {
activeElement: readonly(activeElement), activeElement: readonly(activeElement),
activeHotkey: readonly(activeHotkey), activeHotkey: readonly(activeHotkey),
activeText: readonly(activeText),
isVisible: readonly(isVisible), isVisible: readonly(isVisible),
show, show,
hide, hide,
+9 -20
View File
@@ -10,7 +10,7 @@ export const useTopic = () => {
FailedToDecryptProviderApiKey, FailedToDecryptProviderApiKey,
FailedToGenerate, FailedToGenerate,
} }
const autoRename = async (topicId: string): Promise<Result<string, AutoRenameError>> => { const autoRename = async (topicId: string): Promise<Result<void, AutoRenameError>> => {
const { settings } = await useUserSettings(); const { settings } = await useUserSettings();
if (!settings.value.systemAssistants.rename.enabled) { if (!settings.value.systemAssistants.rename.enabled) {
@@ -54,7 +54,7 @@ export const useTopic = () => {
} }
} }
const { agents } = await useAgents(); const { patchTopicLocally, refresh } = await useAgents();
try { try {
// @ts-ignore - excessive stack depth // @ts-ignore - excessive stack depth
@@ -65,31 +65,20 @@ export const useTopic = () => {
providerApiKey, providerApiKey,
}), }),
onRequest() { onRequest() {
const topics = agents.value.flatMap(agent => agent.topics); patchTopicLocally(topicId, { renaming: true });
const topic = topics.find(topic => topic.id === topicId);
if (topic) {
agents.value = agents.value.map(agent => {
return agent.id === topic.agentId ? { ...agent, topics: agent.topics.map(t => t.id === topic.id ? { ...topic, renaming: true } : t) } : agent;
});
}
}, },
onRequestError() { });
const topics = agents.value.flatMap(agent => agent.topics);
const topic = topics.find(topic => topic.id === topicId);
if (topic) {
agents.value = agents.value.map(agent => {
return agent.id === topic.agentId ? { ...agent, topics: agent.topics.map(t => t.id === topic.id ? { ...topic, renaming: false } : t) } : agent;
});
}
},
}) as { ok: true, renameId: string } | { ok: false, code: string };
if (!res.ok) { if (!res.ok) {
patchTopicLocally(topicId, { renaming: false });
await refresh();
console.error('Failed to auto-rename:', res.code); console.error('Failed to auto-rename:', res.code);
return Err(AutoRenameError.FailedToGenerate); return Err(AutoRenameError.FailedToGenerate);
} }
return Ok(res.renameId); return Ok(undefined);
} catch (error) { } catch (error) {
patchTopicLocally(topicId, { renaming: false });
await refresh();
console.error('Failed to auto-rename:', error); console.error('Failed to auto-rename:', error);
return Err(AutoRenameError.FailedToGenerate); return Err(AutoRenameError.FailedToGenerate);
} }
+38 -7
View File
@@ -1,11 +1,14 @@
export const useUserEvents = () => { export const useUserEvents = () => {
const eventSource = ref<EventSource | null>(null); const eventSource = ref<EventSource | null>(null);
const agents = useState<AgentWithTopics[]>("agents_state", () => []);
const isConnected = useState('userEvents:connected', () => false); const isConnected = useState('userEvents:connected', () => false);
const lastEventTimestamp = useState<number>('userEvents:lastTimestamp', () => 0); const lastEventTimestamp = useState<number>('userEvents:lastTimestamp', () => 0);
const reconnectAttempts = ref(0); const reconnectAttempts = ref(0);
const connectionStartTime = ref<number>(0); const connectionStartTime = ref<number>(0);
const pendingReconnect = ref<NodeJS.Timeout | null>(null); const pendingReconnect = ref<NodeJS.Timeout | null>(null);
let reconciliationTimer: ReturnType<typeof setInterval> | undefined;
let reconciling = false;
const BASE_DELAY = 1000; const BASE_DELAY = 1000;
const MAX_DELAY = 30000; const MAX_DELAY = 30000;
@@ -50,7 +53,6 @@ export const useUserEvents = () => {
cleanup(); cleanup();
connectionStartTime.value = Date.now(); connectionStartTime.value = Date.now();
reconnectAttempts.value = 0;
const source = new EventSource('/api/events'); const source = new EventSource('/api/events');
eventSource.value = source; eventSource.value = source;
@@ -59,6 +61,7 @@ export const useUserEvents = () => {
console.log('User events connected'); console.log('User events connected');
isConnected.value = true; isConnected.value = true;
reconnectAttempts.value = 0; reconnectAttempts.value = 0;
void reconcile();
}; };
source.onerror = () => { source.onerror = () => {
@@ -164,20 +167,48 @@ export const useUserEvents = () => {
}; };
const reconcile = async () => { const reconcile = async () => {
console.log('Reconciling user state...'); if (reconciling) {
const { data } = await useFetch<AgentWithTopics[]>('/api/agents'); return;
if (data.value) { }
const { agents } = await useAgents(); reconciling = true;
agents.value = data.value; try {
agents.value = await $fetch<AgentWithTopics[]>("/api/agents");
} catch (error) {
console.error("Failed to reconcile topics:", error);
} finally {
reconciling = false;
} }
}; };
const resume = () => {
if (document.visibilityState !== "visible") {
return;
}
if (!isConnected.value) {
reconnectAttempts.value = 0;
void connect();
}
void reconcile();
};
onMounted(() => { onMounted(() => {
connect(); void connect();
document.addEventListener("visibilitychange", resume);
window.addEventListener("online", resume);
window.addEventListener("pageshow", resume);
reconciliationTimer = setInterval(() => {
if (document.visibilityState === "visible" && agents.value.some(agent => agent.topics.some(topic => topic.renaming))) {
void reconcile();
}
}, 15000);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
cleanup(); cleanup();
clearInterval(reconciliationTimer);
document.removeEventListener("visibilitychange", resume);
window.removeEventListener("online", resume);
window.removeEventListener("pageshow", resume);
}); });
return { return {
+2 -3
View File
@@ -18,8 +18,7 @@ useHead({
}); });
if (import.meta.client) { if (import.meta.client) {
const { connect } = useUserEvents(); useUserEvents();
connect();
} }
</script> </script>
@@ -28,7 +27,7 @@ if (import.meta.client) {
<div class="p-2 w-full h-full max-w-full overflow-x-hidden"> <div class="p-2 w-full h-full max-w-full overflow-x-hidden">
<main <main
class="bg-[var(--bg-surface)] flex flex-col h-full w-full border border-solid border-[var(--color-border)] rounded-lg overflow-hidden"> class="bg-[var(--bg-surface)] flex flex-col h-full w-full border border-solid border-[var(--color-border)] rounded-lg overflow-hidden">
<div class="flex-1 overflow-y-auto"> <div class="flex-1 min-h-0 flex flex-col">
<slot /> <slot />
</div> </div>
</main> </main>
+45 -5
View File
@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { nanoid } from 'nanoid';
import type { BaseMessage } from '~/composables/useChat'; import type { BaseMessage } from '~/composables/useChat';
import type { ModelWithProvider } from '~/composables/useModels'; import type { ModelWithProvider } from '~/composables/useModels';
@@ -43,13 +44,29 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
children: [], children: [],
generationId: null, generationId: null,
activeChildId: null, activeChildId: null,
attachments: [], attachments: message.files?.map(file => ({
id: nanoid(),
userId: user.value!.id,
topicId: '',
messageId: '',
fileId: file.id,
createdAt: new Date(),
file: {
id: file.id,
userId: user.value!.id,
name: file.name,
mimeType: file.mimeType,
size: file.size ?? 0,
url: file.url,
createdAt: new Date(),
},
})) ?? [],
deleted: false, deleted: false,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
} }
const topic = await createTopic(agent.value!.id); const topic = await createTopic(agent.value!.id, message);
if (!topic) throw new Error('Failed to create topic'); if (!topic) throw new Error('Failed to create topic');
const { sendMessage, startGeneration } = await useChat(topic.id, false); const { sendMessage, startGeneration } = await useChat(topic.id, false);
@@ -75,12 +92,34 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
return startGeneration(model, undefined, route.params.id as string); return startGeneration(model, undefined, route.params.id as string);
}; };
const handleAddMessage = async (message: BaseMessage, role: 'user' | 'assistant', _model: ModelWithProvider | null) => {
const user = useAuth().user;
if (!user) {
console.error('No user');
return;
}
const topic = await createTopic(agent.value!.id, message);
if (!topic) throw new Error('Failed to create topic');
const { sendMessage } = await useChat(topic.id, false);
const res = await sendMessage(message, undefined, role);
if (res.ok === false) {
console.error('Failed to send message:', res.error);
return;
}
await navigateTo(`/agent/${route.params.id}/topic/${topic.id}`);
};
</script> </script>
<template> <template>
<div class="flex flex-col h-full w-full max-w-full overflow-x-hidden"> <div class="flex flex-col h-full w-full max-w-full overflow-x-hidden">
<div class="h-14 flex items-center justify-between px-4 border-b border-[var(--color-border)]"> <div
class="z-5 h-14 flex items-center justify-between px-4 border-b border-[var(--color-border)] bg-[var(--bg-surface)]">
<div class="flex items-center gap-2 max-w-full"> <div class="flex items-center gap-2 max-w-full">
<button v-if="!sidebarOpen" @click="openSidebar" <button v-if="!sidebarOpen" @click="openSidebar"
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors"> class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
@@ -93,7 +132,7 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
</div> </div>
<div ref="chatPaneWrapper" <div ref="chatPaneWrapper"
class="chat-scroll-container justify-center w-full h-full [scrollbar-width:thin] [scrollbar-color:#888_transparent] overflow-y-scroll overflow-x-hidden flex justify-center pt-4"> class="chat-scroll-container justify-center w-full flex-1 min-h-0 [scrollbar-width:thin] [scrollbar-color:#888_transparent] overflow-y-scroll overflow-x-hidden flex justify-center pt-4">
<div class="chatPane max-w-4xl w-full min-h-full flex flex-col px-4"> <div class="chatPane max-w-4xl w-full min-h-full flex flex-col px-4">
<div class="w-full px-px flex flex-col flex-grow gap-2" <div class="w-full px-px flex flex-col flex-grow gap-2"
:class="pendingMessage === null ? 'justify-end pb-28' : 'pb-9'"> :class="pendingMessage === null ? 'justify-end pb-28' : 'pb-9'">
@@ -108,7 +147,8 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
<div class="sticky bottom-0 z-10 bg-[var(--bg-surface)] pb-4 w-full rounded-t-2xl"> <div class="sticky bottom-0 z-10 bg-[var(--bg-surface)] pb-4 w-full rounded-t-2xl">
<ChatInput v-model="inputValue" class="[view-transition-name:chat-prompt] duration-150 ease-in-out" <ChatInput v-model="inputValue" class="[view-transition-name:chat-prompt] duration-150 ease-in-out"
:agent="agent" :providers="providers?.filter(p => p.enabled)" @submit="handleSubmit" /> :allow-manual-role="true" :agent="agent" :providers="providers?.filter(p => p.enabled)"
@submit="handleSubmit" @add-message="handleAddMessage" />
</div> </div>
</div> </div>
</div> </div>
+24 -22
View File
@@ -55,30 +55,32 @@ const changeSystemPrompt = async (e: Event) => {
</script> </script>
<template> <template>
<div class="h-14 flex items-center justify-between px-4"> <div class="h-full flex flex-col overflow-hidden pb-4">
<div class="flex items-center gap-2"> <div class="h-14 flex items-center justify-between px-4 shrink-0">
<button v-if="!sidebarOpen" @click="openSidebar" <div class="flex items-center gap-2">
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors"> <button v-if="!sidebarOpen" @click="openSidebar"
<span class="i-mynaui-panel-left-open text-5"></span> class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
</button> <span class="i-mynaui-panel-left-open text-5"></span>
</div> </button>
</div>
<div class="flex flex-col gap-4 px-14 w-full h-full">
<div class="flex items-center gap-4">
<div>
<img v-if="agent?.imageUrl" :src="agent.imageUrl" class="w-16 h-16 rounded-full object-cover" />
<span class="text-16 i-mynaui-check-hexagon"></span>
</div> </div>
<input v-model="name" @input="handleNameInput" placeholder="Agent Name..."
class="placeholder:text-[var(--text-tertiary)] w-full bg-transparent rounded-none border-b-4 border-b-[var(--color-border)] text-12 p-0"
type="text" />
</div> </div>
<div class="flex flex-col gap-2 w-full h-full mb-14">
<label class="text-sm text-[var(--text-secondary)]">System Message</label> <div class="flex flex-col gap-4 px-4 md:px-14 w-full flex-1 min-h-0">
<textarea v-model="systemPrompt" placeholder="You are a helpful assistant." <div class="flex items-center gap-4">
class="p-4 w-full h-full resize-none bg-transparent rounded-lg border border-[var(--color-border)]" <div>
@input="changeSystemPrompt"></textarea> <img v-if="agent?.imageUrl" :src="agent.imageUrl" class="w-16 h-16 rounded-full object-cover" />
<span class="text-16 i-mynaui-check-hexagon"></span>
</div>
<input v-model="name" @input="handleNameInput" placeholder="Agent Name..."
class="placeholder:text-[var(--text-tertiary)] w-full bg-transparent rounded-none border-b-4 border-b-[var(--color-border)] text-12 p-0"
type="text" />
</div>
<div class="flex flex-col gap-2 w-full flex-1 min-h-0">
<label class="text-sm text-[var(--text-secondary)]">System Message</label>
<textarea v-model="systemPrompt" placeholder="You are a helpful assistant."
class="p-4 w-full flex-1 min-h-0 resize-none bg-transparent rounded-lg border border-[var(--color-border)]"
@input="changeSystemPrompt"></textarea>
</div>
</div> </div>
</div> </div>
</template> </template>
+96 -100
View File
@@ -1,14 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { type BaseMessage, type Message, ChatErrorType } from '~/composables/useChat'; import { type BaseMessage, type Message, ChatErrorType } from '~/composables/useChat';
import type { ModelWithProvider } from '~/composables/useModels'; import type { ModelWithProvider } from '~/composables/useModels';
import { buildFocusedMessageTree } from '~~/utils/message'; import { buildFocusedMessageTree, getActiveMessage, getNextActiveChildId } from '~~/utils/message';
const rootStart = Date.now();
const chatPaneWrapper = ref<HTMLElement | null>(null); const chatPaneWrapper = ref<HTMLElement | null>(null);
const inputValue = ref<BaseMessage>({ content: '', fileIds: [] }); const inputValue = ref<BaseMessage>({ content: '', fileIds: [] });
const route = useRoute(); const route = useRoute();
const { getAgent, deleteTopic } = await useAgents(); const { getAgent, deleteTopic, forkTopic } = await useAgents();
const { open: sidebarOpen, openSidebar } = useSidebar(); const { open: sidebarOpen, openSidebar } = useSidebar();
const { providers, allModels } = await useModels(); const { providers, allModels } = await useModels();
const { addShortcut } = useKeyboardShortcuts(); const { addShortcut } = useKeyboardShortcuts();
@@ -54,17 +52,37 @@ const submitMessage = async (message: BaseMessage, model: ModelWithProvider | nu
startGeneration(model); startGeneration(model);
}; };
const handleAddMessage = async (message: BaseMessage, role: 'user' | 'assistant', _model: ModelWithProvider | null) => {
inputValue.value = { content: '', fileIds: [] };
const res = await sendMessage(message, async () => {
await nextTick();
setTimeout(() => {
scrollToBottom('instant');
});
}, role);
if (!res.ok) {
console.error('Failed to add message:', res.error);
const chatInput = document.getElementById('chat') as HTMLInputElement | null;
if (chatInput) {
inputValue.value = message;
nextTick(() => {
chatInput.focus();
});
}
}
};
const handleRegenerate = async (message: Message) => { const handleRegenerate = async (message: Message) => {
if (!agent.value!.defaultModelId) { if (!agent.value!.defaultModelId) {
console.error('No model selected'); console.error('No model selected');
return; return;
} }
let messageId: string; const activeMsg = getActiveMessage(message);
if (message.activeChildId && message.children.length > 0) { if (!activeMsg) {
messageId = message.activeChildId; console.error('No active message');
} else { return;
messageId = message.id;
} }
const model = allModels.value.find(m => m.id === agent.value!.defaultModelId); const model = allModels.value.find(m => m.id === agent.value!.defaultModelId);
@@ -73,35 +91,54 @@ const handleRegenerate = async (message: Message) => {
return; return;
} }
const res = await regenerateMessage(messageId, buildFocusedMessageTree(topic.value!.messages), model); const res = await regenerateMessage(activeMsg.id, buildFocusedMessageTree(topic.value!.messages), model);
if (!res.ok) { if (!res.ok) {
console.error('Failed to regenerate message:', ChatErrorType[res.error]); console.error('Failed to regenerate message:', ChatErrorType[res.error]);
return; return;
} }
} };
const flatMessages = computed(() => { const flatMessages = computed(() => {
const messages: Message[] = []; const messages: Message[] = [];
for (const message of topic.value?.messages ?? []) { for (const message of topic.value?.messages ?? []) {
messages.push(message); if (!message.deleted) {
messages.push(message);
}
if (message.children.length > 0) { if (message.children.length > 0) {
messages.push(...message.children as Message[]); for (const child of message.children) {
if (child && !child.deleted) {
messages.push(child as Message);
}
}
} }
} }
return messages; return messages;
}); });
const handleDelete = async (rootMessage: Message) => { const handleFork = async (message: Message) => {
let messageId: string; const activeMsg = getActiveMessage(message);
if (rootMessage.activeChildId && rootMessage.children.length > 0) { if (!activeMsg) {
messageId = rootMessage.activeChildId; console.error('No active message');
} else { return;
messageId = rootMessage.id;
} }
const result = await deleteMessagesFocusedDescendant(messageId); const newTopicId = await forkTopic(topicId.value, activeMsg.id);
if (newTopicId) {
await navigateTo(`/agent/${route.params.id}/topic/${newTopicId}`);
} else {
console.error('Failed to fork topic');
}
};
const handleDelete = async (rootMessage: Message) => {
const activeMsg = getActiveMessage(rootMessage);
if (!activeMsg) {
return;
}
const result = await deleteMessagesFocusedDescendant(activeMsg.id);
if (!result.ok) { if (!result.ok) {
console.error('Failed to delete message:', result.error); console.error('Failed to delete message:', result.error);
return; return;
@@ -111,21 +148,19 @@ const handleDelete = async (rootMessage: Message) => {
await navigateTo(`/agent/${route.params.id}`); await navigateTo(`/agent/${route.params.id}`);
deleteTopic(route.params.id as string, topicId.value); deleteTopic(route.params.id as string, topicId.value);
} }
} };
const { scrollToBottom, isAtBottom } = useAutoScroll(chatPaneWrapper);
const handleResize = () => { const handleResize = () => {
const el = chatPaneWrapper.value; if (!isAtBottom()) return;
if (!el) return;
const atBottom = (el.scrollHeight - el.scrollTop - el.clientHeight) <= 80;
if (!atBottom) return;
nextTick().then(() => { nextTick().then(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
scrollToBottom('instant'); scrollToBottom('instant');
}); });
}); });
} };
const activeGeneration = computed(() => { const activeGeneration = computed(() => {
if (topic.value === null) return null; if (topic.value === null) return null;
@@ -133,8 +168,6 @@ const activeGeneration = computed(() => {
return generations?.find((generation) => generation?.status === 'pending') ?? null; return generations?.find((generation) => generation?.status === 'pending') ?? null;
}); });
const { scrollToBottom } = useAutoScroll(chatPaneWrapper);
addShortcut(['ctrl', 'alt', 'n'], (event) => { addShortcut(['ctrl', 'alt', 'n'], (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@@ -168,76 +201,39 @@ addShortcut(['ctrl', 'shift', 'c'], (event) => {
handleCancel(); handleCancel();
}) })
addShortcut(['alt', '['], async (event) => { const navigateLastMessage = (direction: -1 | 1) => {
const lastMessage = topic.value?.messages?.at(-1);
if (!lastMessage || lastMessage.children.length === 0) {
return;
}
const nextActiveChildId = getNextActiveChildId(lastMessage, direction);
patchMessageLocally(lastMessage.id, { activeChildId: nextActiveChildId });
};
addShortcut(['alt', '['], (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
navigateLastMessage(-1);
});
const lastMessage = topic.value?.messages?.at(-1); addShortcut(['ctrl', 'alt', 'arrowleft'], (event) => {
if (lastMessage && lastMessage.children.length > 0) {
const currentIdx = lastMessage.children.findIndex(c => c?.id === lastMessage.activeChildId);
const prevIdx = Math.max(0, currentIdx);
if (currentIdx > 0) {
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[prevIdx - 1]!.id });
}
}
})
addShortcut(['alt', ']'], async (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
navigateLastMessage(-1);
});
const lastMessage = topic.value?.messages?.at(-1); addShortcut(['alt', ']'], (event) => {
if (lastMessage && lastMessage.children.length > 0) { event.preventDefault();
const currentIdx = lastMessage.children.findIndex(c => c?.id === lastMessage.activeChildId); event.stopPropagation();
const maxIdx = lastMessage.deleted ? lastMessage.children.length - 1 : lastMessage.children.length; navigateLastMessage(1);
const nextIdx = Math.min(maxIdx, currentIdx + 1); });
if (lastMessage.deleted) { addShortcut(['ctrl', 'alt', 'arrowright'], (event) => {
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[nextIdx]?.id ?? null }); event.preventDefault();
} else { event.stopPropagation();
if (nextIdx === 0) { navigateLastMessage(1);
patchMessageLocally(lastMessage.id, { activeChildId: null }); });
} else {
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[nextIdx - 1]?.id ?? null });
}
}
}
})
addShortcut(['ctrl', 'alt', 'arrowleft'], async (event) => {
const lastMessage = topic.value?.messages?.at(-1);
if (lastMessage && lastMessage.children.length > 0) {
event.preventDefault();
event.stopPropagation();
const currentIdx = lastMessage.children.findIndex(c => c?.id === lastMessage.activeChildId);
if (currentIdx > 0) {
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[currentIdx - 1]!.id });
}
}
})
addShortcut(['ctrl', 'alt', 'arrowright'], async (event) => {
const lastMessage = topic.value?.messages?.at(-1);
if (lastMessage && lastMessage.children.length > 0) {
event.preventDefault();
event.stopPropagation();
const currentIdx = lastMessage.children.findIndex(c => c?.id === lastMessage.activeChildId);
const maxIdx = lastMessage.deleted ? lastMessage.children.length - 1 : lastMessage.children.length;
const nextIdx = Math.min(maxIdx, currentIdx + 1);
if (lastMessage.deleted) {
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[nextIdx]?.id ?? null });
} else {
if (nextIdx === 0) {
patchMessageLocally(lastMessage.id, { activeChildId: null });
} else {
patchMessageLocally(lastMessage.id, { activeChildId: lastMessage.children[nextIdx - 1]?.id ?? null });
}
}
}
})
addShortcut(['ctrl', 'alt', 'backspace'], async (event) => { addShortcut(['ctrl', 'alt', 'backspace'], async (event) => {
event.preventDefault(); event.preventDefault();
@@ -257,13 +253,12 @@ const handleCancel = async () => {
method: 'POST', method: 'POST',
}); });
}; };
console.log("full page render took", Date.now() - rootStart);
</script> </script>
<template> <template>
<div class="flex flex-col h-full w-full max-w-full overflow-x-hidden"> <div class="flex flex-col h-full w-full max-w-full overflow-x-hidden">
<div class="h-14 flex items-center justify-between px-4 border-b border-[var(--color-border)]"> <div
class="z-5 h-14 flex items-center justify-between px-4 border-b border-[var(--color-border)] bg-[var(--bg-surface)]">
<div class="flex items-center gap-2 max-w-full"> <div class="flex items-center gap-2 max-w-full">
<button v-if="!sidebarOpen" @click="openSidebar" <button v-if="!sidebarOpen" @click="openSidebar"
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors"> class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
@@ -277,7 +272,7 @@ console.log("full page render took", Date.now() - rootStart);
<!-- chat pane --> <!-- chat pane -->
<div ref="chatPaneWrapper" <div ref="chatPaneWrapper"
class="chat-scroll-container justify-center w-full h-full [scrollbar-width:thin] [scrollbar-color:#888_transparent] overflow-y-scroll overflow-x-hidden flex justify-center pt-4"> class="chat-scroll-container justify-center w-full flex-1 min-h-0 [scrollbar-width:thin] [scrollbar-color:#888_transparent] overflow-y-scroll overflow-x-hidden flex justify-center pt-4">
<div class="chatPane max-w-4xl w-full min-h-full flex flex-col px-4"> <div class="chatPane max-w-4xl w-full min-h-full flex flex-col px-4">
<div class="w-full px-px flex flex-col flex-grow gap-2 pb-9"> <div class="w-full px-px flex flex-col flex-grow gap-2 pb-9">
<Suspense> <Suspense>
@@ -285,16 +280,17 @@ console.log("full page render took", Date.now() - rootStart);
<Message v-for="message in topic.messages" :key="message.id" :message="message" <Message v-for="message in topic.messages" :key="message.id" :message="message"
@edit="(value) => patchMessageLocally(message.id, { content: value })" @edit="(value) => patchMessageLocally(message.id, { content: value })"
@patch="(updates) => patchMessageLocally(message.id, updates)" @patch="(updates) => patchMessageLocally(message.id, updates)"
@delete="handleDelete(message)" @regenerate="handleRegenerate(message)" /> @delete="handleDelete(message)" @regenerate="handleRegenerate(message)"
@fork="handleFork(message)" />
</template> </template>
</Suspense> </Suspense>
</div> </div>
<div class="sticky bottom-0 z-10 bg-[var(--bg-surface)] pb-4 w-full rounded-t-2xl"> <div class="sticky bottom-0 z-10 bg-[var(--bg-surface)] pb-4 w-full rounded-t-2xl">
<ChatInput v-model="inputValue" class="[view-transition-name:chat-prompt] duration-150 ease-in-out" <ChatInput v-model="inputValue" class="[view-transition-name:chat-prompt] duration-150 ease-in-out"
:loading="activeGeneration !== null" :agent="agent" :loading="activeGeneration !== null" :allow-manual-role="true" :agent="agent"
:providers="providers?.filter(p => p.enabled)" @submit="submitMessage" @cancel="handleCancel" :providers="providers?.filter(p => p.enabled)" @submit="submitMessage"
@resize="handleResize" /> @add-message="handleAddMessage" @cancel="handleCancel" @resize="handleResize" />
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -95,7 +95,7 @@ const handleChatSubmit = async (message: BaseMessage, model: ModelWithProvider |
} }
} }
const topic = await createTopic(targetAgent.id); const topic = await createTopic(targetAgent.id, message);
if (!topic) { if (!topic) {
console.error('Failed to create topic'); console.error('Failed to create topic');
return; return;
+2 -1
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue'; import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue';
import { resolveFileUrl } from '~~/utils/url';
const { open: sidebarOpen, openSidebar } = useSidebar(); const { open: sidebarOpen, openSidebar } = useSidebar();
const { const {
@@ -88,7 +89,7 @@ const handleRowClick = (e: MouseEvent) => {
if (file && isImage(file.mimeType)) { if (file && isImage(file.mimeType)) {
const row = trigger.closest('[data-file-id]') as HTMLElement; const row = trigger.closest('[data-file-id]') as HTMLElement;
previewOrigin.value = row?.getBoundingClientRect() ?? null; previewOrigin.value = row?.getBoundingClientRect() ?? null;
previewSrc.value = file.url; previewSrc.value = resolveFileUrl(file.url);
} }
break; break;
} }
+1 -1
View File
@@ -3,7 +3,7 @@ export default defineNuxtPlugin((nuxtApp) => {
mounted(el: any, binding: any) { mounted(el: any, binding: any) {
el.clickOutsideEvent = (event: Event) => { el.clickOutsideEvent = (event: Event) => {
if (!el.contains(event.target as Node)) { if (!el.contains(event.target as Node)) {
binding.value(); binding.value(event);
} }
}; };
-65
View File
@@ -1,65 +0,0 @@
import { unified } from 'unified';
import { visit } from 'unist-util-visit';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import remarkBreaks from 'remark-breaks';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
function remarkSplitBlocks() {
return (tree: any) => {
visit(tree, 'paragraph', (node, index, parent) => {
const breakIndex = node.children.findIndex((child: any) => child.type === 'break');
if (breakIndex !== -1) {
const beforeBreak = node.children.slice(0, breakIndex);
const afterBreak = node.children.slice(breakIndex + 1);
node.children = beforeBreak;
const newParagraph = {
type: 'paragraph',
children: afterBreak,
};
let rootChildIndex = -1;
if (parent.type === 'root') {
rootChildIndex = index!;
} else {
rootChildIndex = tree.children.findIndex((child: any) =>
child === parent || (child.children && child.children.includes(node))
);
if (rootChildIndex === -1) {
rootChildIndex = tree.children.indexOf(parent);
}
}
if (rootChildIndex !== -1) {
tree.children.splice(rootChildIndex + 1, 0, newParagraph);
}
return index! + 1;
}
});
};
}
export default defineNuxtPlugin((nuxtApp) => {
const remark =
unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkMath)
.use(remarkBreaks)
.use(remarkSplitBlocks)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeKatex, { output: 'mathml' });
return {
provide: {
remark,
}
}
})
+45
View File
@@ -0,0 +1,45 @@
import type { ComarkPlugin } from "comark";
import type { SearchSource, StoredToolValue } from "./tool-rendering";
import { parseToolValue, readSearchSources } from "./tool-rendering";
export const citationPlugin: ComarkPlugin = {
name: "source-citations",
markdownItPlugins: [
(markdown) => {
markdown.inline.ruler.before("link", "source_citation", (state, silent) => {
if (state.linkLevel > 0 || !state.src.startsWith("[[cite:", state.pos)) {
return false;
}
const match = /^\[\[cite:([a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*)\]\]/.exec(state.src.slice(state.pos));
if (!match) {
return false;
}
if (silent) {
return false;
}
const token = state.push("mdc_inline_component", "source-cite", 0);
token.attrSet("ids", match[1] ?? "");
state.pos += match[0].length;
return true;
});
},
],
};
export const collectCitationSources = (
parts: readonly { toolCall?: { toolName: string; output?: StoredToolValue } | null }[],
): ReadonlyMap<string, SearchSource> => {
const sources = new Map<string, SearchSource>();
for (const part of parts) {
if (part.toolCall?.toolName !== "search") {
continue;
}
const results = readSearchSources(parseToolValue(part.toolCall.output), { deduplicate: false });
for (const source of results ?? []) {
if (source.sourceId) {
sources.set(source.sourceId, source);
}
}
}
return sources;
};
+11
View File
@@ -0,0 +1,11 @@
export const getComposerHeight = (
scrollHeight: number,
lineHeight: number,
padding: number,
borders: number,
borderBox: boolean,
): number => {
const contentHeight = Math.min(Math.max(scrollHeight - padding, lineHeight * 2), lineHeight * 10);
// A stable CSS-pixel boundary avoids 47.999… / 48.01 oscillations.
return Math.round(contentHeight + (borderBox ? padding + borders : 0));
};
+277
View File
@@ -0,0 +1,277 @@
import type { MarkdownDocument, Node } from 'comark';
import { createSerializedMarkdownParser as createComarkParser } from 'comark';
import breaks from 'comark/plugins/breaks';
import footnotes from 'comark/plugins/footnotes';
import math from 'comark/plugins/math';
import { citationPlugin } from './citations';
export type MarkdownTree = MarkdownDocument;
export type MarkdownAstNode = Node;
export type MarkdownElementNode = Exclude<MarkdownAstNode, string | [null, ...unknown[]]>;
const footnoteSyntax = {
name: 'footnote-syntax',
markdownItPlugins: [
(markdown: any) => markdown.inline.ruler.before('link', 'footnote_inline', (state: any, silent: boolean) => {
const start = state.pos;
if (!state.src.startsWith('[^', start)) {
return false;
}
const end = state.src.indexOf(']', start + 2);
const label = end === -1 ? '' : state.src.slice(start + 2, end);
if (end === -1 || label.length === 0 || /\s/.test(label) || silent) {
return false;
}
state.push('mdc_inline_span', 'span', 1);
state.push('text', '', 0).content = `^${label}`;
state.push('mdc_inline_span', 'span', -1);
state.pos = end + 1;
return true;
}),
],
};
const parserPlugins = [
breaks(),
footnoteSyntax,
footnotes(),
math(),
] as const;
const createConfiguredParser = (autoClose: boolean, citations: boolean) => createComarkParser({
registerDefaultPlugins: false,
autoClose,
plugins: citations ? [...parserPlugins, citationPlugin] : parserPlugins,
});
const protectDollarRunsInLine = (line: string): string => {
let result = '';
let segment = '';
let codeDelimiterLength = 0;
const flushSegment = () => {
result += segment.replace(/\${3,}/g, (run) => run.replaceAll('$', '&#36;'));
segment = '';
};
for (let index = 0; index < line.length;) {
let delimiterLength = 0;
if (line[index] === '`') {
while (line[index + delimiterLength] === '`') {
delimiterLength += 1;
}
}
if (codeDelimiterLength !== 0) {
result += line.slice(index, index + (delimiterLength || 1));
if (delimiterLength === codeDelimiterLength) {
codeDelimiterLength = 0;
}
index += delimiterLength || 1;
continue;
}
if (delimiterLength === 0) {
segment += line[index];
index += 1;
continue;
}
flushSegment();
result += line.slice(index, index + delimiterLength);
codeDelimiterLength = delimiterLength;
index += delimiterLength;
}
flushSegment();
return result;
};
const protectDollarRuns = (content: string): string => {
// Fast path: most messages contain no dollar runs; avoid line-splitting entirely.
if (!content.includes('$')) {
return content;
}
const lines = content.split('\n');
let inFence = false;
return lines.map((line) => {
const fence = /^\s{0,3}(`{3,}|~{3,})/.exec(line);
if (fence !== null) {
inFence = !inFence;
return line;
}
if (inFence) {
return line;
}
return protectDollarRunsInLine(line);
}).join('\n');
};
export const createMarkdownParser = (options: { citations?: boolean } = {}) => {
const citations = options.citations === true;
const streamingParser = createConfiguredParser(true, citations);
const finalParser = createConfiguredParser(false, citations);
return (content: string, options: { streaming?: boolean } = {}) => {
const protectedContent = protectDollarRuns(content);
// Do not let auto-close complete an unfinished citation on the model's behalf.
const incompleteCitation = citations && /\[\[cite:[a-zA-Z0-9_,\-]*\]?$/.test(protectedContent);
if (options.streaming === true && !incompleteCitation) {
return streamingParser(protectedContent, { streaming: true });
}
return finalParser(protectedContent, { streaming: false });
};
};
export const agentParser = createMarkdownParser({ citations: true });
export const userParser = createMarkdownParser({ citations: false });
export const getNodeTag = (node: MarkdownAstNode): string | null => {
if (!Array.isArray(node) || node[0] === null) {
return null;
}
return node[0];
};
export const getNodeAttributes = (node: MarkdownAstNode): Record<string, unknown> => {
if (!Array.isArray(node) || node[0] === null || typeof node[1] !== 'object' || node[1] === null) {
return {};
}
return node[1] as Record<string, unknown>;
};
export const getNodeChildren = (node: MarkdownAstNode): MarkdownAstNode[] => {
if (!Array.isArray(node) || node[0] === null) {
return [];
}
return node.slice(2) as MarkdownAstNode[];
};
export const textContent = (node: MarkdownAstNode): string => {
if (typeof node === 'string') {
return node;
}
if (!Array.isArray(node) || node[0] === null) {
return '';
}
return getNodeChildren(node).map(textContent).join('');
};
const splitParagraph = (attributes: Record<string, unknown>, children: MarkdownAstNode[]): MarkdownElementNode[] => {
const paragraphs: MarkdownElementNode[] = [];
let current: MarkdownAstNode[] = [];
for (const child of children) {
if (getNodeTag(child) === 'br') {
paragraphs.push(['p', { ...attributes }, ...current] as MarkdownElementNode);
current = [];
continue;
}
current.push(child);
}
if (current.length > 0 || paragraphs.length === 0) {
paragraphs.push(['p', { ...attributes }, ...current] as MarkdownElementNode);
}
return paragraphs;
};
const hasBrChild = (children: MarkdownAstNode[]): boolean =>
children.some((child) => getNodeTag(child) === 'br');
/**
* Transform a single element node into its replacement list. Returns a
* single-element array holding the ORIGINAL node reference when nothing
* changed, so callers can skip all allocation for unaffected subtrees.
*/
const transformElement = (node: MarkdownElementNode): MarkdownAstNode[] => {
const tag = node[0];
const children = getNodeChildren(node);
const transformedChildren = transformChildren(children);
if (tag === 'p') {
if (!hasBrChild(transformedChildren)) {
if (transformedChildren === children) return [node];
return [[tag, { ...node[1] }, ...transformedChildren] as MarkdownElementNode];
}
return splitParagraph({ ...node[1] }, transformedChildren);
}
if ((tag === 'li' || tag === 'blockquote') && hasBrChild(transformedChildren)) {
const segments: MarkdownAstNode[][] = [[]];
for (const child of transformedChildren) {
if (getNodeTag(child) === 'br') {
segments.push([]);
} else {
segments[segments.length - 1]?.push(child);
}
}
return [[
tag,
{ ...node[1] },
...segments
.filter((segment) => segment.length > 0)
.map((segment) => ['p', {}, ...segment] as MarkdownElementNode),
] as MarkdownElementNode];
}
if (transformedChildren === children) return [node];
return [[tag, { ...node[1] }, ...transformedChildren] as MarkdownElementNode];
};
const transformChildren = (nodes: MarkdownAstNode[]): MarkdownAstNode[] => {
let result: MarkdownAstNode[] | null = null;
for (let index = 0; index < nodes.length; index++) {
const node = nodes[index]!;
const replacement = typeof node === 'string' || !Array.isArray(node) || node[0] === null
? [node]
: transformElement(node);
if (result === null) {
if (replacement.length === 1 && replacement[0] === node) continue;
result = nodes.slice(0, index);
}
result.push(...replacement);
}
return result ?? nodes;
};
/**
* Comark's breaks plugin emits `br` nodes. Promote each hard break to a
* sibling paragraph to keep the chat renderer's established layout.
*
* Copy-on-write: trees without `br` nodes are returned by reference with no
* allocation, so the common case costs a single tag scan per node.
*/
export const splitParagraphBreaks = (tree: MarkdownTree): MarkdownTree => {
const nodes = transformChildren(tree.nodes);
return nodes === tree.nodes ? tree : { ...tree, nodes };
};
export const toVueAttributes = (attributes: Record<string, unknown>): Record<string, unknown> => {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(attributes)) {
if (key !== '$') {
result[key] = value;
}
}
return result;
};
+18 -3
View File
@@ -54,6 +54,9 @@ import {
LogoClosedRouter, LogoClosedRouter,
LogoPoolside, LogoPoolside,
LogoTencent, LogoTencent,
LogoThinkingMachines,
LogoDotsStudio,
LogoNexAgi,
LogoHuggingFace, LogoHuggingFace,
} from '#components'; } from '#components';
import { markRaw } from 'vue'; import { markRaw } from 'vue';
@@ -65,6 +68,18 @@ interface ModelConfig {
} }
const MODEL_MAPPINGS: ModelConfig[] = [ const MODEL_MAPPINGS: ModelConfig[] = [
{
Icon: markRaw(LogoNexAgi),
keywords: [/nex-agi\//, /\/nex-/],
},
{
Icon: markRaw(LogoDotsStudio),
keywords: [/dots-studio/, /^dots/, /\/dots/],
},
{
Icon: markRaw(LogoThinkingMachines),
keywords: [/^thinkingmachines\//, /\/linkling/],
},
{ Icon: markRaw(LogoOpenAI), keywords: [/gpt-3/], props: { type: 'gpt3' } }, { Icon: markRaw(LogoOpenAI), keywords: [/gpt-3/], props: { type: 'gpt3' } },
{ Icon: markRaw(LogoOpenAI), keywords: [/gpt-4/], props: { type: 'gpt4' } }, { Icon: markRaw(LogoOpenAI), keywords: [/gpt-4/], props: { type: 'gpt4' } },
{ Icon: markRaw(LogoOpenAI), keywords: [/gpt-5/], props: { type: 'gpt5' } }, { Icon: markRaw(LogoOpenAI), keywords: [/gpt-5/], props: { type: 'gpt5' } },
@@ -99,7 +114,7 @@ const MODEL_MAPPINGS: ModelConfig[] = [
}, },
{ {
Icon: markRaw(LogoGrok), Icon: markRaw(LogoGrok),
keywords: [/^grok-/, /^x-ai\//], keywords: [/^grok-/, /^x-ai\//, /^~x-ai\/grok-latest$/],
}, },
{ {
Icon: markRaw(LogoGemini), Icon: markRaw(LogoGemini),
@@ -164,7 +179,7 @@ const MODEL_MAPPINGS: ModelConfig[] = [
}, },
{ {
Icon: markRaw(LogoZAI), Icon: markRaw(LogoZAI),
keywords: [/zai-/, /(^|\/|-)?glm-?\d/] keywords: [/zai-/, /(^|\/|-)?glm-?\d/, /^~z-ai\/glm-(flash-)?latest$/]
}, },
{ {
Icon: markRaw(LogoGLMV), Icon: markRaw(LogoGLMV),
@@ -196,7 +211,7 @@ const MODEL_MAPPINGS: ModelConfig[] = [
}, },
{ {
Icon: markRaw(LogoMeta), Icon: markRaw(LogoMeta),
keywords: [/llama/, /\/l3/, /meta-llama\//] keywords: [/llama/, /\/l3/, /meta-llama\//, /(^|\/)muse-spark($|-)/, /^meta\//]
}, },
{ {
Icon: markRaw(LogoPerplexity), Icon: markRaw(LogoPerplexity),
+289
View File
@@ -0,0 +1,289 @@
export type StoredToolValue = Readonly<{ type: string; value: string }> | null | undefined;
export const parseToolValue = (stored: StoredToolValue): unknown => {
if (!stored) {
return null;
}
if (stored.type !== "json") {
return stored.value;
}
try {
return JSON.parse(stored.value);
} catch {
// Tool arguments may still be streaming, or come from older records.
return stored.value;
}
};
export const formatToolValue = (stored: StoredToolValue): string => {
const value = parseToolValue(stored);
return typeof value === "string" ? value : JSON.stringify(value, null, 2) ?? "";
};
export interface SearchSource {
sourceId?: string;
title: string;
url: string;
hostname: string;
snippet: string;
}
export const readSearchWarnings = (value: unknown): { engine: string; reason: string }[] => {
if (typeof value !== "object" || value === null || !("warnings" in value) || !Array.isArray(value.warnings)) {
return [];
}
return value.warnings.flatMap((warning: unknown) => {
if (typeof warning !== "object" || warning === null
|| !("engine" in warning) || typeof warning.engine !== "string"
|| !("reason" in warning) || typeof warning.reason !== "string") {
return [];
}
return [{ engine: warning.engine, reason: warning.reason }];
});
};
export const readSearchSources = (value: unknown, options: { deduplicate?: boolean } = {}): SearchSource[] | null => {
// Older tool calls stored the results array directly.
if (typeof value === "object" && value !== null && "results" in value) {
value = value.results;
}
if (!Array.isArray(value)) {
return null;
}
const sources: SearchSource[] = [];
const seen = new Set<string>();
for (const item of value) {
if (typeof item !== "object" || item === null || !("link" in item) || typeof item.link !== "string") {
continue;
}
let url: URL;
try {
url = new URL(item.link);
} catch {
continue;
}
if (url.protocol !== "https:" && url.protocol !== "http:") {
continue;
}
url.hash = "";
if (options.deduplicate !== false && seen.has(url.href)) {
continue;
}
seen.add(url.href);
sources.push({
...("sourceId" in item && typeof item.sourceId === "string" ? { sourceId: item.sourceId } : {}),
title: "title" in item && typeof item.title === "string" ? item.title : url.hostname,
url: url.href,
hostname: url.hostname,
snippet: "snippet" in item && typeof item.snippet === "string" ? item.snippet : "",
});
}
return sources;
};
export interface ToolDisplayInfo {
icon: string;
verb: string;
target: string;
}
export const formatDuration = (start?: Date | string | number | null, end?: Date | string | number | null): string | null => {
if (!start || !end) {
return null;
}
const startTime = typeof start === "number" ? start : new Date(start).getTime();
const endTime = typeof end === "number" ? end : new Date(end).getTime();
if (Number.isNaN(startTime) || Number.isNaN(endTime) || endTime <= startTime) {
return null;
}
const seconds = (endTime - startTime) / 1000;
if (seconds < 0.05) {
return null;
}
return `${seconds.toFixed(1)}s`;
};
const extractField = (obj: unknown, fields: string[]): string => {
if (typeof obj !== "object" || obj === null) {
return typeof obj === "string" ? obj : "";
}
for (const field of fields) {
if (field in obj) {
const val = (obj as Record<string, unknown>)[field];
if (typeof val === "string" && val.length > 0) {
return val;
}
}
}
return "";
};
export const DOMAIN_COLORS = [
"#eab308", // amber / yellow
"#16a34a", // emerald / green
"#0284c7", // sky / blue
"#ea580c", // orange
"#dc2626", // red
"#7c3aed", // violet
"#0d9488", // teal
"#db2777", // pink
"#4f46e5", // indigo
];
export const getDomainColor = (domain: string): string => {
const clean = domain.replace(/^www\./, "").toLowerCase();
let hash = 0;
for (let i = 0; i < clean.length; i++) {
hash = (hash << 5) - hash + clean.charCodeAt(i);
hash |= 0;
}
const index = Math.abs(hash) % DOMAIN_COLORS.length;
return DOMAIN_COLORS[index] ?? "#eab308";
};
export const domainInitial = (domain: string): string => {
const clean = domain.replace(/^www\./, "").trim();
return clean ? clean.charAt(0).toUpperCase() : "?";
};
export const getFaviconUrl = (hostname: string): string => {
const clean = hostname.replace(/^www\./, "").trim();
return `https://icons.duckduckgo.com/ip3/${clean}.ico`;
};
export const getToolDisplayInfo = (toolCall: {
toolName: string;
status: string;
input?: StoredToolValue;
output?: StoredToolValue;
error?: StoredToolValue;
}): ToolDisplayInfo => {
const rawInput = parseToolValue(toolCall.input);
const rawOutput = parseToolValue(toolCall.output);
const rawError = parseToolValue(toolCall.error);
const isPending = toolCall.status === "pending";
const isFailed = toolCall.status === "failed";
const isCancelled = toolCall.status === "cancelled";
const name = toolCall.toolName.toLowerCase();
const errorSummary = typeof rawError === "string"
? (rawError.trim().split("\n")[0] ?? "")
: extractField(rawError, ["message", "error"]);
if (name === "search" || name.includes("search")) {
const query = extractField(rawInput, ["query", "q"]);
const target = query ? `"${query}"` : "";
const icon = "i-mynaui-search";
if (isPending) {
return { icon, verb: "Searching the web", target };
}
if (isFailed) {
return { icon: "i-mynaui-danger-triangle", verb: "Search failed", target: target || errorSummary };
}
if (isCancelled) {
return { icon, verb: "Search cancelled", target };
}
const sources = readSearchSources(rawOutput);
const count = sources?.length ?? 0;
const verb = count > 0 ? `Searched ${count} ${count === 1 ? "site" : "sites"}` : "Searched the web";
return { icon, verb, target };
}
if (name === "fetchurl" || name === "fetch_url" || name.includes("fetch") || name.includes("scrape")) {
const url = extractField(rawInput, ["url", "link", "uri", "href"]);
const icon = "i-mynaui-link";
if (isPending) {
return { icon, verb: "Fetching URL", target: url };
}
if (isFailed) {
return { icon: "i-mynaui-danger-triangle", verb: "Fetch failed", target: url || errorSummary };
}
if (isCancelled) {
return { icon, verb: "Fetch cancelled", target: url };
}
return { icon, verb: "Fetched URL", target: url };
}
if (name === "python" || name.includes("python") || name.includes("pyeval")) {
const code = extractField(rawInput, ["code", "script", "command"]);
const firstLine = code.trim().split("\n")[0] ?? "";
const icon = "i-mynaui-terminal";
if (isPending) {
return { icon, verb: "Running Python", target: firstLine };
}
if (isFailed) {
return { icon: "i-mynaui-danger-triangle", verb: "Python failed", target: firstLine || errorSummary };
}
if (isCancelled) {
return { icon, verb: "Python cancelled", target: firstLine };
}
return { icon, verb: "Ran Python", target: firstLine };
}
if (name.includes("read") || name.includes("cat") || name.includes("view")) {
const path = extractField(rawInput, ["path", "file", "filePath", "filename"]);
const icon = "i-mynaui-file";
if (isPending) {
return { icon, verb: "Reading", target: path };
}
if (isFailed) {
return { icon: "i-mynaui-danger-triangle", verb: "Read failed", target: path || errorSummary };
}
if (isCancelled) {
return { icon, verb: "Read cancelled", target: path };
}
return { icon, verb: "Read", target: path };
}
if (name.includes("edit") || name.includes("write") || name.includes("modify") || name.includes("patch") || name.includes("save")) {
const path = extractField(rawInput, ["path", "file", "filePath", "filename"]);
const icon = "i-mynaui-edit";
if (isPending) {
return { icon, verb: "Editing", target: path };
}
if (isFailed) {
return { icon: "i-mynaui-danger-triangle", verb: "Edit failed", target: path || errorSummary };
}
if (isCancelled) {
return { icon, verb: "Edit cancelled", target: path };
}
return { icon, verb: "Edited", target: path };
}
if (name.includes("query") || name.includes("sql") || name.includes("database") || name.includes("db")) {
const query = extractField(rawInput, ["query", "sql", "statement", "command"]);
const icon = "i-mynaui-database";
if (isPending) {
return { icon, verb: "Querying", target: query };
}
if (isFailed) {
const target = query || errorSummary;
return { icon: "i-mynaui-database", verb: "Query failed", target };
}
if (isCancelled) {
return { icon, verb: "Query cancelled", target: query };
}
return { icon, verb: "Queried", target: query };
}
const formattedName = toolCall.toolName
.replace(/([A-Z])/g, " $1")
.replace(/[-_]/g, " ")
.trim();
const capitalized = formattedName.charAt(0).toUpperCase() + formattedName.slice(1);
const genericTarget = extractField(rawInput, ["query", "path", "command", "prompt", "url"])
|| (isFailed ? errorSummary : "");
if (isPending) {
return { icon: "i-mynaui-tool", verb: `Running ${capitalized}`, target: genericTarget };
}
if (isFailed) {
return { icon: "i-mynaui-danger-triangle", verb: `${capitalized} failed`, target: genericTarget || errorSummary };
}
if (isCancelled) {
return { icon: "i-mynaui-tool", verb: `${capitalized} cancelled`, target: genericTarget };
}
return { icon: "i-mynaui-tool", verb: capitalized, target: genericTarget };
};
+100 -263
View File
@@ -15,34 +15,29 @@
"@ai-sdk/openai-compatible": "^2.0.41", "@ai-sdk/openai-compatible": "^2.0.41",
"@aws-sdk/client-s3": "^3.1028.0", "@aws-sdk/client-s3": "^3.1028.0",
"@aws-sdk/s3-request-presigner": "^3.1028.0", "@aws-sdk/s3-request-presigner": "^3.1028.0",
"@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@6913", "@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@9489",
"@floating-ui/vue": "^1.1.11", "@floating-ui/vue": "^1.1.11",
"@iconify-json/mynaui": "^1.2.17", "@iconify-json/mynaui": "^1.2.17",
"@nuxt/fonts": "0.14.0", "@nuxt/fonts": "0.14.0",
"@nuxt/hints": "1.0.0-alpha.5", "@nuxt/hints": "1.0.0-alpha.5",
"@nuxt/icon": "2.2.0", "@nuxt/icon": "2.2.0",
"@openrouter/ai-sdk-provider": "^2.5.1", "@openrouter/ai-sdk-provider": "^2.9.0",
"@pydantic/monty": "^0.0.17", "@pydantic/monty": "^0.0.17",
"@sentry/nuxt": "^10.48.0", "@sentry/nuxt": "^10.48.0",
"@tanstack/vue-virtual": "^3.13.23", "@tanstack/vue-virtual": "^3.13.23",
"ai": "^6.0.156", "ai": "^6.0.182",
"ai-sdk-ollama": "^3.8.3", "ai-sdk-ollama": "^3.8.3",
"better-auth": "^1.6.2", "better-auth": "^1.6.2",
"comark": "0.6.2",
"comlink": "^4.4.2", "comlink": "^4.4.2",
"drizzle-orm": "^1.0.0-beta.21", "drizzle-orm": "1.0.0-rc.4-5d5b77c",
"glob": "^13.0.6", "glob": "^13.0.6",
"katex": "0.17.0",
"longcat-ai-sdk-provider": "^0.0.5", "longcat-ai-sdk-provider": "^0.0.5",
"nanoid": "^5.1.7", "nanoid": "^5.1.7",
"nuxt": "^4.4.2", "nuxt": "^4.4.2",
"pg": "^8.20.0", "pg": "^8.20.0",
"rehype-katex": "^7.0.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"shiki": "^3.23.0", "shiki": "^3.23.0",
"unified": "^11.0.5",
"vue": "^3.6.0-beta.10", "vue": "^3.6.0-beta.10",
"vue-router": "^5.0.4", "vue-router": "^5.0.4",
"zod": "^4.3.6", "zod": "^4.3.6",
@@ -77,7 +72,7 @@
"@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.30", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-j3fe/6lUUkHPD/51OgMXN9UD7p1QSQEAlroIinmb3MhJ1s+O0MnqdRa30IM7dRHafNp0FQ9X4YpobY85iMknUQ=="], "@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.30", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-j3fe/6lUUkHPD/51OgMXN9UD7p1QSQEAlroIinmb3MhJ1s+O0MnqdRa30IM7dRHafNp0FQ9X4YpobY85iMknUQ=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="], "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.114", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MqkZ5sd+qiq6RgIxELkoFQXg2/JwK+WCMaot7U+rtrZpWJl3fSyYvc28SC03b256o4F7OXjQtdjTqs81B2w+dA=="],
"@ai-sdk/google": ["@ai-sdk/google@3.0.61", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jEKU1Mjcy5CoicejdJQIzM0ntYwyXR8vtYgAZYriKaOuLAiAhiiU538++fGU3CC9HJH/mL1OfsCwMM3gFiCNsw=="], "@ai-sdk/google": ["@ai-sdk/google@3.0.61", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jEKU1Mjcy5CoicejdJQIzM0ntYwyXR8vtYgAZYriKaOuLAiAhiiU538++fGU3CC9HJH/mL1OfsCwMM3gFiCNsw=="],
@@ -179,20 +174,12 @@
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="],
"@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="],
"@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
"@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="],
"@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="], "@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="],
"@azure/core-http-compat": ["@azure/core-http-compat@2.3.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw=="],
"@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="],
"@azure/core-paging": ["@azure/core-paging@1.6.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA=="],
"@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.22.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg=="], "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.22.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg=="],
"@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], "@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="],
@@ -201,10 +188,6 @@
"@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], "@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="],
"@azure/keyvault-common": ["@azure/keyvault-common@2.0.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.5.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", "@azure/logger": "^1.1.4", "tslib": "^2.2.0" } }, "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w=="],
"@azure/keyvault-keys": ["@azure/keyvault-keys@4.10.0", "", { "dependencies": { "@azure-rest/core-client": "^2.3.3", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-http-compat": "^2.2.0", "@azure/core-lro": "^2.7.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.19.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/keyvault-common": "^2.0.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag=="],
"@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="],
"@azure/msal-browser": ["@azure/msal-browser@4.29.0", "", { "dependencies": { "@azure/msal-common": "15.15.0" } }, "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w=="], "@azure/msal-browser": ["@azure/msal-browser@4.29.0", "", { "dependencies": { "@azure/msal-common": "15.15.0" } }, "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w=="],
@@ -265,9 +248,9 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@better-auth/core": ["@better-auth/core@1.4.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg=="], "@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@6913", { "peerDependencies": { "@better-auth/core": "1.5.0-beta.13", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0", "prettier": "^3.7.4" } }], "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@9489", { "peerDependencies": { "@better-auth/core": "^1.7.0-beta.3", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0" }, "optionalPeers": ["drizzle-orm"] }],
"@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "kysely": "^0.27.0 || ^0.28.0" }, "optionalPeers": ["kysely"] }, "sha512-YMMm75jek/MNCAFWTAaq/U3VPmFnrwZW4NhBjjAwruHQJEIrSZZaOaUEXuUpFRRBhWqg7OOltQcHMwU/45CkuA=="], "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "kysely": "^0.27.0 || ^0.28.0" }, "optionalPeers": ["kysely"] }, "sha512-YMMm75jek/MNCAFWTAaq/U3VPmFnrwZW4NhBjjAwruHQJEIrSZZaOaUEXuUpFRRBhWqg7OOltQcHMwU/45CkuA=="],
@@ -279,7 +262,7 @@
"@better-auth/telemetry": ["@better-auth/telemetry@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-o4gHKXqizUxVUUYChZZTowLEzdsz3ViBE/fKFzfHqNFUnF+aVt8QsbLSfipq1WpTIXyJVT/SnH0hgSdWxdssbQ=="], "@better-auth/telemetry": ["@better-auth/telemetry@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-o4gHKXqizUxVUUYChZZTowLEzdsz3ViBE/fKFzfHqNFUnF+aVt8QsbLSfipq1WpTIXyJVT/SnH0hgSdWxdssbQ=="],
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
@@ -405,8 +388,6 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@js-joda/core": ["@js-joda/core@5.7.0", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="],
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
"@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="], "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="],
@@ -457,9 +438,9 @@
"@nuxt/vite-builder": ["@nuxt/vite-builder@4.4.2", "", { "dependencies": { "@nuxt/kit": "4.4.2", "@rollup/plugin-replace": "^6.0.3", "@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue-jsx": "^5.1.4", "autoprefixer": "^10.4.27", "consola": "^3.4.2", "cssnano": "^7.1.3", "defu": "^6.1.4", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "get-port-please": "^3.2.0", "jiti": "^2.6.1", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.1", "mocked-exports": "^0.1.1", "nypm": "^0.6.5", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "postcss": "^8.5.8", "seroval": "^1.5.1", "std-env": "^4.0.0", "ufo": "^1.6.3", "unenv": "^2.0.0-rc.24", "vite": "^7.3.1", "vite-node": "^5.3.0", "vite-plugin-checker": "^0.12.0", "vue-bundle-renderer": "^2.2.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0", "@babel/plugin-syntax-jsx": "^7.25.0", "nuxt": "4.4.2", "rolldown": "^1.0.0-beta.38", "rollup-plugin-visualizer": "^6.0.0 || ^7.0.1", "vue": "^3.3.4" }, "optionalPeers": ["@babel/plugin-proposal-decorators", "@babel/plugin-syntax-jsx", "rolldown", "rollup-plugin-visualizer"] }, "sha512-fJaIwMA8ID6BU5EqmoDvnhq4qYDJeWjdHk4jfqy8D3Nm7CoUW0BvX7Ee92XoO05rtUiClGlk/NQ1Ii8hs3ZIbw=="], "@nuxt/vite-builder": ["@nuxt/vite-builder@4.4.2", "", { "dependencies": { "@nuxt/kit": "4.4.2", "@rollup/plugin-replace": "^6.0.3", "@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue-jsx": "^5.1.4", "autoprefixer": "^10.4.27", "consola": "^3.4.2", "cssnano": "^7.1.3", "defu": "^6.1.4", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "get-port-please": "^3.2.0", "jiti": "^2.6.1", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.1", "mocked-exports": "^0.1.1", "nypm": "^0.6.5", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "postcss": "^8.5.8", "seroval": "^1.5.1", "std-env": "^4.0.0", "ufo": "^1.6.3", "unenv": "^2.0.0-rc.24", "vite": "^7.3.1", "vite-node": "^5.3.0", "vite-plugin-checker": "^0.12.0", "vue-bundle-renderer": "^2.2.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0", "@babel/plugin-syntax-jsx": "^7.25.0", "nuxt": "4.4.2", "rolldown": "^1.0.0-beta.38", "rollup-plugin-visualizer": "^6.0.0 || ^7.0.1", "vue": "^3.3.4" }, "optionalPeers": ["@babel/plugin-proposal-decorators", "@babel/plugin-syntax-jsx", "rolldown", "rollup-plugin-visualizer"] }, "sha512-fJaIwMA8ID6BU5EqmoDvnhq4qYDJeWjdHk4jfqy8D3Nm7CoUW0BvX7Ee92XoO05rtUiClGlk/NQ1Ii8hs3ZIbw=="],
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.9.0", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Seva+NCa0WUQnJIUE5GzHsUv1WTIeyqwz0ELl2VtS6NP+eF+77yCXGFVOMbvoCM7QMjlnhv7931e89R+8pJdcQ=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="],
@@ -977,33 +958,27 @@
"@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.23", "", { "dependencies": { "@tanstack/virtual-core": "3.13.23" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-b5jPluAR6U3eOq6GWAYSpj3ugnAIZgGR0e6aGAgyRse0Yu6MVQQ0ZWm9SArSXWtageogn6bkVD8D//c4IjW3xQ=="], "@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.23", "", { "dependencies": { "@tanstack/virtual-core": "3.13.23" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-b5jPluAR6U3eOq6GWAYSpj3ugnAIZgGR0e6aGAgyRse0Yu6MVQQ0ZWm9SArSXWtageogn6bkVD8D//c4IjW3xQ=="],
"@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="],
"@tootallnate/once": ["@tootallnate/once@1.1.2", "", {}, "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="], "@tootallnate/once": ["@tootallnate/once@1.1.2", "", {}, "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="], "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
"@types/mssql": ["@types/mssql@9.1.9", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-P0nCgw6vzY23UxZMnbI4N7fnLGANt4LI4yvxze1paPj+LuN28cFv5EI+QidP8udnId/BKhkcRhm/BleNsjK65A=="],
"@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="],
@@ -1013,8 +988,6 @@
"@types/pg-pool": ["@types/pg-pool@2.0.7", "", { "dependencies": { "@types/pg": "*" } }, "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng=="], "@types/pg-pool": ["@types/pg-pool@2.0.7", "", { "dependencies": { "@types/pg": "*" } }, "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng=="],
"@types/readable-stream": ["@types/readable-stream@4.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
"@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="],
@@ -1023,7 +996,7 @@
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="], "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
"@unhead/vue": ["@unhead/vue@2.1.13", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.13" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-HYy0shaHRnLNW9r85gppO8IiGz0ONWVV3zGdlT8CQ0tbTwixznJCIiyqV4BSV1aIF1jJIye0pd1p/k6Eab8Z/A=="], "@unhead/vue": ["@unhead/vue@2.1.13", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.13" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-HYy0shaHRnLNW9r85gppO8IiGz0ONWVV3zGdlT8CQ0tbTwixznJCIiyqV4BSV1aIF1jJIye0pd1p/k6Eab8Z/A=="],
@@ -1077,7 +1050,7 @@
"@vercel/nft": ["@vercel/nft@0.27.10", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0-rc.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg=="], "@vercel/nft": ["@vercel/nft@0.27.10", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0-rc.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg=="],
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="], "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="],
@@ -1171,7 +1144,7 @@
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
"ai": ["ai@6.0.156", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uyi/5LYbugHQxZsR2PeAFOZEL4WqKkzZw4pv0nQvvdgxgVOsM7snOmGrYkp5fShxH/vnd08SXvHCVTX7oUW7xQ=="], "ai": ["ai@6.0.182", "", { "dependencies": { "@ai-sdk/gateway": "3.0.114", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ooJdziFjYrYRcsCx107roqA8gDTI3P82nUfroNWIhVvwrkYzEN3W1l50YK+XNqkUew8AiimaW0/SLBewRXMuHQ=="],
"ai-sdk-ollama": ["ai-sdk-ollama@3.8.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.23", "jsonrepair": "^3.13.3", "ollama": "^0.6.3" }, "peerDependencies": { "ai": "^6.0.154" } }, "sha512-KId/S++eb0CgTPFTtHzCGCrO73kXZLK+hyyZx5k8LVqU2XOEHYKVbIwDiQ+hm3okHjnsGehn4zR4QNm14SUM3Q=="], "ai-sdk-ollama": ["ai-sdk-ollama@3.8.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.23", "jsonrepair": "^3.13.3", "ollama": "^0.6.3" }, "peerDependencies": { "ai": "^6.0.154" } }, "sha512-KId/S++eb0CgTPFTtHzCGCrO73kXZLK+hyyZx5k8LVqU2XOEHYKVbIwDiQ+hm3okHjnsGehn4zR4QNm14SUM3Q=="],
@@ -1197,6 +1170,8 @@
"are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="], "are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="], "ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="],
"ast-walker-scope": ["ast-walker-scope@0.8.3", "", { "dependencies": { "@babel/parser": "^7.28.4", "ast-kit": "^2.1.3" } }, "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg=="], "ast-walker-scope": ["ast-walker-scope@0.8.3", "", { "dependencies": { "@babel/parser": "^7.28.4", "ast-kit": "^2.1.3" } }, "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg=="],
@@ -1209,8 +1184,6 @@
"b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="], "b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="], "balanced-match": ["balanced-match@4.0.3", "", {}, "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g=="],
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
@@ -1227,7 +1200,7 @@
"birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="], "birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="],
"bl": ["bl@6.1.6", "", { "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg=="], "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
@@ -1261,8 +1234,6 @@
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
@@ -1293,11 +1264,13 @@
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"comark": ["comark@0.6.2", "", { "dependencies": { "entities": "^8.0.0", "htmlparser2": "^12.0.0", "js-yaml": "^5.2.1", "markdown-exit": "1.1.0-beta.2" }, "peerDependencies": { "beautiful-mermaid": "^1.1.3", "katex": "^0.17.0", "rangi": "^2.2.0", "shiki": "^4.3.1" }, "optionalPeers": ["beautiful-mermaid", "katex", "rangi", "shiki"] }, "sha512-hlWa7KlGPfRxovQavnHZlLlTUKrfwE7/o4ewCY0FzqFtRvVT8VdslrumQwELIiK9utsurpoSWCBFj1yVMtDOtw=="],
"comlink": ["comlink@4.4.2", "", {}, "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g=="], "comlink": ["comlink@4.4.2", "", {}, "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
@@ -1355,8 +1328,6 @@
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
@@ -1389,13 +1360,13 @@
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], "domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="],
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], "domhandler": ["domhandler@6.0.1", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], "domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="],
"dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="], "dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="],
@@ -1403,7 +1374,7 @@
"drizzle-kit": ["drizzle-kit@1.0.0-beta.21", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-qG1vkkXPhz9GJ6RZhM/DvzL5jeDNYn35cyGeFA0+8iZQ6GTP4FhbOltNDHh/8XNmcI89b40lXJuH1PdxaA7zdQ=="], "drizzle-kit": ["drizzle-kit@1.0.0-beta.21", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-qG1vkkXPhz9GJ6RZhM/DvzL5jeDNYn35cyGeFA0+8iZQ6GTP4FhbOltNDHh/8XNmcI89b40lXJuH1PdxaA7zdQ=="],
"drizzle-orm": ["drizzle-orm@1.0.0-beta.21", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-HZcIbVn5J9T/Z91Wj12Pn7Pi8/1aykS/GPJf2lXeZnEuPjxaBfQ+YAt0Sl+XI+9R/D1BpK+2fdIqbpuaTbcvqA=="], "drizzle-orm": ["drizzle-orm@1.0.0-rc.4-5d5b77c", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql-pg": ">=4.0.0-beta.58 || >=4.0.0", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@sinclair/typebox": ">=0.34.8", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "arktype": ">=2.0.0", "better-sqlite3": ">=9.3.0", "bun-types": "*", "effect": ">=4.0.0-beta.58 || >=4.0.0", "expo-sqlite": ">=14.0.0", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5", "typebox": ">=1.0.0", "valibot": ">=1.0.0-beta.7", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@sinclair/typebox", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/mssql", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "arktype", "better-sqlite3", "bun-types", "effect", "expo-sqlite", "mssql", "mysql2", "pg", "postgres", "sql.js", "sqlite3", "typebox", "valibot", "zod"] }, "sha512-Oq9W4B11PracWY9cuCLTFT+JA5kXqR6rBmWcM8oh0xwLgCviVl9wh6ZuWX7Zpv1Jir/8W+M37syvjSYtj1f5iA=="],
"duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="], "duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
@@ -1425,7 +1396,7 @@
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
@@ -1469,8 +1440,6 @@
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
@@ -1569,30 +1538,16 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
"hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="],
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
"hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
"hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="], "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"htmlparser2": ["htmlparser2@12.0.0", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "domutils": "^4.0.2", "entities": "^8.0.0" } }, "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
@@ -1661,8 +1616,6 @@
"is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
"is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
@@ -1685,10 +1638,10 @@
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
"js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"js-yaml": ["js-yaml@5.2.3", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.mjs" } }, "sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw=="],
"jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="], "jsbi": ["jsbi@4.3.2", "", {}, "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -1709,7 +1662,7 @@
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
"katex": ["katex@0.16.28", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg=="], "katex": ["katex@0.17.0", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
@@ -1749,6 +1702,8 @@
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"linkify-it": ["linkify-it@5.0.2", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q=="],
"listhen": ["listhen@1.9.0", "", { "dependencies": { "@parcel/watcher": "^2.4.1", "@parcel/watcher-wasm": "^2.4.1", "citty": "^0.1.6", "clipboardy": "^4.0.0", "consola": "^3.2.3", "crossws": ">=0.2.0 <0.4.0", "defu": "^6.1.4", "get-port-please": "^3.1.2", "h3": "^1.12.0", "http-shutdown": "^1.2.2", "jiti": "^2.1.2", "mlly": "^1.7.1", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", "ufo": "^1.5.4", "untun": "^0.1.3", "uqr": "^0.1.2" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg=="], "listhen": ["listhen@1.9.0", "", { "dependencies": { "@parcel/watcher": "^2.4.1", "@parcel/watcher-wasm": "^2.4.1", "citty": "^0.1.6", "clipboardy": "^4.0.0", "consola": "^3.2.3", "crossws": ">=0.2.0 <0.4.0", "defu": "^6.1.4", "get-port-please": "^3.1.2", "h3": "^1.12.0", "http-shutdown": "^1.2.2", "jiti": "^2.1.2", "mlly": "^1.7.1", "node-forge": "^1.3.1", "pathe": "^1.1.2", "std-env": "^3.7.0", "ufo": "^1.5.4", "untun": "^0.1.3", "uqr": "^0.1.2" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg=="],
"loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="], "loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="],
@@ -1783,8 +1738,6 @@
"longcat-ai-sdk-provider": ["longcat-ai-sdk-provider@0.0.5", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": ">=6.0.0" } }, "sha512-VbuhUuJNXnjoRYnpKBi3TSqFsHJaRRlzjFC+Rv4cxbzgHl6KTcohNeVvgbB95p49lMl4kGH6I/VxgdPOSkAi8A=="], "longcat-ai-sdk-provider": ["longcat-ai-sdk-provider@0.0.5", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": ">=6.0.0" } }, "sha512-VbuhUuJNXnjoRYnpKBi3TSqFsHJaRRlzjFC+Rv4cxbzgHl6KTcohNeVvgbB95p49lMl4kGH6I/VxgdPOSkAi8A=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
"magic-regexp": ["magic-regexp@0.10.0", "", { "dependencies": { "estree-walker": "^3.0.3", "magic-string": "^0.30.12", "mlly": "^1.7.2", "regexp-tree": "^0.1.27", "type-level-regexp": "~0.1.17", "ufo": "^1.5.4", "unplugin": "^2.0.0" } }, "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg=="], "magic-regexp": ["magic-regexp@0.10.0", "", { "dependencies": { "estree-walker": "^3.0.3", "magic-string": "^0.30.12", "mlly": "^1.7.2", "regexp-tree": "^0.1.27", "type-level-regexp": "~0.1.17", "ufo": "^1.5.4", "unplugin": "^2.0.0" } }, "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg=="],
@@ -1797,96 +1750,24 @@
"make-fetch-happen": ["make-fetch-happen@9.1.0", "", { "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^6.0.0", "minipass": "^3.1.3", "minipass-collect": "^1.0.2", "minipass-fetch": "^1.3.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.2", "promise-retry": "^2.0.1", "socks-proxy-agent": "^6.0.0", "ssri": "^8.0.0" } }, "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg=="], "make-fetch-happen": ["make-fetch-happen@9.1.0", "", { "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^6.0.0", "minipass": "^3.1.3", "minipass-collect": "^1.0.2", "minipass-fetch": "^1.3.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.2", "promise-retry": "^2.0.1", "socks-proxy-agent": "^6.0.0", "ssri": "^8.0.0" } }, "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg=="],
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], "markdown-exit": ["markdown-exit@1.1.0-beta.2", "", { "dependencies": { "@types/linkify-it": "^5.0.0", "@types/mdurl": "^2.0.0", "entities": "^7.0.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" } }, "sha512-8CzMGVlFZ4DEfnc8KU+4ycUW2SIOuiXqCHD7z51ecVEi/weyc0f2ylQbCm4KoKuVlTZSuMUMnWT0hTyquZ7anQ=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
"mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="],
"mdast-util-newline-to-break": ["mdast-util-newline-to-break@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0" } }, "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog=="],
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"mdurl": ["mdurl@2.1.0", "", {}, "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
"micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="],
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
@@ -1937,8 +1818,6 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mssql": ["mssql@11.0.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.5.0", "commander": "^11.0.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", "tedious": "^18.2.1" }, "bin": { "mssql": "bin/mssql" } }, "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w=="],
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
"nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], "nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="],
@@ -1949,8 +1828,6 @@
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
"native-duplexpair": ["native-duplexpair@1.0.0", "", {}, "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA=="],
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
@@ -2005,9 +1882,9 @@
"onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
@@ -2029,8 +1906,6 @@
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
@@ -2141,8 +2016,6 @@
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"pretty-bytes": ["pretty-bytes@7.1.0", "", {}, "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw=="], "pretty-bytes": ["pretty-bytes@7.1.0", "", {}, "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
@@ -2155,12 +2028,14 @@
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
@@ -2171,6 +2046,8 @@
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"rangi": ["rangi@2.2.0", "", { "bin": { "rangi": "dist/cli.mjs", "rcat": "dist/cli.mjs" } }, "sha512-zMXLNs+3ejB2dg5kjJdNbllNq6FrdtEbHA9f3POxbSIHb0CyslALbY8qSDCE2r+/6Ku7xaVHKtnC5qHbF2SgNg=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"rc9": ["rc9@3.0.0", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.5" } }, "sha512-MGOue0VqscKWQ104udASX/3GYDcKyPI4j4F8gu/jHHzglpmy9a/anZK3PNe8ug6aZFl+9GxLtdhe3kVZuMaQbA=="], "rc9": ["rc9@3.0.0", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.5" } }, "sha512-MGOue0VqscKWQ104udASX/3GYDcKyPI4j4F8gu/jHHzglpmy9a/anZK3PNe8ug6aZFl+9GxLtdhe3kVZuMaQbA=="],
@@ -2193,20 +2070,6 @@
"regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="], "regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="],
"rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="],
"remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="],
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
"remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="],
@@ -2307,8 +2170,6 @@
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"sqlite3": ["sqlite3@5.1.7", "", { "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.1", "tar": "^6.1.11" }, "optionalDependencies": { "node-gyp": "8.x" } }, "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog=="], "sqlite3": ["sqlite3@5.1.7", "", { "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.1", "tar": "^6.1.11" }, "optionalDependencies": { "node-gyp": "8.x" } }, "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog=="],
"srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="], "srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="],
@@ -2367,10 +2228,6 @@
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], "tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
"tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="],
"tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="],
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="], "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
"terser-webpack-plugin": ["terser-webpack-plugin@5.3.16", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q=="], "terser-webpack-plugin": ["terser-webpack-plugin@5.3.16", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q=="],
@@ -2397,8 +2254,6 @@
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
@@ -2411,6 +2266,8 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="], "ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
@@ -2431,8 +2288,6 @@
"unicorn-magic": ["unicorn-magic@0.4.0", "", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="], "unicorn-magic": ["unicorn-magic@0.4.0", "", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], "unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="],
"unimport": ["unimport@6.0.2", "", { "dependencies": { "acorn": "^8.16.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1" } }, "sha512-ZSOkrDw380w+KIPniY3smyXh2h7H9v2MNr9zejDuh239o5sdea44DRAYrv+rfUi2QGT186P2h0GPGKvy8avQ5g=="], "unimport": ["unimport@6.0.2", "", { "dependencies": { "acorn": "^8.16.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1" } }, "sha512-ZSOkrDw380w+KIPniY3smyXh2h7H9v2MNr9zejDuh239o5sdea44DRAYrv+rfUi2QGT186P2h0GPGKvy8avQ5g=="],
@@ -2441,14 +2296,10 @@
"unique-slug": ["unique-slug@2.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w=="], "unique-slug": ["unique-slug@2.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w=="],
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
@@ -2481,8 +2332,6 @@
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"vite": ["vite@8.0.0-beta.15", "", { "dependencies": { "@oxc-project/runtime": "0.114.0", "lightningcss": "^1.31.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-rc.5", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-RHX7IvsJlEfjyA1rS7MY0UsmF91etdLAamslHR5lfuO3W/BXRdXm2tRE64ztpSPZbKqB4wAAZ0AwtF6QzfKZLA=="], "vite": ["vite@8.0.0-beta.15", "", { "dependencies": { "@oxc-project/runtime": "0.114.0", "lightningcss": "^1.31.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-rc.5", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-RHX7IvsJlEfjyA1rS7MY0UsmF91etdLAamslHR5lfuO3W/BXRdXm2tRE64ztpSPZbKqB4wAAZ0AwtF6QzfKZLA=="],
@@ -2513,8 +2362,6 @@
"watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="], "watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="], "web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
@@ -2567,6 +2414,10 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q=="], "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q=="],
"@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.973.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q=="], "@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.973.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q=="],
@@ -2603,27 +2454,7 @@
"@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], "@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@better-auth/core/better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="], "@bomb.sh/tab/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"@better-auth/kysely-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
"@better-auth/kysely-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
"@better-auth/memory-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
"@better-auth/memory-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
"@better-auth/mongo-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
"@better-auth/mongo-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
"@better-auth/prisma-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
"@better-auth/prisma-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
"@better-auth/telemetry/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
"@better-auth/telemetry/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
"@dxup/nuxt/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "@dxup/nuxt/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
@@ -2681,8 +2512,6 @@
"@nuxt/vite-builder/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], "@nuxt/vite-builder/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
"@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
"@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], "@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
"@opentelemetry/sql-common/@opentelemetry/core": ["@opentelemetry/core@2.5.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA=="], "@opentelemetry/sql-common/@opentelemetry/core": ["@opentelemetry/core@2.5.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA=="],
@@ -2713,22 +2542,14 @@
"@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"@sentry/cloudflare/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
"@sentry/node/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
"@sentry/nuxt/@nuxt/kit": ["@nuxt/kit@3.21.1", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-QORZRjcuTKgo++XP1Pc2c2gqwRydkaExrIRfRI9vFsPA3AzuHVn5Gfmbv1ic8y34e78mr5DMBvJlelUaeOuajg=="], "@sentry/nuxt/@nuxt/kit": ["@nuxt/kit@3.21.1", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-QORZRjcuTKgo++XP1Pc2c2gqwRydkaExrIRfRI9vFsPA3AzuHVn5Gfmbv1ic8y34e78mr5DMBvJlelUaeOuajg=="],
"@types/connect/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], "@types/connect/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
"@types/mssql/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
"@types/mysql/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], "@types/mysql/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
"@types/pg-pool/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], "@types/pg-pool/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
"@types/readable-stream/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
"@types/tedious/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], "@types/tedious/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
"@typespec/ts-http-runtime/http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "@typespec/ts-http-runtime/http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
@@ -2757,6 +2578,8 @@
"@vue/compiler-core/@vue/shared": ["@vue/shared@3.6.0-beta.10", "", {}, "sha512-13JUfIAd06F+IBnObE8mExDAMOknPIBjPBUN2JeemmuQwj5i20GduCLbHLVbxSkpFD0RGH4z2mOxUUdD+8M/Aw=="], "@vue/compiler-core/@vue/shared": ["@vue/shared@3.6.0-beta.10", "", {}, "sha512-13JUfIAd06F+IBnObE8mExDAMOknPIBjPBUN2JeemmuQwj5i20GduCLbHLVbxSkpFD0RGH4z2mOxUUdD+8M/Aw=="],
"@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vue/compiler-dom/@vue/shared": ["@vue/shared@3.6.0-beta.10", "", {}, "sha512-13JUfIAd06F+IBnObE8mExDAMOknPIBjPBUN2JeemmuQwj5i20GduCLbHLVbxSkpFD0RGH4z2mOxUUdD+8M/Aw=="], "@vue/compiler-dom/@vue/shared": ["@vue/shared@3.6.0-beta.10", "", {}, "sha512-13JUfIAd06F+IBnObE8mExDAMOknPIBjPBUN2JeemmuQwj5i20GduCLbHLVbxSkpFD0RGH4z2mOxUUdD+8M/Aw=="],
@@ -2787,6 +2610,10 @@
"@vue/server-renderer/@vue/shared": ["@vue/shared@3.6.0-beta.10", "", {}, "sha512-13JUfIAd06F+IBnObE8mExDAMOknPIBjPBUN2JeemmuQwj5i20GduCLbHLVbxSkpFD0RGH4z2mOxUUdD+8M/Aw=="], "@vue/server-renderer/@vue/shared": ["@vue/shared@3.6.0-beta.10", "", {}, "sha512-13JUfIAd06F+IBnObE8mExDAMOknPIBjPBUN2JeemmuQwj5i20GduCLbHLVbxSkpFD0RGH4z2mOxUUdD+8M/Aw=="],
"ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
@@ -2799,16 +2626,14 @@
"ast-walker-scope/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], "ast-walker-scope/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"better-auth/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="],
"better-auth/@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-KawrNNuhgmpcc5PgLs6HesMckxCscz5J+BQ99iRmU1cLzG/A87IcydrmYtep+K8WHPN0HmZ/i4z/nOBCtxE2qA=="], "better-auth/@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-KawrNNuhgmpcc5PgLs6HesMckxCscz5J+BQ99iRmU1cLzG/A87IcydrmYtep+K8WHPN0HmZ/i4z/nOBCtxE2qA=="],
"better-auth/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
"better-call/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
"better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], "better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"browserslist/caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="], "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="],
"c12/giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], "c12/giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
@@ -2827,9 +2652,11 @@
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], "css-select/domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "css-select/domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
@@ -2859,8 +2686,6 @@
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"listhen/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "listhen/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
@@ -2875,6 +2700,8 @@
"make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"markdown-exit/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@@ -2913,8 +2740,6 @@
"nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], "readdir-glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
@@ -2939,14 +2764,14 @@
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], "tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], "tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"tedious/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"tsx/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "tsx/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
@@ -2993,6 +2818,8 @@
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@ai-sdk/gateway/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
"@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], "@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="],
"@aws-crypto/crc32c/@aws-sdk/types/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], "@aws-crypto/crc32c/@aws-sdk/types/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="],
@@ -3013,10 +2840,6 @@
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@better-auth/core/better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
"@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"@dxup/nuxt/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "@dxup/nuxt/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="], "@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="],
@@ -3075,16 +2898,12 @@
"@types/connect/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "@types/connect/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/mssql/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/mysql/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "@types/mysql/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/pg-pool/@types/pg/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], "@types/pg-pool/@types/pg/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
"@types/pg-pool/@types/pg/pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="], "@types/pg-pool/@types/pg/pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="],
"@types/readable-stream/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@types/tedious/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "@types/tedious/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@unocss/cli/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "@unocss/cli/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
@@ -3169,6 +2988,8 @@
"@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"ai/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
"archiver-utils/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "archiver-utils/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], "archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
@@ -3177,6 +2998,12 @@
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"css-select/domhandler/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"css-select/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"css-select/domutils/domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
"fontaine/css-tree/mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], "fontaine/css-tree/mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
@@ -3357,12 +3184,8 @@
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"tar-fs/tar-stream/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"tedious/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
@@ -3497,8 +3320,6 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
"@nuxt/kit/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "@nuxt/kit/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"@nuxt/kit/mlly/pkg-types/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], "@nuxt/kit/mlly/pkg-types/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
@@ -3523,14 +3344,20 @@
"@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@types/pg-pool/@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "@types/pg-pool/@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"@unocss/transformer-attributify-jsx/oxc-parser/@oxc-parser/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], "@unocss/transformer-attributify-jsx/oxc-parser/@oxc-parser/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="],
"@vercel/nft/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "@vercel/nft/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"@vue-macros/common/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "@vue-macros/common/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
@@ -3539,6 +3366,8 @@
"cacache/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "cacache/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"css-select/domutils/dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"node-gyp/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "node-gyp/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"nuxt/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "nuxt/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
@@ -3565,16 +3394,20 @@
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"tar-fs/tar-stream/bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"unimport/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "unimport/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"unimport/mlly/pkg-types/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], "unimport/mlly/pkg-types/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
"vue-router/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"wide-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "wide-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@nuxt/nitro-server/vue/@vue/compiler-dom/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"@nuxt/nitro-server/vue/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@nuxt/nitro-server/vue/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@nuxt/nitro-server/vue/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"@vercel/nft/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@vercel/nft/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@@ -3583,8 +3416,12 @@
"node-gyp/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "node-gyp/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"nuxt/vue/@vue/compiler-dom/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"nuxt/vue/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "nuxt/vue/@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"nuxt/vue/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
} }
} }
+14
View File
@@ -0,0 +1,14 @@
# Tool renderers
Tool presentation lives in `app/components/Message/Agent/Tool/`.
To add a renderer:
1. Create a Vue component accepting `toolCall: Readonly<ToolCall>` and optional `part?: MessagePart`.
2. Register its exact tool name in `renderers.ts`.
3. Wrap your custom presentation with `Disclosure.vue`. Provide `title`, `subtitle`, `icon`, and `customTab` (specifying `id`, `label`, `count`, and optional `copyContent`). Place the custom UI in the named slot matching your tab ID.
The expanded card seamlessly integrates custom renderer tabs alongside standard inspector tabs (`Input`, `Output`, `Trace`, and `Error` on failure). Line counts and duration are formatted automatically. Built-in `Copy` and `Retry` actions allow quick clipboard access and retry handling. Unknown tools fall back to `Generic.vue` with smart heuristics for common tool signatures (read, edit, query, fetch, python).
Use `parseToolValue` from `app/utils/tool-rendering.ts` to decode stored values as `unknown`, then validate the tool-specific shape. Inputs and outputs can change while streaming, so derive presentation with computed values and handle pending, completed, failed, and cancelled calls. Never execute tool output as markup or code.
@@ -0,0 +1,4 @@
-- Custom SQL migration file, put your code below! --
UPDATE files
SET url = regexp_replace(url, '^https?://[^/]+', '')
WHERE url ~ '^https?://[^/]+';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,58 @@
-- Convert naive timestamps to timestamptz.
-- Existing values were written as session-local wall clocks (via now()/defaultNow)
-- or occasionally as UTC components (drizzle mapToDriverValue toISOString).
-- Interpret stored values in the current session TimeZone so local wall-clock
-- rows (the common path for messages) keep the same absolute instant.
--
-- Drizzle reads naive `timestamp` as UTC (textToDateWithTz), which shifted
-- displayed times by the server offset (e.g. -5h in America/Chicago).
ALTER TABLE "providers"
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "models"
ALTER COLUMN "released_at" TYPE timestamptz USING "released_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "agents"
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "topics"
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "messages"
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "message_parts"
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
ALTER COLUMN "last_updated_at" TYPE timestamptz USING "last_updated_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "tool_calls"
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "files"
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "attachments"
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "users"
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "sessions"
ALTER COLUMN "expires_at" TYPE timestamptz USING "expires_at" AT TIME ZONE current_setting('TimeZone'),
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "accounts"
ALTER COLUMN "access_token_expires_at" TYPE timestamptz USING "access_token_expires_at" AT TIME ZONE current_setting('TimeZone'),
ALTER COLUMN "refresh_token_expires_at" TYPE timestamptz USING "refresh_token_expires_at" AT TIME ZONE current_setting('TimeZone'),
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
ALTER TABLE "verifications"
ALTER COLUMN "expires_at" TYPE timestamptz USING "expires_at" AT TIME ZONE current_setting('TimeZone'),
ALTER COLUMN "created_at" TYPE timestamptz USING "created_at" AT TIME ZONE current_setting('TimeZone'),
ALTER COLUMN "updated_at" TYPE timestamptz USING "updated_at" AT TIME ZONE current_setting('TimeZone');
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
ALTER TABLE "topics" ADD COLUMN "rename_attempt_id" text;
--> statement-breakpoint
ALTER TABLE "topics" ADD COLUMN "rename_expires_at" timestamp with time zone;
--> statement-breakpoint
ALTER TABLE "topics" ADD COLUMN "title_source" text DEFAULT 'manual' NOT NULL;
File diff suppressed because it is too large Load Diff
+36 -31
View File
@@ -11,6 +11,11 @@ import {
} from 'drizzle-orm/pg-core'; } from 'drizzle-orm/pg-core';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
// Always use timestamptz. Drizzle treats naive `timestamp` values as UTC on read
// (via textToDateWithTz), which shifts displayed times by the server offset
// (e.g. -5h in America/Chicago).
const timestamptz = (name: string) => timestamp(name, { withTimezone: true });
export const roleEnum = pgEnum('role', ['user', 'assistant']); export const roleEnum = pgEnum('role', ['user', 'assistant']);
export const partTypeEnum = pgEnum('part_type', ['reasoning', 'text', 'tool-call', 'file']); export const partTypeEnum = pgEnum('part_type', ['reasoning', 'text', 'tool-call', 'file']);
export const statusEnum = pgEnum('status', ['pending', 'completed', 'failed', 'cancelled']); export const statusEnum = pgEnum('status', ['pending', 'completed', 'failed', 'cancelled']);
@@ -28,10 +33,6 @@ export const settings = pgTable('settings', {
enabled: boolean; enabled: boolean;
modelId: string | null; modelId: string | null;
}; };
rerank: {
enabled: boolean;
modelId: string | null;
};
}>().notNull().default({} as any), }>().notNull().default({} as any),
appearance: jsonb('appearance').$type<{ appearance: jsonb('appearance').$type<{
colorScheme?: 'light' | 'dark' | 'system'; colorScheme?: 'light' | 'dark' | 'system';
@@ -54,8 +55,8 @@ export const providers = pgTable('providers', {
apiKey?: string; apiKey?: string;
apiProxyUrl?: string; apiProxyUrl?: string;
}>().default({}), }>().default({}),
createdAt: timestamp('created_at').notNull().defaultNow(), createdAt: timestamptz('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()), updatedAt: timestamptz('updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()),
}, (table) => [ }, (table) => [
index('providers_userId_idx').on(table.userId), index('providers_userId_idx').on(table.userId),
]); ]);
@@ -89,7 +90,7 @@ export const models = pgTable('models', {
supportedParameters: text('supported_parameters').array(), supportedParameters: text('supported_parameters').array(),
isCustom: boolean('is_custom').notNull().default(false), isCustom: boolean('is_custom').notNull().default(false),
enabled: boolean('enabled').notNull().default(true), enabled: boolean('enabled').notNull().default(true),
releasedAt: timestamp('released_at'), releasedAt: timestamptz('released_at'),
}, (table) => [ }, (table) => [
index('models_providerId_idx').on(table.providerId), index('models_providerId_idx').on(table.providerId),
index('models_userId_idx').on(table.userId), index('models_userId_idx').on(table.userId),
@@ -105,11 +106,12 @@ export const agents = pgTable('agents', {
config: jsonb('config').$type<{ config: jsonb('config').$type<{
search?: { search?: {
enabled: boolean; enabled: boolean;
rerank: boolean; };
maxResults: number; tools?: {
} python?: boolean;
};
}>().default({}), }>().default({}),
createdAt: timestamp('created_at').notNull().defaultNow(), createdAt: timestamptz('created_at').notNull().defaultNow(),
}, (table) => [ }, (table) => [
index('agents_userId_idx').on(table.userId), index('agents_userId_idx').on(table.userId),
]); ]);
@@ -120,7 +122,10 @@ export const topics = pgTable('topics', {
agentId: text('agent_id').notNull().references(() => agents.id, { onDelete: 'cascade' }), agentId: text('agent_id').notNull().references(() => agents.id, { onDelete: 'cascade' }),
name: text('name').notNull(), name: text('name').notNull(),
renaming: boolean('renaming').default(false), renaming: boolean('renaming').default(false),
createdAt: timestamp('created_at').notNull().defaultNow(), renameAttemptId: text('rename_attempt_id'),
renameExpiresAt: timestamptz('rename_expires_at'),
titleSource: text('title_source').notNull().default('manual'),
createdAt: timestamptz('created_at').notNull().defaultNow(),
}, (table) => [ }, (table) => [
index('topics_agentId_idx').on(table.agentId), index('topics_agentId_idx').on(table.agentId),
index('topics_userId_idx').on(table.userId), index('topics_userId_idx').on(table.userId),
@@ -136,8 +141,8 @@ export const messages = pgTable('messages', {
content: text('content'), content: text('content'),
activeChildId: text('active_child_id'), activeChildId: text('active_child_id'),
deleted: boolean('deleted').default(false), deleted: boolean('deleted').default(false),
updatedAt: timestamp('updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()), updatedAt: timestamptz('updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()),
createdAt: timestamp('created_at').notNull().defaultNow(), createdAt: timestamptz('created_at').notNull().defaultNow(),
}, (table) => [ }, (table) => [
index('messages_topicId_idx').on(table.topicId), index('messages_topicId_idx').on(table.topicId),
index('messages_parentMessageId_idx').on(table.parentMessageId), index('messages_parentMessageId_idx').on(table.parentMessageId),
@@ -154,8 +159,8 @@ export const messageParts = pgTable('message_parts', {
content: text('content'), content: text('content'),
providerOptions: jsonb('provider_options'), providerOptions: jsonb('provider_options'),
finished: boolean('finished').notNull().default(false), finished: boolean('finished').notNull().default(false),
createdAt: timestamp('created_at').notNull().defaultNow(), createdAt: timestamptz('created_at').notNull().defaultNow(),
lastUpdatedAt: timestamp('last_updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()), lastUpdatedAt: timestamptz('last_updated_at').notNull().defaultNow().$onUpdate(() => /* @__PURE__ */ new Date()),
}, (table) => [ }, (table) => [
index('message_parts_topicId_idx').on(table.topicId), index('message_parts_topicId_idx').on(table.topicId),
index('message_parts_messageId_idx').on(table.messageId), index('message_parts_messageId_idx').on(table.messageId),
@@ -179,7 +184,7 @@ export const toolCalls = pgTable('tool_calls', {
type: ToolCallType; type: ToolCallType;
value: string; value: string;
}>(), }>(),
createdAt: timestamp('created_at').notNull().defaultNow(), createdAt: timestamptz('created_at').notNull().defaultNow(),
}, (table) => [ }, (table) => [
index('tool_calls_userId_idx').on(table.userId), index('tool_calls_userId_idx').on(table.userId),
]); ]);
@@ -214,7 +219,7 @@ export const files = pgTable('files', {
mimeType: text('mime_type').notNull(), mimeType: text('mime_type').notNull(),
size: integer('size').notNull().default(0), size: integer('size').notNull().default(0),
url: text('url').notNull(), url: text('url').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(), createdAt: timestamptz('created_at').notNull().defaultNow(),
}, (table) => [ }, (table) => [
index('files_userId_idx').on(table.userId), index('files_userId_idx').on(table.userId),
]); ]);
@@ -235,7 +240,7 @@ export const attachments = pgTable('attachments', {
topicId: text('topic_id').notNull().references(() => topics.id, { onDelete: 'cascade' }), topicId: text('topic_id').notNull().references(() => topics.id, { onDelete: 'cascade' }),
messageId: text('message_id').notNull().references(() => messages.id, { onDelete: 'cascade' }), messageId: text('message_id').notNull().references(() => messages.id, { onDelete: 'cascade' }),
fileId: text('file_id').notNull().references(() => files.id, { onDelete: 'cascade' }), fileId: text('file_id').notNull().references(() => files.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').notNull().defaultNow(), createdAt: timestamptz('created_at').notNull().defaultNow(),
}, (table) => [ }, (table) => [
index('attachments_topicId_idx').on(table.topicId), index('attachments_topicId_idx').on(table.topicId),
index('attachments_messageId_idx').on(table.messageId), index('attachments_messageId_idx').on(table.messageId),
@@ -248,8 +253,8 @@ export const users = pgTable("users", {
email: text("email").notNull().unique(), email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(), emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"), image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamptz("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at") updatedAt: timestamptz("updated_at")
.defaultNow() .defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
@@ -259,10 +264,10 @@ export const sessions = pgTable(
"sessions", "sessions",
{ {
id: text("id").primaryKey(), id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(), expiresAt: timestamptz("expires_at").notNull(),
token: text("token").notNull().unique(), token: text("token").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamptz("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at") updatedAt: timestamptz("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
ipAddress: text("ip_address"), ipAddress: text("ip_address"),
@@ -286,12 +291,12 @@ export const accounts = pgTable(
accessToken: text("access_token"), accessToken: text("access_token"),
refreshToken: text("refresh_token"), refreshToken: text("refresh_token"),
idToken: text("id_token"), idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"), accessTokenExpiresAt: timestamptz("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), refreshTokenExpiresAt: timestamptz("refresh_token_expires_at"),
scope: text("scope"), scope: text("scope"),
password: text("password"), password: text("password"),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamptz("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at") updatedAt: timestamptz("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
}, },
@@ -304,9 +309,9 @@ export const verifications = pgTable(
id: text("id").primaryKey(), id: text("id").primaryKey(),
identifier: text("identifier").notNull(), identifier: text("identifier").notNull(),
value: text("value").notNull(), value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(), expiresAt: timestamptz("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(), createdAt: timestamptz("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at") updatedAt: timestamptz("updated_at")
.defaultNow() .defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date()) .$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(), .notNull(),
+10 -11
View File
@@ -86,7 +86,7 @@ export default defineNuxtConfig({
}, },
optimizeDeps: { optimizeDeps: {
include: [ include: [
// shiki // Shiki
'shiki/core', 'shiki/core',
'shiki/wasm', 'shiki/wasm',
'shiki/engine/javascript', 'shiki/engine/javascript',
@@ -104,23 +104,21 @@ export default defineNuxtConfig({
'@shikijs/langs/json', '@shikijs/langs/json',
'@shikijs/langs/bash', '@shikijs/langs/bash',
'comark',
'comark/plugins/breaks',
'comark/plugins/footnotes',
'comark/plugins/math',
'comlink',
'katex',
'shiki',
'@vue/devtools-core', '@vue/devtools-core',
'@vue/devtools-kit', '@vue/devtools-kit',
'@sentry/nuxt', '@sentry/nuxt',
'@floating-ui/vue', '@floating-ui/vue',
'@nuxt/hints/runtime/hydration/component', '@nuxt/hints/runtime/hydration/component',
'@tanstack/vue-virtual', '@tanstack/vue-virtual',
'shiki',
'better-auth/vue', 'better-auth/vue',
'unified',
'remark-gfm',
'remark-parse',
'remark-rehype',
'remark-math',
'remark-breaks',
'rehype-katex',
'unist-util-visit',
'comlink',
'nanoid', 'nanoid',
'drizzle-orm/pg-core' 'drizzle-orm/pg-core'
], ],
@@ -169,6 +167,7 @@ export default defineNuxtConfig({
public: { public: {
disableSignup: process.env.DISABLE_SIGNUP === 'true' || process.env.DISABLE_SIGNUP === '1', disableSignup: process.env.DISABLE_SIGNUP === 'true' || process.env.DISABLE_SIGNUP === '1',
publicUrl: process.env.NUXT_PUBLIC_URL ?? 'http://localhost:3000',
} }
}, },
+6 -11
View File
@@ -20,34 +20,29 @@
"@ai-sdk/openai-compatible": "^2.0.41", "@ai-sdk/openai-compatible": "^2.0.41",
"@aws-sdk/client-s3": "^3.1028.0", "@aws-sdk/client-s3": "^3.1028.0",
"@aws-sdk/s3-request-presigner": "^3.1028.0", "@aws-sdk/s3-request-presigner": "^3.1028.0",
"@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@6913", "@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@9489",
"@floating-ui/vue": "^1.1.11", "@floating-ui/vue": "^1.1.11",
"@iconify-json/mynaui": "^1.2.17", "@iconify-json/mynaui": "^1.2.17",
"@nuxt/fonts": "0.14.0", "@nuxt/fonts": "0.14.0",
"@nuxt/hints": "1.0.0-alpha.5", "@nuxt/hints": "1.0.0-alpha.5",
"@nuxt/icon": "2.2.0", "@nuxt/icon": "2.2.0",
"@openrouter/ai-sdk-provider": "^2.5.1", "@openrouter/ai-sdk-provider": "^2.9.0",
"@pydantic/monty": "^0.0.17", "@pydantic/monty": "^0.0.17",
"@sentry/nuxt": "^10.48.0", "@sentry/nuxt": "^10.48.0",
"@tanstack/vue-virtual": "^3.13.23", "@tanstack/vue-virtual": "^3.13.23",
"ai": "^6.0.156", "ai": "^6.0.182",
"ai-sdk-ollama": "^3.8.3", "ai-sdk-ollama": "^3.8.3",
"better-auth": "^1.6.2", "better-auth": "^1.6.2",
"comark": "0.6.2",
"comlink": "^4.4.2", "comlink": "^4.4.2",
"drizzle-orm": "^1.0.0-beta.21", "drizzle-orm": "1.0.0-rc.4-5d5b77c",
"glob": "^13.0.6", "glob": "^13.0.6",
"katex": "0.17.0",
"longcat-ai-sdk-provider": "^0.0.5", "longcat-ai-sdk-provider": "^0.0.5",
"nanoid": "^5.1.7", "nanoid": "^5.1.7",
"nuxt": "^4.4.2", "nuxt": "^4.4.2",
"pg": "^8.20.0", "pg": "^8.20.0",
"rehype-katex": "^7.0.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"shiki": "^3.23.0", "shiki": "^3.23.0",
"unified": "^11.0.5",
"vue": "^3.6.0-beta.10", "vue": "^3.6.0-beta.10",
"vue-router": "^5.0.4", "vue-router": "^5.0.4",
"zod": "^4.3.6" "zod": "^4.3.6"
+3 -2
View File
@@ -18,8 +18,9 @@ export default defineEventHandler(async (event) => {
config: z.object({ config: z.object({
search: z.object({ search: z.object({
enabled: z.boolean(), enabled: z.boolean(),
rerank: z.boolean(), }).optional(),
maxResults: z.number().min(1).max(50), tools: z.object({
python: z.boolean().optional(),
}).optional(), }).optional(),
}).optional(), }).optional(),
}) })
+13
View File
@@ -1,9 +1,22 @@
import { and, eq, isNull, lte, or } from "drizzle-orm";
import { topics } from "~~/drizzle/schema";
import { db } from "~~/server/lib/db"; import { db } from "~~/server/lib/db";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
await protectRoute(event); await protectRoute(event);
const userId = event.context.user!.id as string; const userId = event.context.user!.id as string;
// Expired leases also recover jobs interrupted by a server restart.
await db.update(topics).set({
renaming: false,
renameAttemptId: null,
renameExpiresAt: null,
}).where(and(
eq(topics.userId, userId),
eq(topics.renaming, true),
or(isNull(topics.renameExpiresAt), lte(topics.renameExpiresAt, new Date())),
));
const agents = await db.query.agents.findMany({ const agents = await db.query.agents.findMany({
where: { where: {
userId, userId,
+31 -3
View File
@@ -1,4 +1,6 @@
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { auth } from "~~/lib/auth";
import { verifyFileToken } from "~~/server/utils/file-token";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const key = getRouterParam(event, 'key'); const key = getRouterParam(event, 'key');
@@ -6,6 +8,34 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 400, statusMessage: 'Missing file key' }); throw createError({ statusCode: 400, statusMessage: 'Missing file key' });
} }
const query = getQuery(event);
const exp = query.exp ? Number(query.exp) : undefined;
const sig = query.sig as string | undefined;
let authorized = false;
// Path 1: HMAC token (for AI model access)
if (exp && sig && process.env.BETTER_AUTH_SECRET) {
authorized = verifyFileToken(key, exp, sig, process.env.BETTER_AUTH_SECRET);
}
// Path 2: Session auth (for client-side access)
if (!authorized) {
try {
const sessionData = await auth.api.getSession(event);
if (sessionData) {
event.context.user = sessionData.user;
authorized = true;
}
} catch {
// No valid session
}
}
if (!authorized) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
}
const config = useRuntimeConfig(); const config = useRuntimeConfig();
const s3 = new S3Client({ const s3 = new S3Client({
@@ -24,14 +54,12 @@ export default defineEventHandler(async (event) => {
Key: key, Key: key,
})); }));
// 3. Set the correct headers so the browser knows what it's receiving
setHeaders(event, { setHeaders(event, {
'Content-Type': response.ContentType || 'application/octet-stream', 'Content-Type': response.ContentType || 'application/octet-stream',
'Content-Length': response.ContentLength?.toString() || '', 'Content-Length': response.ContentLength?.toString() || '',
'Cache-Control': 'public, max-age=3600', // Optional: cache for 1 hour 'Cache-Control': 'public, max-age=3600',
}); });
// 4. Return the body as a stream directly to the client
return response.Body; return response.Body;
} catch (error: any) { } catch (error: any) {
if (error.name === 'NoSuchKey') { if (error.name === 'NoSuchKey') {
+74 -30
View File
@@ -63,62 +63,106 @@ export default defineEventHandler(async (event) => {
} }
} }
const rootMessage = messagesMap.get(messageId); const targetMessage = messagesMap.get(messageId);
if (!rootMessage) { if (!targetMessage) {
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: 'Invalid message', statusMessage: 'Invalid message',
}); });
} }
if (rootMessage.role === 'user') { if (targetMessage.role === 'user') {
await deeplyDeleteMessage(rootMessage.id, topicId, userId); await deeplyDeleteMessage(targetMessage.id, topicId, userId);
return { ok: true }; return { ok: true };
} }
if (rootMessage.deleted === true) { if (targetMessage.parentMessageId && messagesMap.has(targetMessage.parentMessageId)) {
const childMessage = rootMessage.activeChildId const parent = messagesMap.get(targetMessage.parentMessageId)!;
? rootMessage.children.find(c => c.id === rootMessage.activeChildId) await deeplyDeleteMessage(targetMessage.id, topicId, userId);
: rootMessage.children[0];
if (!childMessage) { const remainingChildren = parent.children.filter((child) => {
console.error('Message not found'); return child.id !== targetMessage.id;
return { ok: false }; });
if (parent.deleted === true && remainingChildren.length === 0) {
await deeplyDeleteMessage(parent.id, topicId, userId);
} else if (parent.activeChildId === targetMessage.id) {
const nextActiveChildId = remainingChildren.length > 0
? (parent.deleted ? remainingChildren[0]!.id : remainingChildren[remainingChildren.length - 1]!.id)
: null;
await db.update(messages).set({ activeChildId: nextActiveChildId }).where(sql`${messages.id} = ${parent.id} AND ${messages.userId} = ${userId}`);
topicEvents.emit(topicId, {
type: 'MESSAGE_UPDATED',
payload: { ...parent, activeChildId: nextActiveChildId },
});
} }
return { ok: true };
}
await deeplyDeleteMessage(childMessage.id, topicId, userId); if (targetMessage.deleted === true) {
const childMessage = targetMessage.activeChildId
? targetMessage.children.find((c) => { return c.id === targetMessage.activeChildId; })
: targetMessage.children[0];
const remainingChildren = rootMessage.children.filter(child => child!.id !== childMessage.id); if (childMessage) {
if (remainingChildren.length === 0) { await deeplyDeleteMessage(childMessage.id, topicId, userId);
await deeplyDeleteMessage(rootMessage.id, topicId, userId); const remainingChildren = targetMessage.children.filter((child) => {
return child.id !== childMessage.id;
});
if (remainingChildren.length === 0) {
await deeplyDeleteMessage(targetMessage.id, topicId, userId);
} else if (targetMessage.activeChildId === childMessage.id) {
const nextActiveChildId = remainingChildren[0]!.id;
await db.update(messages).set({ activeChildId: nextActiveChildId }).where(sql`${messages.id} = ${targetMessage.id} AND ${messages.userId} = ${userId}`);
topicEvents.emit(topicId, {
type: 'MESSAGE_UPDATED',
payload: { ...targetMessage, activeChildId: nextActiveChildId },
});
}
} else {
await deeplyDeleteMessage(targetMessage.id, topicId, userId);
} }
return { ok: true }; return { ok: true };
} }
if ( if (
rootMessage.activeChildId targetMessage.activeChildId
&& rootMessage.children.length > 0 && targetMessage.children.length > 0
) { ) {
const childMessage = rootMessage.children.find(c => c.id === rootMessage.activeChildId); const childMessage = targetMessage.children.find((c) => {
if (!childMessage) { return c.id === targetMessage.activeChildId;
console.error('Message not found'); });
return { ok: false }; if (childMessage) {
await deeplyDeleteMessage(childMessage.id, topicId, userId);
const remainingChildren = targetMessage.children.filter((child) => {
return child.id !== childMessage.id;
});
const nextActiveChildId = remainingChildren.length > 0
? remainingChildren[remainingChildren.length - 1]!.id
: null;
await db.update(messages).set({ activeChildId: nextActiveChildId }).where(sql`${messages.id} = ${targetMessage.id} AND ${messages.userId} = ${userId}`);
topicEvents.emit(topicId, {
type: 'MESSAGE_UPDATED',
payload: { ...targetMessage, activeChildId: nextActiveChildId },
});
return { ok: true };
} }
}
await deeplyDeleteMessage(childMessage.id, topicId, userId); if (targetMessage.children.length === 0) {
await deeplyDeleteMessage(targetMessage.id, topicId, userId);
return { ok: true }; return { ok: true };
} }
if (rootMessage.children.length === 0) { const nextActiveChildId = targetMessage.children[0]?.id ?? null;
await deeplyDeleteMessage(rootMessage.id, topicId, userId); await db.update(messages).set({
return { ok: true }; deleted: true,
} activeChildId: nextActiveChildId,
}).where(sql`${messages.id} = ${targetMessage.id} AND ${messages.userId} = ${userId}`);
await db.update(messages).set({ deleted: true }).where(sql`${messages.id} = ${rootMessage.id} AND ${messages.userId} = ${userId}`);
topicEvents.emit(topicId, { topicEvents.emit(topicId, {
type: 'MESSAGE_UPDATED', type: 'MESSAGE_UPDATED',
payload: { ...rootMessage, deleted: true }, payload: { ...targetMessage, deleted: true, activeChildId: nextActiveChildId },
}); });
return { ok: true }; return { ok: true };
+2 -7
View File
@@ -22,21 +22,16 @@ export default defineEventHandler(async (event) => {
rename: { rename: {
enabled: false, enabled: false,
modelId: null, modelId: null,
},
rerank: {
enabled: false,
modelId: null,
} }
}; };
if (existing) { if (existing) {
const missingSystemAssistants = Object.keys(defaultSystemAssistants).filter(key => !(key in existing!.systemAssistants)); if (!("rename" in existing.systemAssistants) || "rerank" in existing.systemAssistants) {
if (missingSystemAssistants.length > 0) {
const [updated] = await db.update(settings) const [updated] = await db.update(settings)
.set({ .set({
systemAssistants: { systemAssistants: {
...defaultSystemAssistants, ...defaultSystemAssistants,
...existing.systemAssistants, rename: existing.systemAssistants.rename ?? defaultSystemAssistants.rename,
}, },
}) })
.where(eq(settings.id, existing.id)).returning(); .where(eq(settings.id, existing.id)).returning();
+13 -14
View File
@@ -12,7 +12,12 @@ const appearanceSchema = z.object({
}); });
const updateSettingsSchema = z.object({ const updateSettingsSchema = z.object({
systemAssistants: z.any().optional(), systemAssistants: z.object({
rename: z.object({
enabled: z.boolean(),
modelId: z.string().nullable(),
}).partial().optional(),
}).optional(),
appearance: appearanceSchema.optional(), appearance: appearanceSchema.optional(),
}); });
@@ -53,28 +58,22 @@ export default defineEventHandler(async (event) => {
return { ...base, ...incoming }; return { ...base, ...incoming };
}; };
const mergeSystemAssistants = ( const newSystemAssistants = {
existingSA: Record<string, unknown> | null, rename: {
incoming: typeof updates.systemAssistants, enabled: updates.systemAssistants?.rename?.enabled ?? existing.systemAssistants.rename?.enabled ?? false,
): Record<string, unknown> => { modelId: updates.systemAssistants?.rename?.modelId !== undefined
const base = existingSA || {}; ? updates.systemAssistants.rename.modelId
if (!incoming) return base; : existing.systemAssistants.rename?.modelId ?? null,
return { ...base, ...incoming }; },
}; };
const newAppearance = mergeAppearance( const newAppearance = mergeAppearance(
existing.appearance as Record<string, unknown> | null, existing.appearance as Record<string, unknown> | null,
updates.appearance, updates.appearance,
); );
const newSystemAssistants = mergeSystemAssistants(
existing.systemAssistants as Record<string, unknown> | null,
updates.systemAssistants,
);
await db.update(settings) await db.update(settings)
.set({ .set({
appearance: newAppearance, appearance: newAppearance,
// @ts-ignore - I dont have the energy to type this correctly right now
systemAssistants: newSystemAssistants, systemAssistants: newSystemAssistants,
}) })
.where(and(eq(settings.id, existingId))); .where(and(eq(settings.id, existingId)));
@@ -1,44 +1,39 @@
import { and, eq } from 'drizzle-orm'; import { and, eq, isNull } from "drizzle-orm";
import { topics } from '~~/drizzle/schema'; import { topics } from "~~/drizzle/schema";
import { db } from '~~/server/lib/db'; import { db } from "~~/server/lib/db";
import { cancelPendingRename } from '~~/server/utils/renames'; import { cancelPendingRename } from "~~/server/utils/renames";
import { userEvents } from '~~/server/utils/events'; import { topicEvents, userEvents } from "~~/server/utils/events";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
await protectRoute(event); await protectRoute(event);
const topicId = getRouterParam(event, "topicId");
const topicId = getRouterParam(event, 'topicId')!; const userId = event.context.user?.id;
if (!topicId || !userId) {
const success = cancelPendingRename(topicId); setResponseStatus(event, 400);
if (success) { return { ok: false };
const res = await db.update(topics).set({
renaming: false,
}).where(and(eq(topics.id, topicId), eq(topics.userId, event.context.user!.id)));
if (res.rowCount === 0) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid topic',
data: {
code: 'INVALID_TOPIC',
ok: false,
}
});
}
userEvents.emit(topicId, 'topics', {
op: 'update',
payload: {
topicId,
renaming: false,
},
});
return {
ok: true,
};
} }
return { const topic = await db.query.topics.findFirst({ where: { id: topicId, userId } });
ok: false, if (!topic) {
}; setResponseStatus(event, 404);
return { ok: false };
}
const cleared = await db.update(topics).set({
renaming: false,
renameAttemptId: null,
renameExpiresAt: null,
}).where(and(
eq(topics.id, topicId),
eq(topics.userId, userId),
topic.renameAttemptId ? eq(topics.renameAttemptId, topic.renameAttemptId) : isNull(topics.renameAttemptId),
)).returning({ id: topics.id });
if (cleared.length === 0) {
return { ok: false };
}
cancelPendingRename(topicId, topic.renameAttemptId);
const payload = { topicId, renaming: false };
userEvents.emit(userId, "topics", { op: "update", payload });
topicEvents.emit(topicId, { type: "topic_updated", payload });
return { ok: true };
}); });
@@ -1,13 +1,16 @@
import * as z from 'zod'; import * as z from 'zod';
import { renamePrompt } from '~~/prompts'; import { renamePrompt } from '~~/prompts';
import { generateText } from 'ai'; import { generateText, type UserContent } from "ai";
import { nanoid } from "nanoid";
import { resolveFileUrl } from "~~/utils/url";
import { generateFileToken } from "~~/server/utils/file-token";
import { GatewayFetchError, getProviderDetails, type ModelGateway } from '~~/server/utils/ai-provider'; import { GatewayFetchError, getProviderDetails, type ModelGateway } from '~~/server/utils/ai-provider';
import { addPendingRename } from '~~/server/utils/renames'; import { addPendingRename, completeRename } from "~~/server/utils/renames";
import { userEvents } from '~~/server/utils/events'; import { userEvents } from '~~/server/utils/events';
import { db } from '~~/server/lib/db'; import { db } from '~~/server/lib/db';
import { type Model } from '~/composables/useModels'; import { type Model } from '~/composables/useModels';
import { topics } from '~~/drizzle/schema'; import { topics } from '~~/drizzle/schema';
import { eq } from 'drizzle-orm'; import { and, eq, gt, isNull, lte, or } from "drizzle-orm";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
await protectRoute(event); await protectRoute(event);
@@ -44,6 +47,11 @@ export default defineEventHandler(async (event) => {
}, },
with: { with: {
parts: true, parts: true,
attachments: {
with: {
file: true,
}
},
}, },
}); });
@@ -89,7 +97,7 @@ export default defineEventHandler(async (event) => {
}); });
} }
if (topic.renaming) { if (topic.renaming && topic.renameExpiresAt && topic.renameExpiresAt > new Date()) {
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: 'Already renaming', statusMessage: 'Already renaming',
@@ -160,37 +168,70 @@ export default defineEventHandler(async (event) => {
}); });
} }
const abortController = addPendingRename(topicId); const content: UserContent = [{
event.waitUntil(autoRename(topicId, abortController, { gateway: gateway.gateway, model }, gateway.textTransformer, firstMessage.content!, userId)); type: "text",
text: [firstMessage.content || "Describe the attached content.",
...firstMessage.attachments.map(({ file }) => `Attachment: ${file.name}`),
].join("\n").slice(0, 12000),
}];
if (model.inputModalities.includes("image")) {
const images = firstMessage.attachments.filter(({ file }) =>
file.mimeType.startsWith("image/") && file.size <= 10 * 1024 * 1024,
).slice(0, 2);
for (const { file } of images) {
const url = new URL(resolveFileUrl(file.url));
if (file.url.startsWith("/api/files/")) {
const secret = process.env.BETTER_AUTH_SECRET;
if (!secret) {
continue;
}
const { exp, sig } = generateFileToken(file.url.slice("/api/files/".length), secret);
url.searchParams.set("exp", String(exp));
url.searchParams.set("sig", sig);
}
content.push({ type: "image", image: url, mediaType: file.mimeType });
}
}
const attemptId = nanoid();
const claimed = await db.update(topics).set({
renaming: true,
renameAttemptId: attemptId,
renameExpiresAt: new Date(Date.now() + 90000),
}).where(and(
eq(topics.id, topicId),
eq(topics.userId, userId),
eq(topics.name, topic.name),
or(eq(topics.renaming, false), isNull(topics.renameExpiresAt), lte(topics.renameExpiresAt, new Date())),
)).returning({ id: topics.id });
if (claimed.length === 0) {
return { ok: false as const, code: "ALREADY_RENAMING" };
}
userEvents.emit(userId, "topics", {
op: "update", payload: { topicId, renaming: true },
});
const abortController = addPendingRename(topicId, attemptId);
event.waitUntil(autoRename(topicId, attemptId, abortController, { gateway: gateway.gateway, model }, gateway.textTransformer, content, userId));
return { return {
ok: true, ok: true as const,
}; };
}); });
const autoRename = async ( const autoRename = async (
topicId: string, topicId: string,
attemptId: string,
abortController: AbortController, abortController: AbortController,
model: { model: {
gateway: ModelGateway, gateway: ModelGateway,
model: Model, model: Model,
}, },
textTransformer: ((text: string) => string) | ((text: string) => string)[] | undefined, textTransformer: ((text: string) => string) | ((text: string) => string)[] | undefined,
prompt: string, content: UserContent,
userId: string, userId: string,
) => { ) => {
const deadline = setTimeout(() => abortController.abort(), 90000);
try { try {
await db.update(topics).set({
renaming: true,
}).where(eq(topics.id, topicId));
userEvents.emit(model.model.userId, 'topics', {
op: 'update',
payload: {
topicId,
renaming: true,
},
});
const response = await generateText({ const response = await generateText({
model: model.gateway(model.model.externalId), model: model.gateway(model.model.externalId),
providerOptions: { providerOptions: {
@@ -199,10 +240,11 @@ const autoRename = async (
} }
}, },
system: renamePrompt, system: renamePrompt,
prompt, messages: [{ role: "user", content }],
maxRetries: 0,
timeout: 90 * 1000, timeout: 90 * 1000,
abortSignal: abortController.signal, abortSignal: abortController.signal,
}) });
let text = response.text; let text = response.text;
@@ -216,15 +258,30 @@ const autoRename = async (
} }
} }
await db.update(topics).set({ text = text.trim().replace(/^['"]|['"]$/g, "").split("\n")[0]?.trim().slice(0, 120) || "";
if (!text || abortController.signal.aborted) {
return;
}
const updated = await db.update(topics).set({
renaming: false, renaming: false,
renameAttemptId: null,
renameExpiresAt: null,
titleSource: "generated",
name: text, name: text,
}).where(eq(topics.id, topicId)); }).where(and(
eq(topics.id, topicId),
eq(topics.renameAttemptId, attemptId),
gt(topics.renameExpiresAt, new Date()),
)).returning({ id: topics.id });
if (updated.length === 0) {
return;
}
userEvents.emit(model.model.userId, 'topics', { userEvents.emit(model.model.userId, 'topics', {
op: 'update', op: 'update',
payload: { payload: {
topicId, topicId,
name: text, name: text,
titleSource: "generated",
renaming: false, renaming: false,
}, },
}); });
@@ -233,29 +290,26 @@ const autoRename = async (
payload: { payload: {
topicId, topicId,
name: text, name: text,
titleSource: "generated",
renaming: false, renaming: false,
}, },
}); });
} catch (error) { } catch (error) {
console.error('Failed to auto-rename:', error); if (!abortController.signal.aborted) {
await db.update(topics).set({ console.error("Failed to auto-rename:", error);
renaming: false, }
}).where(eq(topics.id, topicId));
userEvents.emit(model.model.userId, 'topics', {
op: 'update',
payload: {
topicId,
renaming: false,
},
});
topicEvents.emit(topicId, {
type: 'topic_updated',
payload: {
topicId,
renaming: false,
},
});
} finally { } finally {
completeRename(topicId); clearTimeout(deadline);
completeRename(topicId, attemptId);
const cleared = await db.update(topics).set({
renaming: false,
renameAttemptId: null,
renameExpiresAt: null,
}).where(and(eq(topics.id, topicId), eq(topics.renameAttemptId, attemptId))).returning({ id: topics.id });
if (cleared.length > 0) {
const payload = { topicId, renaming: false };
userEvents.emit(userId, "topics", { op: "update", payload });
topicEvents.emit(topicId, { type: "topic_updated", payload });
}
} }
} }
+197 -365
View File
@@ -2,8 +2,7 @@ import { db } from "~~/server/lib/db";
import * as z from 'zod'; import * as z from 'zod';
import { type MessageEntity } from '~/composables/useChat'; import { type MessageEntity } from '~/composables/useChat';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { glob } from 'glob'; import { isLoopFinished, type ModelMessage, streamText, type StreamTextTransform, type Tool, tool } from "ai";
import { isLoopFinished, type ModelMessage, rerank, type RerankingModel, streamText, type StreamTextTransform, Tool, tool } from "ai";
import { generations, messageParts, messages, toolCalls, ToolCallType } from "~~/drizzle/schema"; import { generations, messageParts, messages, toolCalls, ToolCallType } from "~~/drizzle/schema";
import { topicEvents } from "~~/server/utils/events"; import { topicEvents } from "~~/server/utils/events";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
@@ -12,7 +11,10 @@ import { type Model } from "~/composables/useModels";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { buildFocusedMessageTree, buildMessageTree, marshallMessages } from "~~/utils/message"; import { buildFocusedMessageTree, buildMessageTree, marshallMessages } from "~~/utils/message";
import path from "path"; import path from "path";
import { isRerankingProvider } from "~~/server/utils/ai-provider"; import { generateFileToken } from "~~/server/utils/file-token";
import { serializeToolError } from "~~/utils/tool-errors";
import { readFirecrawlResponse } from "~~/server/utils/firecrawl";
import { limitSearchResults, readSearchResponse, searchInputSchema, searchOutputSchema } from "~~/server/utils/search";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
await protectRoute(event); await protectRoute(event);
@@ -23,10 +25,6 @@ export default defineEventHandler(async (event) => {
const result = await readValidatedBody(event, z.object({ const result = await readValidatedBody(event, z.object({
parentMessageId: z.string().optional(), parentMessageId: z.string().optional(),
modelId: z.string(), modelId: z.string(),
rerank: z.object({
modelId: z.string(),
providerApiKey: z.string().optional(),
}).optional(),
args: z.record(z.string(), z.any()).optional(), args: z.record(z.string(), z.any()).optional(),
providerApiKey: z.string().optional(), providerApiKey: z.string().optional(),
}).safeParse); }).safeParse);
@@ -38,7 +36,7 @@ export default defineEventHandler(async (event) => {
}); });
} }
const { parentMessageId, modelId, rerank: rerankConfig, providerApiKey, args } = result.data; const { parentMessageId, modelId, providerApiKey, args } = result.data;
const topic = await db.query.topics.findFirst({ const topic = await db.query.topics.findFirst({
where: { where: {
@@ -114,90 +112,7 @@ export default defineEventHandler(async (event) => {
} }
} }
const agentSearchConfig = topic.agent.config?.search; const searchEnabled = topic.agent.config?.search?.enabled === true;
let searchParam: false | { config: SearchTheWebConfig } = false;
if (agentSearchConfig?.enabled) {
searchParam = { config: { rerank: false, maxResults: agentSearchConfig.maxResults ?? 10 } };
if (agentSearchConfig.rerank && rerankConfig) {
const rerankModel = await db.query.models.findFirst({
where: {
id: rerankConfig.modelId,
userId,
},
with: {
provider: true,
}
});
if (rerankModel === undefined) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid rerank model',
data: {
code: 'INVALID_RERANK_MODEL',
ok: false,
}
});
}
const rerankProviderDetails = await getProviderDetails(rerankModel.provider, rerankConfig.providerApiKey, rerankModel);
if (!rerankProviderDetails.ok) {
switch (rerankProviderDetails.error) {
case GatewayFetchError.NoProviderApiKey: {
throw createError({
statusCode: 400,
statusMessage: `${rerankModel.provider.type} provider requires an API key`,
data: {
code: 'NO_RERANK_PROVIDER_API_KEY',
ok: false,
}
});
}
case GatewayFetchError.NoProviderBaseUrl: {
throw createError({
statusCode: 400,
statusMessage: 'Invalid provider URL',
data: {
code: 'BAD_RERANK_PROVIDER_URL',
ok: false,
}
});
}
}
}
const { gateway: rerankGateway } = rerankProviderDetails.data;
if (rerankGateway === null) {
throw createError({
statusCode: 500,
statusMessage: 'Invalid gateway',
data: {
code: 'INVALID_RERANK_GATEWAY',
ok: false,
}
});
}
if (isRerankingProvider(rerankGateway.gateway)) {
searchParam.config = {
rerank: true,
maxResults: agentSearchConfig.maxResults ?? 10,
model: rerankGateway.gateway.reranking(rerankModel.externalId)
};
} else {
throw createError({
statusCode: 500,
statusMessage: 'Invalid rerank model',
data: {
code: 'INVALID_RERANK_MODEL',
ok: false,
}
});
}
}
}
let agentmessage = await db.transaction(async tx => { let agentmessage = await db.transaction(async tx => {
const generationId = nanoid(); const generationId = nanoid();
@@ -237,7 +152,7 @@ export default defineEventHandler(async (event) => {
} }
agentmessage.generationId = generation.id; agentmessage.generationId = generation.id;
// @ts-ignore // @ts-ignore - doesnt exist on the type but yeah it does now
agentmessage.generation = generation; agentmessage.generation = generation;
return agentmessage; return agentmessage;
@@ -311,7 +226,13 @@ export default defineEventHandler(async (event) => {
} }
} }
const topicMessages = marshallMessages(topic.agent, buildFocusedMessageTree(topicMessageTree)); const fileTokenSecret = process.env.BETTER_AUTH_SECRET!;
const topicMessages = marshallMessages(topic.agent, buildFocusedMessageTree(topicMessageTree), {
signFileUrl: (fileKey) => {
const { exp, sig } = generateFileToken(fileKey, fileTokenSecret);
return `exp=${exp}&sig=${sig}`;
},
});
if (topicMessages.ok === false) { if (topicMessages.ok === false) {
throw createError({ throw createError({
statusCode: 500, statusCode: 500,
@@ -324,7 +245,10 @@ export default defineEventHandler(async (event) => {
generateResponse( generateResponse(
agentmessage as MessageEntity, agentmessage as MessageEntity,
{ gateway: gateway.gateway, model, parameters: args }, { gateway: gateway.gateway, model, parameters: args },
searchParam, searchEnabled,
{
python: topic.agent.config?.tools?.python ?? false,
},
agentmessage.generationId!, agentmessage.generationId!,
userId, userId,
topicId, topicId,
@@ -353,233 +277,125 @@ const formatPartId = (partType: string, existingId: string) => {
}; };
const formatToolCallId = (nativeId: string) => { const formatToolCallId = (nativeId: string) => {
return `veridian__tool-${nativeId}-${nanoid()}`; return `veridian__tool-${nativeId.slice(0, 16)}-${nanoid()}`;
}; };
const evalPython = async (code: string) => { const formatPythonValue = (value: unknown): string => {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'string') {
return value;
}
try { try {
let stdout = '' return JSON.stringify(value, (_, v) => typeof v === 'bigint' ? v.toString() : v);
const printCallback = (_: string, text: string) => { } catch {
stdout += text return String(value);
}
const m = new Monty(code)
await m.run({ printCallback })
return stdout
} catch (error) {
if (error instanceof MontySyntaxError) {
console.log('Syntax error:', error.message)
} else if (error instanceof MontyRuntimeError) {
console.log('Runtime error:', error.message)
console.log('Traceback:', error.traceback())
} else if (error instanceof MontyTypingError) {
console.log('Type error:', error.displayDiagnostics())
}
} }
}; };
interface SearchTheWebRerankedConfig { const evalPython = async (code: string): Promise<string> => {
rerank: true; try {
maxResults: number; let stdout = '';
model: RerankingModel; const printCallback = (_: string, text: string) => {
} stdout += text;
};
interface SearchTheWebUnrankedConfig { const m = new Monty(code);
rerank?: false; const result = m.run({
maxResults: number; printCallback,
} limits: {
maxDurationSecs: 10,
type SearchTheWebConfig = SearchTheWebRerankedConfig | SearchTheWebUnrankedConfig; maxMemory: 32 * 1024 * 1024,
maxRecursionDepth: 100,
export const searchTheWeb = (config: SearchTheWebConfig) => { },
return async (query: string) => {
const results = await $fetch<{
results: Array<{
title: string;
url: string;
content: string;
}>;
}>(`${process.env.SEARXNG_URL}/search`, {
query: {
q: query,
format: 'json',
}
}); });
const sites = results.results.map((item: any) => ({ const expressionOutput = formatPythonValue(result);
title: item.title, if (stdout && expressionOutput) {
link: item.url, return stdout.endsWith('\n')
snippet: item.content, ? `${stdout}${expressionOutput}`
})); : `${stdout}\n${expressionOutput}`;
if (config.rerank) {
const { ranking } = await rerank({
model: config.model,
query,
documents: sites.map(site => site.snippet),
});
const ranked_sites = [];
for (let i = 0; i < ranking.length; i++) {
ranked_sites.push(sites[ranking[i]!.originalIndex]);
}
return ranked_sites.slice(0, config.maxResults || 10);
} }
return stdout || expressionOutput || '';
return sites.slice(0, config.maxResults || 10); } catch (error) {
}; if (error instanceof MontySyntaxError) {
return `SyntaxError: ${error.message}`;
}
if (error instanceof MontyRuntimeError) {
return error.display('traceback') || `RuntimeError: ${error.message}`;
}
if (error instanceof MontyTypingError) {
return error.display('concise') || `TypeError: ${error.message}`;
}
return error instanceof Error ? error.message : 'Python execution failed';
}
}; };
// TODO: obviously come up with a better way for the user to define their own tools export const searchTheWeb = async (query: string, results: number) => {
const { listDirectoryTool, globTool, readFileTool, readFilesTool, fetchUrlTool, pythonTool, bashTool } = { const searchUrl = new URL(process.env.SEARCH_API_URL ?? process.env.SEARXNG_URL ?? "");
listDirectoryTool: tool({ searchUrl.pathname = `${searchUrl.pathname.replace(/\/+$/, "")}/search`;
description: 'Lists the contents of a directory',
inputSchema: z.object({
path: z.string(),
}),
outputSchema: z.object({
files: z.array(z.object({ name: z.string(), type: z.string() })),
}),
execute: async ({ path }) => {
const rawFiles = await fs.readdir(path, { withFileTypes: true });
const files = rawFiles.map((file) => ({
name: file.name,
type: file.isFile() ? 'file' : 'directory',
}));
return { const response = await $fetch<unknown>(searchUrl.toString(), {
files, query: {
}; q: query,
}, format: 'json',
}), }
globTool: tool({ });
description: 'Lists files matching a glob pattern',
inputSchema: z.object({
pattern: z.string(),
}),
outputSchema: z.object({
files: z.array(z.object({ path: z.string(), type: z.string() })),
}),
execute: async ({ pattern }) => {
const rawFiles = await glob(pattern, { withFileTypes: true });
const files = rawFiles.map((file) => ({
path: file.parentPath + '/' + file.name,
type: file.isFile() ? 'file' : 'directory',
}));
return { const result = readSearchResponse(response);
files, if (!result.ok) {
}; // Reject execution so the SDK emits a tool-error rather than an empty success.
}, return Promise.reject(new Error(result.error));
}), }
readFileTool: tool({ return limitSearchResults(result.data, results);
description: 'Reads the contents of a file', };
inputSchema: z.object({
path: z.string(),
}),
outputSchema: z.object({
path: z.string(),
content: z.string(),
}),
execute: async ({ path }) => {
const file = await fs.readFile(path);
return {
path,
content: file.toString(),
};
},
}),
readFilesTool: tool({
description: 'Reads the contents of multiple files',
inputSchema: z.object({
paths: z.array(z.string()).describe('a list of file paths to read'),
}),
outputSchema: z.object({
files: z.array(
z.object({
path: z.string(),
content: z.string(),
}),
),
}),
execute: async ({ paths }) => {
const files = await Promise.all(
paths.map(async (path) => {
const file = await fs.readFile(path);
return {
path: path,
content: file.toString(),
};
}),
);
return { const fetchUrlTool = tool({
files, description: 'Fetches the content of a URL',
}; inputSchema: z.object({
}, url: z.string(),
}), }),
fetchUrlTool: tool({ outputSchema: z.object({
description: 'Fetches the content of a URL', content: z.string(),
inputSchema: z.object({
url: z.string(),
}),
outputSchema: z.object({
content: z.string(),
}),
execute: async ({ url }) => {
const response = await fetch(url);
const content = await response.text();
return {
content,
};
},
}), }),
pythonTool: tool({ execute: async ({ url }) => {
description: 'Executes a Python code snippet', const response = await fetch('https://api.firecrawl.dev/v2/scrape', {
inputSchema: z.object({ method: 'POST',
code: z.string(), headers: {
}), 'Content-Type': 'application/json',
outputSchema: z.object({ 'Authorization': 'Bearer ' + process.env.FIRECRAWL_API_KEY,
output: z.string(), },
}), body: JSON.stringify({
execute: async ({ code }) => { url,
const output = await evalPython(code); formats: ["markdown"],
return { }),
output, });
}; const result = await readFirecrawlResponse(response);
}, if (!result.ok) {
}), // The SDK needs a rejected execution to emit a tool-error, not a successful result.
bashTool: tool({ return Promise.reject(new Error(result.error));
description: 'Executes a Bash command', }
inputSchema: z.object({ return {
code: z.string(), content: result.data,
}), };
outputSchema: z.object({ },
output: z.string(), });
}),
execute: async ({ code }) => {
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
async function runCommand(command: string) { const pythonTool = tool({
const { stdout, stderr } = await execAsync(command); description: 'Executes Python in a sandbox. The value of the last expression is returned automatically (no print needed). stdout from print() is also included.',
if (stderr) { inputSchema: z.object({
console.error(`Error: ${stderr}`); code: z.string().describe('Python code to run. Prefer a final expression over print(), e.g. `2 + 2` returns `4`.'),
return stderr;
} else {
return stdout;
}
}
const output = await runCommand(code);
return {
output,
};
},
}), }),
} outputSchema: z.object({
output: z.string(),
}),
execute: async ({ code }) => {
const output = await evalPython(code);
return {
output,
};
},
});
async function generateResponse( async function generateResponse(
message: MessageEntity, message: MessageEntity,
@@ -588,8 +404,9 @@ async function generateResponse(
model: Model, model: Model,
parameters?: Record<string, any>, parameters?: Record<string, any>,
}, },
search: false | { search: boolean,
config: SearchTheWebConfig, enabledTools: {
python?: boolean;
}, },
generationId: string, generationId: string,
userId: string, userId: string,
@@ -608,66 +425,53 @@ async function generateResponse(
const activeToolCalls = new Set<string>(); const activeToolCalls = new Set<string>();
const nativeToDbToolCallId = new Map<string, string>(); const nativeToDbToolCallId = new Map<string, string>();
// TODO: somehow let the user turn on and off tools const tools: Record<string, Tool> = {};
const tools: Record<string, Tool> = {
// writeFile: tool({ if (enabledTools.python) {
// inputSchema: z.object({ tools.python = pythonTool;
// path: z.string(), }
// content: z.string(),
// }),
// outputSchema: z.object({
// success: z.boolean(),
// }),
// execute: async ({ path, content }) => {
// await fs.writeFile(path, content);
// return {
// success: true,
// };
// }
// }),
listDirectory: listDirectoryTool,
glob: globTool,
readFile: readFileTool,
readFiles: readFilesTool,
// fetchUrl: fetchUrlTool,
python: pythonTool,
bash: bashTool,
};
if (search) { if (search) {
tools.search = tool({ tools.search = tool({
description: 'Searches the web', description: `Searches the web. Choose how many results to return on every call using the required results argument.
inputSchema: z.object({
query: z.string(), CITATION FORMAT:
}), Every result includes a sourceId. Cite supporting results immediately after the claim using this exact construction:
outputSchema: z.array( "[[cite:" + the result's sourceId + "]]"
z.object({ The prefix "cite:" is mandatory literal text. Copy the sourceId unchanged after that prefix. Do not output the word "sourceId" as a placeholder.
title: z.string(),
link: z.string(), Example: if a result has sourceId "sd625ebbd4d3544e4914f5a51840d7632":
snippet: z.string(), CORRECT: The report describes rising prices. [[cite:sd625ebbd4d3544e4914f5a51840d7632]]
engine: z.string(), INCORRECT: The report describes rising prices. [[sd625ebbd4d3544e4914f5a51840d7632]]
}) The incorrect form is missing "cite:" and will not render as a citation.
),
execute: async ({ query }) => { For multiple sources, put their exact IDs after a single "cite:" prefix, separated by commas without spaces: [[cite:ID_FROM_FIRST_RESULT,ID_FROM_SECOND_RESULT]]. Replace both placeholders with returned sourceId values. Separate complete citations are also valid: [[cite:ID_FROM_FIRST_RESULT]] [[cite:ID_FROM_SECOND_RESULT]].
return await searchTheWeb(search.config)(query); Use only IDs returned by searches during this response; never invent IDs or copy the example ID unless it actually appears in a result. Write citation markers directly in answer text, not inside backticks or code blocks. Do not repeat URLs or add footnote definitions for these citations.
Search snippets are excerpts, not full-page reads.`,
inputSchema: searchInputSchema,
outputSchema: searchOutputSchema,
execute: async ({ query, results }) => {
return await searchTheWeb(query, results);
}, },
}); });
tools.fetchUrl = fetchUrlTool;
} }
const response = streamText({ const response = streamText({
model: model.gateway(model.model.externalId), model: model.gateway(model.model.externalId),
messages, messages,
allowSystemInMessages: true,
providerOptions: { providerOptions: {
openrouter: { openrouter: {
debug: { debug: {
echo_upstream_body: true, echo_upstream_body: true,
}, },
user: userId, user: userId,
} },
}, },
experimental_transform: streamTransoforms, experimental_transform: streamTransoforms,
stopWhen: isLoopFinished(), stopWhen: isLoopFinished(),
tools: model.model.capabilities.includes('tools') ? tools : undefined, tools: model.model.capabilities.includes('tools') && Object.keys(tools).length > 0 ? tools : undefined,
onError: async (error: any) => { onError: async (error: any) => {
// TODO: the docs say "The stream processing will pause until the callback promise is resolved." Suggesting that this error might not be fatal? // TODO: the docs say "The stream processing will pause until the callback promise is resolved." Suggesting that this error might not be fatal?
console.error('generation error', error); console.error('generation error', error);
@@ -689,6 +493,7 @@ async function generateResponse(
// Mark failed tool calls // Mark failed tool calls
await Promise.all([...activeToolCalls].map(async id => { await Promise.all([...activeToolCalls].map(async id => {
const lastUpdatedAt = new Date();
await db.update(toolCalls).set({ await db.update(toolCalls).set({
status: 'failed', status: 'failed',
error: { type: ToolCallType.Text, value: 'Generation failed' } error: { type: ToolCallType.Text, value: 'Generation failed' }
@@ -700,10 +505,12 @@ async function generateResponse(
messageId: message.id, messageId: message.id,
toolCallId: id, toolCallId: id,
toolName: null, toolName: null,
status: 'failed',
error: { error: {
type: ToolCallType.Text, type: ToolCallType.Text,
value: 'Generation failed' value: 'Generation failed'
} },
lastUpdatedAt: lastUpdatedAt.toISOString(),
} }
}) })
})); }));
@@ -859,6 +666,7 @@ async function generateResponse(
partId: part.id, partId: part.id,
lastUpdatedAt: new Date(), lastUpdatedAt: new Date(),
content: token.text, content: token.text,
offset: part.accumulatedContent.length - token.text.length,
} }
}) })
} }
@@ -959,7 +767,7 @@ async function generateResponse(
throw new Error('Failed to insert message part'); throw new Error('Failed to insert message part');
} }
// @ts-ignore // @ts-ignore - doesnt exist on the type but yeah it does now
part.toolCall = toolCall; part.toolCall = toolCall;
await topicEvents.emit(topicId, { await topicEvents.emit(topicId, {
@@ -972,8 +780,6 @@ async function generateResponse(
}) })
activeToolCalls.add(dbToolCallId); activeToolCalls.add(dbToolCallId);
activeParts.set(key, { id: part.id, accumulatedContent: '' });
} break; } break;
case 'tool-call': { case 'tool-call': {
let inputType: ToolCallType = ToolCallType.Text; let inputType: ToolCallType = ToolCallType.Text;
@@ -1005,6 +811,11 @@ async function generateResponse(
}, },
}) })
.where(eq(toolCalls.id, dbToolCallId)); .where(eq(toolCalls.id, dbToolCallId));
if (token.providerMetadata) await db.update(messageParts)
.set({
providerOptions: token.providerMetadata,
})
.where(eq(messageParts.toolCallId, dbToolCallId));
await topicEvents.emit(topicId, { await topicEvents.emit(topicId, {
type: 'tool-call-delta', type: 'tool-call-delta',
@@ -1049,6 +860,7 @@ async function generateResponse(
topicId, topicId,
messageId: message.id, messageId: message.id,
toolCallId: dbToolCallId, toolCallId: dbToolCallId,
providerOptions: token.providerMetadata,
type: 'tool-call', type: 'tool-call',
content: null, content: null,
finished: false, finished: false,
@@ -1060,7 +872,7 @@ async function generateResponse(
throw new Error('Failed to insert message part'); throw new Error('Failed to insert message part');
} }
// @ts-ignore // @ts-ignore - doesnt exist on the type but yeah it does now
part.toolCall = toolCall; part.toolCall = toolCall;
await topicEvents.emit(topicId, { await topicEvents.emit(topicId, {
@@ -1072,8 +884,6 @@ async function generateResponse(
}) })
activeToolCalls.add(dbToolCallId); activeToolCalls.add(dbToolCallId);
activeParts.set(key, { id: part.id, accumulatedContent: '' });
} }
} break; } break;
case 'tool-result': { case 'tool-result': {
@@ -1097,7 +907,13 @@ async function generateResponse(
status: 'failed', status: 'failed',
error: { type: ToolCallType.Text, value: 'Tool returned invalid output' } error: { type: ToolCallType.Text, value: 'Tool returned invalid output' }
}).where(eq(toolCalls.id, dbToolCallId)); }).where(eq(toolCalls.id, dbToolCallId));
if (token.providerMetadata) await db.update(messageParts)
.set({
providerOptions: token.providerMetadata,
})
.where(eq(messageParts.toolCallId, dbToolCallId));
const lastUpdatedAt = new Date();
await topicEvents.emit(topicId, { await topicEvents.emit(topicId, {
type: 'tool-call-delta', type: 'tool-call-delta',
payload: { payload: {
@@ -1109,6 +925,7 @@ async function generateResponse(
value: outputValue, value: outputValue,
}, },
status: 'failed', status: 'failed',
lastUpdatedAt: lastUpdatedAt.toISOString(),
} }
}) })
@@ -1118,6 +935,7 @@ async function generateResponse(
} }
if (dbToolCallId) { if (dbToolCallId) {
const lastUpdatedAt = new Date();
await db.update(toolCalls).set({ await db.update(toolCalls).set({
status: 'completed', status: 'completed',
output: { output: {
@@ -1125,6 +943,13 @@ async function generateResponse(
value: outputValue, value: outputValue,
}, },
}).where(eq(toolCalls.id, dbToolCallId)); }).where(eq(toolCalls.id, dbToolCallId));
await db.update(messageParts)
.set({
finished: true,
lastUpdatedAt,
...(token.providerMetadata ? { providerOptions: token.providerMetadata } : {}),
})
.where(eq(messageParts.toolCallId, dbToolCallId));
await topicEvents.emit(topicId, { await topicEvents.emit(topicId, {
type: 'tool-call-delta', type: 'tool-call-delta',
@@ -1137,6 +962,7 @@ async function generateResponse(
value: outputValue, value: outputValue,
}, },
status: 'completed', status: 'completed',
lastUpdatedAt: lastUpdatedAt.toISOString(),
} }
}) })
@@ -1157,8 +983,8 @@ async function generateResponse(
outputValue = token.error; outputValue = token.error;
break; break;
case 'object': case 'object':
outputType = ToolCallType.Json; outputType = token.error instanceof Error ? ToolCallType.Text : ToolCallType.Json;
outputValue = JSON.stringify(token.error); outputValue = serializeToolError(token.error);
break; break;
default: default:
console.error('Unknown error type', token.error); console.error('Unknown error type', token.error);
@@ -1169,6 +995,7 @@ async function generateResponse(
const existingDbToolCallId = nativeToDbToolCallId.get(token.toolCallId); const existingDbToolCallId = nativeToDbToolCallId.get(token.toolCallId);
if (existingDbToolCallId && activeToolCalls.has(existingDbToolCallId)) { if (existingDbToolCallId && activeToolCalls.has(existingDbToolCallId)) {
const lastUpdatedAt = new Date();
await db.update(toolCalls).set({ await db.update(toolCalls).set({
status: 'failed', status: 'failed',
error: { error: {
@@ -1176,6 +1003,13 @@ async function generateResponse(
value: outputValue as string, value: outputValue as string,
} }
}).where(eq(toolCalls.id, existingDbToolCallId)); }).where(eq(toolCalls.id, existingDbToolCallId));
await db.update(messageParts)
.set({
finished: true,
lastUpdatedAt,
...(token.providerMetadata ? { providerOptions: token.providerMetadata } : {}),
})
.where(eq(messageParts.toolCallId, existingDbToolCallId));
await topicEvents.emit(topicId, { await topicEvents.emit(topicId, {
type: 'tool-call-delta', type: 'tool-call-delta',
@@ -1183,10 +1017,12 @@ async function generateResponse(
messageId: message.id, messageId: message.id,
toolCallId: existingDbToolCallId, toolCallId: existingDbToolCallId,
toolName: token.toolName, toolName: token.toolName,
status: 'failed',
error: { error: {
type: outputType as ToolCallType, type: outputType as ToolCallType,
value: outputValue as string, value: outputValue as string,
}, },
lastUpdatedAt: lastUpdatedAt.toISOString(),
} }
}) })
} else { } else {
@@ -1220,6 +1056,7 @@ async function generateResponse(
topicId, topicId,
messageId: message.id, messageId: message.id,
toolCallId: dbToolCallId, toolCallId: dbToolCallId,
providerOptions: token.providerMetadata,
type: 'tool-call', type: 'tool-call',
content: null, content: null,
finished: false, finished: false,
@@ -1235,11 +1072,9 @@ async function generateResponse(
type: 'tool-call-start', type: 'tool-call-start',
payload: { payload: {
messageId: message.id, messageId: message.id,
part, part: { ...part, toolCall },
} }
}) })
activeParts.set(key, { id: part.id, accumulatedContent: '' });
} }
if (existingDbToolCallId) { if (existingDbToolCallId) {
@@ -1250,12 +1085,8 @@ async function generateResponse(
case 'finish': { case 'finish': {
let tps; let tps;
if (ttft !== undefined && token.totalUsage.outputTokens !== undefined) { if (requestStart !== undefined && token.totalUsage.outputTokens !== undefined) {
const tokenStreamStart = requestStart! + ttft; const requestDuration = performance.now() - requestStart;
// this is the *real* request duration, excluding the
// TTFT
const requestDuration = performance.now() - tokenStreamStart;
tps = token.totalUsage.outputTokens / (requestDuration / 1000); tps = token.totalUsage.outputTokens / (requestDuration / 1000);
} }
@@ -1428,6 +1259,7 @@ async function generateResponse(
} }
}) })
} finally { } finally {
topicEvents.clearLiveText(topicId, message.id);
completeGeneration(generationId); completeGeneration(generationId);
if (logFile !== undefined) logFile.close(); if (logFile !== undefined) logFile.close();
} }
+274
View File
@@ -0,0 +1,274 @@
import * as z from 'zod';
import { messages, messageParts, attachments, topics, generations, toolCalls } from '~~/drizzle/schema';
import { db } from '~~/server/lib/db';
import { nanoid } from 'nanoid';
import { eq, and, inArray } from 'drizzle-orm';
export default defineEventHandler(async (event) => {
await protectRoute(event);
const userId = event.context.user!.id;
const topicId = getRouterParam(event, 'topicId')!;
const result = await readValidatedBody(event, (body) =>
z
.object({
messageId: z.string(),
})
.safeParse(body),
);
if (!result.success) {
throw createError({
statusCode: 400,
message: result.error.issues[0]!.message,
});
}
const { messageId } = result.data;
const topic = await db.select().from(topics).where(and(
eq(topics.id, topicId),
eq(topics.userId, userId),
)).limit(1).then(rows => rows[0]);
if (!topic) {
throw createError({
statusCode: 404,
statusMessage: 'Not Found',
message: 'Topic not found',
});
}
const topicMessages = await db.select().from(messages)
.where(eq(messages.topicId, topicId))
.orderBy(messages.createdAt);
const forkMessage = topicMessages.find(m => m.id === messageId);
if (!forkMessage) {
throw createError({
statusCode: 404,
statusMessage: 'Not Found',
message: 'Message not found in topic',
});
}
const childrenMap = new Map<string, typeof topicMessages>();
for (const msg of topicMessages) {
if (msg.parentMessageId) {
const siblings = childrenMap.get(msg.parentMessageId) || [];
siblings.push(msg);
childrenMap.set(msg.parentMessageId, siblings);
}
}
const messageIdsToCopy = new Set<string>();
const messageMap = new Map(topicMessages.map(m => [m.id, m]));
const addMessageAndSiblings = (msgId: string) => {
const msg = messageMap.get(msgId);
if (!msg) return;
if (msg.parentMessageId) {
const siblings = childrenMap.get(msg.parentMessageId) || [];
for (const sibling of siblings) {
messageIdsToCopy.add(sibling.id);
}
} else {
messageIdsToCopy.add(msgId);
}
};
let current: typeof forkMessage | undefined = forkMessage;
while (current) {
addMessageAndSiblings(current.id);
current = current.parentMessageId ? messageMap.get(current.parentMessageId) : undefined;
}
const forkIndex = topicMessages.findIndex(m => m.id === messageId);
for (let i = 0; i <= forkIndex; i++) {
const msg = topicMessages[i]!;
if (!msg.parentMessageId) {
messageIdsToCopy.add(msg.id);
const children = childrenMap.get(msg.id) || [];
for (const child of children) {
messageIdsToCopy.add(child.id);
}
}
}
const addDescendants = (msgId: string) => {
const children = childrenMap.get(msgId) || [];
for (const child of children) {
if (!messageIdsToCopy.has(child.id)) {
messageIdsToCopy.add(child.id);
addDescendants(child.id);
}
}
};
const initialIds = Array.from(messageIdsToCopy);
for (const id of initialIds) {
addDescendants(id);
}
const messagesToCopy = topicMessages
.filter(m => messageIdsToCopy.has(m.id))
.sort((a, b) => {
if (!a.parentMessageId && b.parentMessageId) return -1;
if (a.parentMessageId && !b.parentMessageId) return 1;
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
});
const newTopicId = nanoid();
const newTopicName = `${topic.name} (fork)`;
await db.insert(topics).values({
id: newTopicId,
userId,
name: newTopicName,
agentId: topic.agentId,
createdAt: new Date(),
});
const messageIdMap = new Map<string, string>();
const generationIdMap = new Map<string, string>();
const toolCallIdMap = new Map<string, string>();
const originalMessageIds = messagesToCopy.map(m => m.id);
const originalGenerationIds = [
...new Set(
messagesToCopy
.map(m => m.generationId)
.filter((id): id is string => id !== null),
),
];
// Copy generations first so messages can reference them
if (originalGenerationIds.length > 0) {
const generationsToCopy = await db.select().from(generations)
.where(inArray(generations.id, originalGenerationIds));
for (const generation of generationsToCopy) {
const newGenerationId = nanoid();
generationIdMap.set(generation.id, newGenerationId);
await db.insert(generations).values({
id: newGenerationId,
userId,
topicId: newTopicId,
modelId: generation.modelId,
status: generation.status,
tokens: generation.tokens,
error: generation.error,
});
}
}
for (const msg of messagesToCopy) {
const newMessageId = nanoid();
messageIdMap.set(msg.id, newMessageId);
await db.insert(messages).values({
id: newMessageId,
userId,
topicId: newTopicId,
parentMessageId: msg.parentMessageId ? (messageIdMap.get(msg.parentMessageId) ?? null) : null,
generationId: msg.generationId ? (generationIdMap.get(msg.generationId) ?? null) : null,
role: msg.role,
content: msg.content,
activeChildId: null,
deleted: msg.deleted,
createdAt: msg.createdAt,
updatedAt: msg.updatedAt,
});
}
for (const msg of messagesToCopy) {
if (msg.activeChildId) {
const newMessageId = messageIdMap.get(msg.id);
const newActiveChildId = messageIdMap.get(msg.activeChildId);
if (newMessageId && newActiveChildId) {
await db.update(messages)
.set({ activeChildId: newActiveChildId })
.where(eq(messages.id, newMessageId));
}
}
}
if (originalMessageIds.length > 0) {
const parts = await db.select().from(messageParts)
.where(inArray(messageParts.messageId, originalMessageIds));
// Copy referenced tool calls before parts so FKs resolve
const originalToolCallIds = [
...new Set(
parts
.map(p => p.toolCallId)
.filter((id): id is string => id !== null),
),
];
if (originalToolCallIds.length > 0) {
const toolCallsToCopy = await db.select().from(toolCalls)
.where(inArray(toolCalls.id, originalToolCallIds));
for (const toolCall of toolCallsToCopy) {
const newToolCallId = nanoid();
toolCallIdMap.set(toolCall.id, newToolCallId);
await db.insert(toolCalls).values({
id: newToolCallId,
userId,
toolName: toolCall.toolName,
status: toolCall.status,
input: toolCall.input,
output: toolCall.output,
error: toolCall.error,
createdAt: toolCall.createdAt,
});
}
}
for (const part of parts) {
const newMessageId = messageIdMap.get(part.messageId);
if (!newMessageId) continue;
await db.insert(messageParts).values({
userId,
topicId: newTopicId,
messageId: newMessageId,
toolCallId: part.toolCallId ? (toolCallIdMap.get(part.toolCallId) ?? null) : null,
type: part.type,
content: part.content,
providerOptions: part.providerOptions,
finished: part.finished,
// Preserve original timestamps so reasoning duration stays accurate
createdAt: part.createdAt,
lastUpdatedAt: part.lastUpdatedAt,
});
}
const attachmentsResult = await db.select().from(attachments)
.where(inArray(attachments.messageId, originalMessageIds));
for (const attachment of attachmentsResult) {
const newMessageId = messageIdMap.get(attachment.messageId);
if (!newMessageId) continue;
await db.insert(attachments).values({
userId,
topicId: newTopicId,
messageId: newMessageId,
fileId: attachment.fileId,
createdAt: attachment.createdAt,
});
}
}
return {
ok: true,
topicId: newTopicId,
name: newTopicName,
};
});
+37 -72
View File
@@ -1,95 +1,60 @@
import { db } from "~~/server/lib/db"; import { db } from '~~/server/lib/db';
import { topicEvents } from "~~/server/utils/events"; import { topicEvents } from '~~/server/utils/events';
import { createTopicSnapshotStream } from '~~/server/utils/topic-snapshot-stream';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
await protectRoute(event); await protectRoute(event);
const topicId = getRouterParam(event, 'topicId')!; const topicId = getRouterParam(event, 'topicId')!;
const isSSE = getHeader(event, 'accept')?.includes('text/event-stream') ?? false;
const userId = event.context.user!.id as string; const userId = event.context.user!.id as string;
const isSSE = getHeader(event, 'accept')?.includes('text/event-stream') ?? false;
const topic = await db.query.topics.findFirst({ const readTopic = () => db.query.topics.findFirst({
where: { where: { id: topicId, userId },
id: topicId,
userId,
},
with: { with: {
messages: { messages: {
orderBy: { orderBy: { createdAt: 'asc' },
createdAt: 'asc',
},
with: { with: {
parts: { parts: {
orderBy: { orderBy: { createdAt: 'asc' },
createdAt: 'asc', with: { toolCall: true },
},
with: {
toolCall: true,
}
},
attachments: {
with: {
file: true,
}
}, },
attachments: { with: { file: true } },
generation: true, generation: true,
} },
}, },
}, },
}); });
if (!topic) { if (!isSSE) {
console.log("topic not found"); const topic = await readTopic();
throw createError({ if (!topic) {
statusCode: 404, setResponseStatus(event, 404);
statusMessage: 'Not Found', return { ok: false, error: 'Topic not found' };
}); }
} topicEvents.applyLiveText(topicId, topic.messages);
if (isSSE === false) {
return topic; return topic;
} }
// Authorize before subscribing; no topic data is exposed before this check.
const ownedTopic = await db.query.topics.findFirst({
where: { id: topicId, userId },
columns: { id: true },
});
if (!ownedTopic) {
setResponseStatus(event, 404);
return { ok: false, error: 'Topic not found' };
}
const subscription = createTopicSnapshotStream(topicId, getQuery(event).snapshot);
event.node.res.once('close', subscription.close);
const stream = await subscription.open(readTopic);
if (!stream.ok) {
setResponseStatus(event, stream.error === 'read-failed' ? 500 : 404);
return { ok: false, error: stream.error };
}
setHeader(event, 'Content-Type', 'text/event-stream'); setHeader(event, 'Content-Type', 'text/event-stream');
setHeader(event, 'Cache-Control', 'no-cache'); setHeader(event, 'Cache-Control', 'no-cache');
setHeader(event, 'Connection', 'keep-alive'); setHeader(event, 'X-Accel-Buffering', 'no');
const { lastUpdate, count } = getQuery(event); return sendStream(event, stream.data);
const serverLastUpdate = topic.messages.at(-1)?.updatedAt;
const serverCount = topic.messages.length;
// TODO: there is potentially a race condition here where the client could
// connect at milisecond 87, and the last database write was at 0 (there
// is a write every 100ms for some operations), if there were tokens sent
// at 20 40 and 60, those tokens are lost to the client.
let streamController: ReadableStreamDefaultController;
let interval: NodeJS.Timeout;
const stream = new ReadableStream({
start(controller) {
streamController = controller;
controller.enqueue(`:connected\n\n`);
if ((lastUpdate && count) && (lastUpdate === serverLastUpdate?.toISOString() && count === serverCount)) {
controller.enqueue(':already_synced\n\n')
} else {
controller.enqueue(`data: ${JSON.stringify({
type: 'initial_state',
payload: topic
})}\n\n`);
}
interval = setInterval(() => {
controller.enqueue(`:heartbeat\n\n`);
}, 15000);
topicEvents.subscribe(topicId, streamController);
},
cancel() {
topicEvents.unsubscribe(topicId, streamController);
clearInterval(interval);
}
});
return sendStream(event, stream);
}); });
@@ -29,6 +29,12 @@ export default defineEventHandler(async (event) => {
const res = await db.update(topics) const res = await db.update(topics)
.set({ .set({
name, name,
...(name === undefined ? {} : {
titleSource: "manual",
renaming: false,
renameAttemptId: null,
renameExpiresAt: null,
}),
}) })
.where( .where(
and( and(
+119 -42
View File
@@ -1,6 +1,6 @@
import { modelMessageSchema } from 'ai'; import { modelMessageSchema } from 'ai';
import * as z from 'zod'; import * as z from 'zod';
import { attachments, messages } from '~~/drizzle/schema'; import { attachments, messageParts, messages } from '~~/drizzle/schema';
import { db } from '~~/server/lib/db'; import { db } from '~~/server/lib/db';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
@@ -9,6 +9,21 @@ export default defineEventHandler(async (event) => {
const topicId = getRouterParam(event, 'topicId')!; const topicId = getRouterParam(event, 'topicId')!;
const userId = event.context.user!.id; const userId = event.context.user!.id;
const topic = await db.query.topics.findFirst({
where: {
id: topicId,
userId,
},
});
if (!topic) {
throw createError({
statusCode: 404,
statusMessage: 'Not Found',
message: 'Topic not found',
});
}
const result = await readValidatedBody(event, z.object({ const result = await readValidatedBody(event, z.object({
message: z.intersection( message: z.intersection(
z.object({ z.object({
@@ -19,6 +34,7 @@ export default defineEventHandler(async (event) => {
), ),
}).safeParse); }).safeParse);
if (!result.success) { if (!result.success) {
console.log(result.error);
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
statusMessage: 'Bad Request', statusMessage: 'Bad Request',
@@ -28,49 +44,110 @@ export default defineEventHandler(async (event) => {
const { message } = result.data; const { message } = result.data;
await db.insert(messages).values({ if (['user', 'assistant'].includes(message.role) === false) {
// @ts-ignore - drizzle bug
id: message.id,
userId,
topicId,
parentMessageId: null,
generationId: null,
role: message.role,
content: message.content,
});
for (const fileId of message.fileIds || []) {
await db.insert(attachments).values({
userId,
topicId,
messageId: message.id,
fileId,
});
}
const usermessage = await db.query.messages.findFirst({
where: {
id: message.id,
},
with: {
attachments: {
with: {
file: true,
}
}
},
});
if (!usermessage) {
throw createError({ throw createError({
statusCode: 500, statusCode: 400,
statusMessage: 'Failed to insert message', statusMessage: 'Bad Request',
message: 'Failed to insert message', message: 'Invalid role',
}); });
} }
topicEvents.emit(topicId, { type: 'MESSAGE_CREATED', payload: usermessage });
return { switch (message.role) {
ok: true case 'user': {
}; await db.insert(messages).values({
// @ts-ignore - drizzle bug
id: message.id,
userId,
topicId,
parentMessageId: null,
generationId: null,
role: message.role,
content: message.content,
});
for (const fileId of message.fileIds || []) {
await db.insert(attachments).values({
userId,
topicId,
messageId: message.id,
fileId,
});
}
const usermessage = await db.query.messages.findFirst({
where: {
id: message.id,
},
with: {
attachments: {
with: {
file: true,
}
}
},
});
if (!usermessage) {
throw createError({
statusCode: 500,
statusMessage: 'Failed to insert message',
message: 'Failed to insert message',
});
}
topicEvents.emit(topicId, { type: 'MESSAGE_CREATED', payload: usermessage });
return {
ok: true
};
}
case 'assistant': {
const [dbmessage] = await db.insert(messages).values({
// @ts-ignore - drizzle bug
id: message.id,
userId,
topicId,
parentMessageId: null,
generationId: null,
role: message.role,
content: message.content,
}).returning();
if (!dbmessage) {
throw createError({
statusCode: 500,
statusMessage: 'Failed to insert message',
message: 'Failed to insert message',
});
}
const [part] = await db.insert(messageParts).values({
userId,
topicId,
messageId: dbmessage.id,
type: 'text',
content: message.content,
providerOptions: null,
finished: true,
createdAt: new Date(),
lastUpdatedAt: new Date(),
}).returning();
// TODO: transaction
if (!part) {
throw createError({
statusCode: 500,
statusMessage: 'Failed to insert message part',
message: 'Failed to insert message part',
});
}
topicEvents.emit(topicId, { type: 'MESSAGE_CREATED', payload: dbmessage });
topicEvents.emit(topicId, { type: 'text-start', payload: { messageId: dbmessage.id, part } });
topicEvents.emit(topicId, { type: 'text-end', payload: { messageId: dbmessage.id, partId: part.id, lastUpdatedAt: new Date(), content: message.content } });
return {
ok: true
};
}
}
}) })
+1
View File
@@ -30,6 +30,7 @@ export default defineEventHandler(async (event) => {
id, id,
userId, userId,
name, name,
titleSource: "provisional",
agentId, agentId,
createdAt: new Date(), createdAt: new Date(),
}); });
+17 -2
View File
@@ -39,7 +39,22 @@ export default defineEventHandler(async (event) => {
responseChecksumValidation: 'WHEN_REQUIRED', responseChecksumValidation: 'WHEN_REQUIRED',
}); });
const key = `veridian__uploads/${Date.now()}-${file.name.replace(/[^a-zA-Z0-9.]/g, '_')}-${event.context.user!.id}` const fileParts = file.name.split('.');
if (fileParts.length < 2) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid file name',
data: {
code: 'INVALID_FILE_NAME',
ok: false,
}
});
}
const fileExt = fileParts.pop()!;
const fileName = fileParts.join('.').replace(/[^a-zA-Z0-9.]/g, '_');
const key = `veridian__uploads/${Date.now()}-${fileName}-${event.context.user!.id}.${fileExt}`;
const command = new PutObjectCommand({ const command = new PutObjectCommand({
ACL: 'public-read', ACL: 'public-read',
@@ -56,7 +71,7 @@ export default defineEventHandler(async (event) => {
return { return {
url, url,
assetUrl: `${process.env.NUXT_PUBLIC_URL}/api/files/${key}`, assetUrl: `/api/files/${key}`,
}; };
} catch (error) { } catch (error) {
console.error('Failed to generate presigned URL:', error); console.error('Failed to generate presigned URL:', error);
-8
View File
@@ -67,11 +67,3 @@ export async function getProviderDetails(provider: ProviderDrizzle, providerApiK
} }
}); });
} }
export function isRerankingProvider(gateway: any): gateway is { reranking: (modelId: string) => any } {
return (
gateway !== null &&
'reranking' in gateway &&
typeof (gateway as any).reranking === 'function'
);
}
+14 -6
View File
@@ -1,5 +1,9 @@
import { createLiveTopicText } from './live-topic-text';
type StreamSink = Pick<ReadableStreamDefaultController, 'enqueue'>;
interface StreamConnection { interface StreamConnection {
controller: ReadableStreamDefaultController; controller: StreamSink;
lastPing: number; lastPing: number;
subscribedAt: number; subscribedAt: number;
} }
@@ -91,9 +95,13 @@ export const userEvents = {
}; };
const topicSubscribers = new Map<string, Set<StreamConnection>>(); const topicSubscribers = new Map<string, Set<StreamConnection>>();
const liveTopicText = createLiveTopicText();
export const topicEvents = { export const topicEvents = {
subscribe(topicId: string, controller: ReadableStreamDefaultController) { applyLiveText: liveTopicText.apply,
clearLiveText: liveTopicText.removeMessage,
subscribe(topicId: string, controller: StreamSink) {
if (!topicSubscribers.has(topicId)) { if (!topicSubscribers.has(topicId)) {
topicSubscribers.set(topicId, new Set()); topicSubscribers.set(topicId, new Set());
} }
@@ -120,7 +128,7 @@ export const topicEvents = {
(conn as any).pingInterval = pingInterval; (conn as any).pingInterval = pingInterval;
}, },
unsubscribe(topicId: string, controller: ReadableStreamDefaultController) { unsubscribe(topicId: string, controller: StreamSink) {
const connections = topicSubscribers.get(topicId); const connections = topicSubscribers.get(topicId);
if (!connections) return; if (!connections) return;
@@ -137,14 +145,14 @@ export const topicEvents = {
}, },
async emit(topicId: string, events: Array<{ type: string; payload: any; timestamp?: number }> | { type: string; payload: any; timestamp?: number }) { async emit(topicId: string, events: Array<{ type: string; payload: any; timestamp?: number }> | { type: string; payload: any; timestamp?: number }) {
const connections = topicSubscribers.get(topicId);
if (!connections || connections.size === 0) return;
if (!Array.isArray(events)) { if (!Array.isArray(events)) {
events = [events]; events = [events];
} }
for (const event of events) { for (const event of events) {
liveTopicText.update(topicId, event);
const connections = topicSubscribers.get(topicId);
if (!connections || connections.size === 0) continue;
const timestamp = event.timestamp ?? Date.now(); const timestamp = event.timestamp ?? Date.now();
const data = `data: ${JSON.stringify({ ...event, timestamp })}\n\n`; const data = `data: ${JSON.stringify({ ...event, timestamp })}\n\n`;
+37
View File
@@ -0,0 +1,37 @@
import { createHmac, timingSafeEqual } from 'crypto';
const DEFAULT_EXPIRY_MS = 30 * 60 * 1000; // 30 minutes
export function generateFileToken(
fileKey: string,
secret: string,
expiresInMs = DEFAULT_EXPIRY_MS,
): { exp: number; sig: string } {
const exp = Date.now() + expiresInMs;
const payload = `${fileKey}:${exp}`;
const sig = createHmac('sha256', secret).update(payload).digest('hex');
return { exp, sig };
}
export function verifyFileToken(
fileKey: string,
exp: number,
sig: string,
secret: string,
): boolean {
if (Date.now() > exp) {
return false;
}
const payload = `${fileKey}:${exp}`;
const expected = createHmac('sha256', secret).update(payload).digest('hex');
const sigBuffer = Buffer.from(sig, 'hex');
const expectedBuffer = Buffer.from(expected, 'hex');
if (sigBuffer.length !== expectedBuffer.length) {
return false;
}
return timingSafeEqual(sigBuffer, expectedBuffer);
}
+36
View File
@@ -0,0 +1,36 @@
import type { Result } from "../../types/result";
import { z } from "zod";
import { Err, Ok } from "../../types/result";
const scrapeResponseSchema = z.object({
success: z.boolean().optional(),
error: z.union([z.string(), z.object({ message: z.string() })]).optional(),
message: z.string().optional(),
data: z.unknown().optional(),
});
export const readFirecrawlResponse = async (response: Response): Promise<Result<string, string>> => {
const context = `Firecrawl scrape (HTTP ${response.status})`;
let body: unknown;
try {
body = await response.json();
} catch {
return Err(`${context}: received a non-JSON or unreadable response.`);
}
const parsed = scrapeResponseSchema.safeParse(body);
if (!parsed.success) {
return Err(`${context}: received an unexpected response format.`);
}
const payload = parsed.data;
const providerError = typeof payload.error === "string" ? payload.error : payload.error?.message;
if (!response.ok || payload.success === false || providerError) {
return Err(`${context}: ${providerError || payload.message || response.statusText || "Scrape failed without an error message."}`);
}
const content = z.object({ markdown: z.string() }).safeParse(payload.data);
if (!content.success || content.data.markdown.trim() === "") {
return Err(`${context}: no Markdown content was returned for this URL.`);
}
return Ok(content.data.markdown);
};
+65
View File
@@ -0,0 +1,65 @@
import { appendTextDelta } from '../../utils/topic-sync';
interface LivePart {
messageId: string;
part: Record<string, any> & { id: string; content: string };
}
/** Covers the interval between an emitted delta and its throttled DB write. */
export const createLiveTopicText = () => {
const topics = new Map<string, Map<string, LivePart>>();
const removeMessage = (topicId: string, messageId: string) => {
const parts = topics.get(topicId);
if (!parts) return;
for (const [id, entry] of parts) {
if (entry.messageId === messageId) parts.delete(id);
}
if (parts.size === 0) topics.delete(topicId);
};
return {
update(topicId: string, event: { type: string; payload: any }) {
const { type, payload } = event;
if (type === 'text-start') {
let parts = topics.get(topicId);
if (!parts) topics.set(topicId, parts = new Map());
parts.set(payload.part.id, {
messageId: payload.messageId,
part: { ...payload.part, toolCall: null, content: payload.part.content ?? '' },
});
} else if (type === 'text-delta') {
const entry = topics.get(topicId)?.get(payload.partId);
if (entry) {
entry.part.content = appendTextDelta(entry.part.content, payload.content, payload.offset);
entry.part.lastUpdatedAt = payload.lastUpdatedAt;
}
} else if (type === 'text-end' || type === 'text-delete') {
const parts = topics.get(topicId);
parts?.delete(payload.partId);
if (parts?.size === 0) topics.delete(topicId);
} else if (type === 'MESSAGE_DELETED') {
removeMessage(topicId, payload.id);
}
},
apply(topicId: string, messages: { id: string; parts: any[] }[]) {
const parts = topics.get(topicId);
if (!parts) return;
const byId = new Map(messages.map(message => [message.id, message]));
for (const entry of parts.values()) {
const message = byId.get(entry.messageId);
if (!message) continue;
const existing = message.parts.find(part => part.id === entry.part.id);
if (existing) {
if (!existing.finished && (existing.content?.length ?? 0) <= entry.part.content.length) {
existing.content = entry.part.content;
existing.lastUpdatedAt = entry.part.lastUpdatedAt;
}
} else {
message.parts.push({ ...entry.part });
}
}
},
removeMessage,
};
};
-116
View File
@@ -1,116 +0,0 @@
import { type ToolSet, type TextStreamPart, type ToolCallPart } from 'ai';
export function createLongcatTransformer<TOOLS extends ToolSet>(): (options: {
tools: TOOLS;
stopStream: () => void;
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>> {
let buffer = '';
let hasToolCallInStep = false;
let lastChunkId: string | undefined;
let lastChunkType: 'text' | 'reasoning' | undefined;
let step = 0;
return (_opts) => {
return new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({
transform(chunk, controller) {
if (chunk.type === 'finish-step' || chunk.type === 'finish') {
step++;
if (hasToolCallInStep) {
// We clone the chunk and overwrite the finishReason.
// This tricks the SDK into thinking the model requested a tool natively.
const modifiedChunk = {
...chunk,
finishReason: 'tool-calls' as const,
};
// Reset for the next potential step
if (chunk.type === 'finish-step') {
hasToolCallInStep = false;
}
controller.enqueue(modifiedChunk);
return;
}
}
if (chunk.type === 'text-start' || chunk.type === 'reasoning-start') {
lastChunkId = chunk.id;
lastChunkType = chunk.type.split('-')[1] as 'text' | 'reasoning';
}
// We only care about text chunks
if (chunk.type !== 'text-delta' && chunk.type !== 'reasoning-delta') {
controller.enqueue(chunk);
return;
}
buffer += chunk.text;
// Check if we have a full tool call in the buffer
const pattern = /<longcat_tool_call>([\s\S]*?)<\/longcat_tool_call>/g;
let lastIndex = 0;
let match;
while ((match = pattern.exec(buffer)) !== null) {
console.log("longcat tool call found at index", match.index);
// 1. Enqueue any text that appeared BEFORE the tool call
const textBefore = buffer.substring(lastIndex, match.index);
if (textBefore) {
controller.enqueue({ type: chunk.type, text: textBefore, id: lastChunkId ?? chunk.type.includes('reasoning') ? `reasoning-${step}` : `text-${step}` });
}
// 2. Parse the XML content
const content = match[1]!.trim();
const toolNameMatch = content.match(/^([^\s<]+)/);
if (toolNameMatch) {
hasToolCallInStep = true;
const toolName = toolNameMatch[1];
const args: Record<string, any> = {};
const argRegex = /<longcat_arg_key>(.*?)<\/longcat_arg_key>\s*<longcat_arg_value>(.*?)<\/longcat_arg_value>/gs;
let argMatch;
while ((argMatch = argRegex.exec(content)) !== null) {
args[argMatch[1]!.trim()] = argMatch[2]!.trim();
}
// 3. EMIT A TOOL CALL PART
// This is the "magic" - the SDK will see this and act as if the LLM
// called a native tool.
const toolCallId = `lc-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;
controller.enqueue({
type: 'tool-call',
// @ts-ignore
id: toolCallId,
toolCallId,
toolName,
input: args,
dynamic: true,
});
}
lastIndex = pattern.lastIndex;
}
// Keep the remaining buffer (unclosed tags) for the next chunk
buffer = buffer.substring(lastIndex);
// If there's no open tag starting, we can flush the buffer as text
if (!buffer.includes('<longcat_tool_call>')) {
if (buffer) {
controller.enqueue({ type: chunk.type, text: buffer, id: lastChunkId ?? chunk.type.includes('reasoning') ? `reasoning-${step}` : `text-${step}` });
buffer = '';
}
}
},
flush(controller) {
if (buffer && lastChunkId && lastChunkType) {
controller.enqueue({ type: `${lastChunkType}-delta`, text: buffer, id: lastChunkId });
}
}
});
};
}
+5 -1
View File
@@ -14,7 +14,11 @@ export default {
} }
return Ok({ return Ok({
gateway: createOpenAI({ name: 'ClosedRouter', apiKey, baseURL }), gateway: createOpenAI({
name: 'ClosedRouter',
apiKey,
baseURL
}),
streamTransformer: undefined, streamTransformer: undefined,
textTransformer: undefined, textTransformer: undefined,
}); });
+15 -25
View File
@@ -1,31 +1,21 @@
interface PendingRename { const pendingRenames = new Map<string, { attemptId: string; controller: AbortController }>();
topicId: string;
abortController: AbortController;
}
const pendingRenames: Map<string, AbortController> = new Map(); export const cancelPendingRename = (topicId: string, attemptId?: string | null) => {
const pending = pendingRenames.get(topicId);
export const cancelPendingRename = (topicId: string) => { if (pending && pending.attemptId === attemptId) {
const pendingRename = pendingRenames.get(topicId); pending.controller.abort();
if (pendingRename) {
pendingRename.abort();
pendingRenames.delete(topicId);
return true;
}
return false;
};
export const completeRename = (topicId: string) => {
const controller = pendingRenames.get(topicId);
if (controller) {
pendingRenames.delete(topicId); pendingRenames.delete(topicId);
} }
}; };
export const addPendingRename = (topicId: string) => { export const completeRename = (topicId: string, attemptId: string) => {
const abortController = new AbortController(); if (pendingRenames.get(topicId)?.attemptId === attemptId) {
pendingRenames.delete(topicId);
pendingRenames.set(topicId, abortController); }
return abortController; };
export const addPendingRename = (topicId: string, attemptId: string) => {
const controller = new AbortController();
pendingRenames.set(topicId, { attemptId, controller });
return controller;
}; };
+55
View File
@@ -0,0 +1,55 @@
import type { Result } from "../../types/result";
import { randomUUID } from "node:crypto";
import { z } from "zod";
import { Err, Ok } from "../../types/result";
const searchResponseSchema = z.object({
results: z.array(z.object({
title: z.string(),
url: z.string(),
content: z.string().optional().default(""),
})),
unresponsive_engines: z.array(z.tuple([z.string(), z.string()])).optional().default([]),
});
export const searchInputSchema = z.object({
query: z.string(),
results: z.number().int().positive().describe("Number of search results to return; choose this for every search."),
});
export const searchOutputSchema = z.object({
results: z.array(z.object({
title: z.string(),
link: z.string(),
sourceId: z.string().describe('Copy this exact ID into [[cite:ID]]. The literal "cite:" prefix is required; [[ID]] is invalid.').optional(),
snippet: z.string(),
})),
warnings: z.array(z.object({ engine: z.string(), reason: z.string() })),
});
type SearchOutput = z.infer<typeof searchOutputSchema>;
export const limitSearchResults = (output: SearchOutput, results: number): SearchOutput => ({
results: output.results.slice(0, results),
warnings: output.warnings,
});
export const readSearchResponse = (body: unknown): Result<SearchOutput, string> => {
const parsed = searchResponseSchema.safeParse(body);
if (!parsed.success) {
return Err("Search returned an unexpected response format.");
}
const warnings = parsed.data.unresponsive_engines.map(([engine, reason]) => ({ engine, reason }));
if (parsed.data.results.length === 0 && warnings.length > 0) {
return Err(`Search returned no results because engines failed: ${warnings.map(warning => `${warning.engine}: ${warning.reason}`).join("; ")}`);
}
return Ok({
results: parsed.data.results.map(item => ({
sourceId: `s${randomUUID().replaceAll("-", "")}`,
title: item.title,
link: item.url,
snippet: item.content,
})),
warnings,
});
};
+70
View File
@@ -0,0 +1,70 @@
import { createHash } from 'node:crypto';
import { Err, Ok } from '../../types/result';
import { serializeTopicSnapshot } from '../../utils/topic-sync';
import { topicEvents } from './events';
interface TopicSnapshot {
messages: { id: string; parts: any[] }[];
}
export const createTopicSnapshotStream = (topicId: string, offeredFingerprint: unknown) => {
let controller: ReadableStreamDefaultController | undefined;
let closed = false;
const buffered: string[] = [];
const sink = {
enqueue(frame: string) {
if (closed) return;
if (controller) controller.enqueue(frame);
else buffered.push(frame);
},
};
const close = () => {
closed = true;
topicEvents.unsubscribe(topicId, sink);
buffered.length = 0;
};
return {
close,
async open(readSnapshot: () => Promise<TopicSnapshot | undefined>) {
if (closed) return Err('closed' as const);
// Subscribe before reading: changes during the query must not be lost.
topicEvents.subscribe(topicId, sink);
let topic;
try {
topic = await readSnapshot();
} catch (error) {
close();
console.error('Failed to read topic snapshot', error);
return Err('read-failed' as const);
}
if (closed || !topic) {
close();
return Err('not-found' as const);
}
// Include deltas already emitted but not yet flushed to PostgreSQL.
topicEvents.applyLiveText(topicId, topic.messages);
const fingerprint = createHash('sha256').update(serializeTopicSnapshot(topic)).digest('hex');
const matches = offeredFingerprint === fingerprint;
return Ok(new ReadableStream<string>({
start(streamController) {
controller = streamController;
controller.enqueue(`data: ${JSON.stringify(matches
? { type: 'synced', payload: { id: topicId } }
: { type: 'initial_state', payload: topic })}\n\n`);
for (const frame of buffered) {
if (frame.startsWith('data: ')) {
const replay = { ...JSON.parse(frame.slice(6)), replay: true };
controller.enqueue(`data: ${JSON.stringify(replay)}\n\n`);
} else {
controller.enqueue(frame);
}
}
buffered.length = 0;
},
cancel: close,
}));
},
};
};
+84
View File
@@ -0,0 +1,84 @@
import { describe, expect, test } from "bun:test";
import { collectCitationSources } from "../app/utils/citations";
import { createMarkdownParser, getNodeChildren, getNodeTag } from "../app/utils/markdown";
import type { MarkdownAstNode } from "../app/utils/markdown";
import { readSearchResponse } from "../server/utils/search";
const citations = (nodes: MarkdownAstNode[]): MarkdownAstNode[] => nodes.flatMap(node =>
getNodeTag(node) === "source-cite" ? [node] : citations(getNodeChildren(node)),
);
describe("citation parsing", () => {
test("keeps citation markers as text unless explicitly enabled", async () => {
const content = "Claim [[cite:1234]] and [[cite:s7,s9]].";
for (const options of [{}, { citations: false }]) {
for (const streaming of [false, true]) {
const tree = await createMarkdownParser(options)(content, { streaming });
expect(citations(tree.nodes)).toEqual([]);
expect(getNodeChildren(tree.nodes[0] ?? "")).toEqual([content]);
}
}
});
test("renders single and grouped references as dedicated nodes", async () => {
const tree = await createMarkdownParser({ citations: true })("Claim [[cite:s7]]. Another [[cite:s7,s9]].");
expect(citations(tree.nodes)).toEqual([
["source-cite", { ids: "s7" }],
["source-cite", { ids: "s7,s9" }],
]);
});
test("does not interpret code, escaped references, or link labels", async () => {
for (const content of [
"`[[cite:s7]]`",
"```text\n[[cite:s7]]\n```",
"\\[[cite:s7]]",
"[Claim [[cite:s7]]](https://example.com)",
]) {
const tree = await createMarkdownParser({ citations: true })(content);
expect(citations(tree.nodes)).toEqual([]);
}
});
test("streaming never auto-completes a citation", async () => {
const parse = createMarkdownParser({ citations: true });
const content = "Claim [[cite:s7,s9]]";
for (let length = 1; length < content.length; length++) {
expect(citations((await parse(content.slice(0, length), { streaming: true })).nodes)).toEqual([]);
}
expect(citations((await parse(content, { streaming: true })).nodes)).toHaveLength(1);
expect(citations((await parse(content)).nodes)).toHaveLength(1);
});
});
describe("citation sources", () => {
test("IDs survive storage, including duplicate URLs and multiple searches", () => {
const first = readSearchResponse({ results: [
{ title: "One", url: "https://example.com" },
{ title: "Two", url: "https://example.com" },
] });
const second = readSearchResponse({ results: [{ title: "Three", url: "https://example.com" }] });
expect(first.ok && second.ok).toBe(true);
if (!first.ok || !second.ok) {
return;
}
const parts = [first.data, second.data].map(output => ({
toolCall: { toolName: "search", output: { type: "json", value: JSON.stringify(output) } },
}));
const sources = collectCitationSources(parts);
expect(sources.size).toBe(3);
expect(sources.get(first.data.results[0]?.sourceId ?? "")?.title).toBe("One");
expect(collectCitationSources(JSON.parse(JSON.stringify(parts)))).toEqual(sources);
expect(sources.has("invented")).toBe(false);
expect(collectCitationSources([]).size).toBe(0);
});
test("ignores legacy results, non-search tools, and unsafe URLs", () => {
const output = { type: "json", value: JSON.stringify({ results: [
{ title: "Legacy", link: "https://example.com" },
{ sourceId: "bad", title: "Bad", link: "javascript:alert(1)" },
] }) };
expect(collectCitationSources([{ toolCall: { toolName: "search", output } }]).size).toBe(0);
expect(collectCitationSources([{ toolCall: { toolName: "python", output } }]).size).toBe(0);
});
});
+25
View File
@@ -0,0 +1,25 @@
import { describe, expect, test } from 'bun:test';
import { getComposerHeight } from '../app/utils/composer-size';
describe('composer height stability', () => {
test('subpixel noise never oscillates around the two-line minimum', () => {
const heights = [47.99999, 48, 48.01].map(height => getComposerHeight(height, 24, 0, 0, true));
expect(heights).toEqual([48, 48, 48]);
});
test('grows and shrinks for actual lines, capped at ten lines', () => {
expect([24, 48, 72, 264, 72, 24].map(height => getComposerHeight(height, 24, 0, 0, true)))
.toEqual([48, 48, 72, 240, 72, 48]);
});
test('accounts for padding and borders according to box sizing', () => {
expect(getComposerHeight(80, 24, 8, 2, true)).toBe(82);
expect(getComposerHeight(80, 24, 8, 2, false)).toBe(72);
expect(getComposerHeight(10, 24, 8, 2, true)).toBe(58);
});
test('respects fractional typography rather than hardcoding a 24px line', () => {
expect(getComposerHeight(40, 20.25, 0, 0, true)).toBe(41);
expect(getComposerHeight(1000, 20.25, 0, 0, true)).toBe(203);
});
});
+52
View File
@@ -0,0 +1,52 @@
import type { MarkdownTree } from '../app/utils/markdown';
import { expect, test } from 'bun:test';
import { splitParagraphBreaks } from '../app/utils/markdown';
test('paragraph splitting preserves nested formatting without mutating a cached parser tree', () => {
const tree = { nodes: [
['p', {}, 'before', ['br', {}], ['strong', {}, 'after']],
['blockquote', {}, 'one', ['br', {}], ['em', {}, 'two']],
] } as MarkdownTree;
const original = structuredClone(tree);
const result = splitParagraphBreaks(tree);
expect(result.nodes).toEqual([
['p', {}, 'before'],
['p', {}, ['strong', {}, 'after']],
['blockquote', {}, ['p', {}, 'one'], ['p', {}, ['em', {}, 'two']]],
]);
expect(tree).toEqual(original);
expect(splitParagraphBreaks(tree)).toEqual(result);
});
test('br-free trees return by reference with no allocation', () => {
const tree = { nodes: [
['p', {}, 'plain ', ['strong', {}, 'text']],
['ul', {}, ['li', {}, 'one'], ['li', {}, 'two']],
] } as MarkdownTree;
expect(splitParagraphBreaks(tree)).toBe(tree);
});
test('single newlines still split into sibling paragraphs', () => {
const tree = { nodes: [
['p', {}, 'line one', ['br', {}], 'line two'],
] } as MarkdownTree;
expect(splitParagraphBreaks(tree).nodes).toEqual([
['p', {}, 'line one'],
['p', {}, 'line two'],
]);
});
test('nested changes rebuild only the affected path', () => {
const tree = { nodes: [
['blockquote', {}, ['p', {}, 'kept']],
['p', {}, 'split', ['br', {}], 'me'],
] } as MarkdownTree;
const result = splitParagraphBreaks(tree);
expect(result).not.toBe(tree);
expect(result.nodes[0]).toBe(tree.nodes[0]);
expect(result.nodes).toEqual([
['blockquote', {}, ['p', {}, 'kept']],
['p', {}, 'split'],
['p', {}, 'me'],
]);
});
+190
View File
@@ -0,0 +1,190 @@
import { describe, expect, test } from "bun:test";
import {
buildFocusedMessageTree,
buildMessageTree,
getActiveMessage,
getCurrentChildIndex,
getMessageCount,
getNextActiveChildId,
} from "../utils/message";
describe("message focus and versioning", () => {
test("reproduces and fixes the 3/2 focus bug after deleting root message and regenerating", () => {
// Step 1: Generate initial assistant message (A)
const msgA = {
id: "msg_a",
userId: "user_1",
topicId: "topic_1",
role: "assistant" as const,
content: "Response 1",
parentMessageId: null,
generationId: null,
activeChildId: null,
deleted: false,
createdAt: new Date(),
updatedAt: new Date(),
parts: [],
generation: null,
attachments: [],
};
let tree = buildMessageTree([msgA]);
expect(tree.length).toBe(1);
expect(getMessageCount(tree[0]!)).toBe(1);
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_a");
// Step 2: Regenerate the message -> creates child B, activeChildId points to B
const msgB = {
...msgA,
id: "msg_b",
content: "Response 2",
parentMessageId: "msg_a",
};
msgA.activeChildId = "msg_b";
tree = buildMessageTree([msgA, msgB]);
expect(tree.length).toBe(1);
expect(tree[0]!.children.length).toBe(1);
expect(getMessageCount(tree[0]!)).toBe(2);
// Variant 2 of 2: index should be 1 (displayed as 1 + 1 = 2)
expect(getCurrentChildIndex(tree[0]!)).toBe(1);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_b");
// Step 3: Switch focus to first, root message (A)
const prevId = getNextActiveChildId(tree[0]!, -1);
expect(prevId).toBeNull();
msgA.activeChildId = null;
tree = buildMessageTree([msgA, msgB]);
expect(getMessageCount(tree[0]!)).toBe(2);
// Variant 1 of 2: index should be 0 (displayed as 0 + 1 = 1)
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_a");
// Step 4: Delete the root message (A)
// Root is soft-deleted, active child switches to B
msgA.deleted = true;
msgA.activeChildId = "msg_b";
tree = buildMessageTree([msgA, msgB]);
expect(tree.length).toBe(1);
expect(tree[0]!.deleted).toBe(true);
expect(getMessageCount(tree[0]!)).toBe(1);
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_b");
// Step 5: Regenerate the message -> creates child C with parentMessageId: msg_a
const msgC = {
...msgA,
id: "msg_c",
content: "Response 3",
parentMessageId: "msg_a",
deleted: false,
};
msgA.activeChildId = "msg_c";
tree = buildMessageTree([msgA, msgB, msgC]);
expect(tree.length).toBe(1);
expect(tree[0]!.children.length).toBe(2);
expect(getMessageCount(tree[0]!)).toBe(2);
// Crucial test: When C (second child) is active, index must be 1 (displayed as 2/2, NOT 3/2!)
expect(getCurrentChildIndex(tree[0]!)).toBe(1);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_c");
// Verify switching focus back to B works seamlessly
const backToB = getNextActiveChildId(tree[0]!, -1);
expect(backToB).toBe("msg_b");
msgA.activeChildId = backToB;
tree = buildMessageTree([msgA, msgB, msgC]);
expect(getCurrentChildIndex(tree[0]!)).toBe(0);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_b");
// Verify switching forward to C works seamlessly
const forwardToC = getNextActiveChildId(tree[0]!, 1);
expect(forwardToC).toBe("msg_c");
msgA.activeChildId = forwardToC;
tree = buildMessageTree([msgA, msgB, msgC]);
expect(getCurrentChildIndex(tree[0]!)).toBe(1);
expect(getActiveMessage(tree[0]!)?.id).toBe("msg_c");
});
test("buildFocusedMessageTree excludes soft-deleted root messages", () => {
const root = {
id: "root",
userId: "user_1",
topicId: "topic_1",
role: "assistant" as const,
content: "Root",
parentMessageId: null,
generationId: null,
activeChildId: "child_1",
deleted: true,
createdAt: new Date(),
updatedAt: new Date(),
parts: [],
generation: null,
attachments: [],
};
const child1 = {
...root,
id: "child_1",
content: "Child 1",
parentMessageId: "root",
activeChildId: null,
deleted: false,
};
const tree = buildMessageTree([root, child1]);
const focused = buildFocusedMessageTree(tree);
expect(focused.length).toBe(1);
expect(focused[0]!.id).toBe("child_1");
expect(focused[0]!.deleted).toBe(false);
});
test("buildMessageTree filters out deleted roots with no children", () => {
const deletedEmptyRoot = {
id: "deleted_root",
userId: "user_1",
topicId: "topic_1",
role: "assistant" as const,
content: "Empty",
parentMessageId: null,
generationId: null,
activeChildId: null,
deleted: true,
createdAt: new Date(),
updatedAt: new Date(),
parts: [],
generation: null,
attachments: [],
};
const tree = buildMessageTree([deletedEmptyRoot]);
expect(tree.length).toBe(0);
});
test("getNextActiveChildId clamps to bounds and handles null fallback", () => {
const root = {
id: "root",
deleted: true,
activeChildId: null as string | null,
children: [
{ id: "child_a" },
{ id: "child_b" },
],
};
// If activeChildId is null and root is deleted, going left stays at child_a
expect(getNextActiveChildId(root, -1)).toBe("child_a");
// Going right moves to child_b
expect(getNextActiveChildId(root, 1)).toBe("child_b");
// When at child_b, going right clamps to child_b
root.activeChildId = "child_b";
expect(getNextActiveChildId(root, 1)).toBe("child_b");
});
});
+82
View File
@@ -0,0 +1,82 @@
import { describe, expect, test } from "bun:test";
import { limitSearchResults, readSearchResponse, searchInputSchema, searchOutputSchema } from "../server/utils/search";
import { readSearchSources, readSearchWarnings } from "../app/utils/tool-rendering";
const site = { title: "Example", url: "https://example.com", content: "Example content" };
describe("search invocation count", () => {
test("requires a positive integer on every invocation", () => {
expect(searchInputSchema.safeParse({ query: "example" }).success).toBe(false);
for (const results of [0, -1, 1.5, "2"]) {
expect(searchInputSchema.safeParse({ query: "example", results }).success).toBe(false);
}
expect(searchInputSchema.safeParse({ query: "example", results: 2 }).success).toBe(true);
});
test("slices ranked search results without dropping warnings", () => {
const output = {
results: [
{ title: "First", link: "https://first.example", snippet: "" },
{ title: "Second", link: "https://second.example", snippet: "" },
],
warnings: [{ engine: "google", reason: "CAPTCHA" }],
};
expect(limitSearchResults(output, 1)).toEqual({
results: [output.results[0]],
warnings: output.warnings,
});
expect(limitSearchResults(output, 3)).toEqual(output);
});
});
describe("search engine failures", () => {
test("returns an error when empty results accompany engine failures", () => {
const result = readSearchResponse({
results: [],
unresponsive_engines: [["google", "Too many requests"], ["bing", "Suspended: Too many requests"]],
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain("google: Too many requests");
expect(result.error).toContain("bing: Suspended: Too many requests");
}
});
test("keeps partial results successful with renderable warnings", () => {
const result = readSearchResponse({ results: [site], unresponsive_engines: [["google", "CAPTCHA"]] });
expect(result.ok).toBe(true);
if (result.ok) {
expect(searchOutputSchema.safeParse(result.data).success).toBe(true);
expect(readSearchSources(result.data)?.[0]?.url).toBe(site.url + "/");
expect(readSearchWarnings(result.data)).toEqual([{ engine: "google", reason: "CAPTCHA" }]);
}
});
test("accepts genuinely empty results without engine failures", () => {
const result = readSearchResponse({ results: [] });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data).toEqual({ results: [], warnings: [] });
}
});
test("accepts results without snippets or failures", () => {
const result = readSearchResponse({ results: [{ title: site.title, url: site.url }] });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.data.results[0]?.snippet).toBe("");
expect(result.data.warnings).toEqual([]);
}
});
test("rejects malformed responses", () => {
expect(readSearchResponse({ error: "Invalid query" }).ok).toBe(false);
});
test("still renders legacy arrays and ignores malformed warnings", () => {
const legacy = [{ title: site.title, link: site.url, snippet: site.content }];
expect(readSearchSources(legacy)).toEqual(readSearchSources({ results: legacy }));
expect(readSearchWarnings(legacy)).toEqual([]);
expect(readSearchWarnings({ warnings: [null, {}, { engine: "google", reason: 429 }] })).toEqual([]);
});
});
+33
View File
@@ -0,0 +1,33 @@
import { expect, test } from "bun:test";
import { readStoredToolError, serializeToolError } from "../utils/tool-errors";
const validationError = "Invalid input for tool search: Type validation failed: Value: {\"query\":123}.\nError message: [{\"code\":\"custom\"}]";
test("tool Error instances persist their message, not an empty JSON object", () => {
expect(serializeToolError(new Error(validationError))).toBe(validationError);
expect(serializeToolError(validationError)).toBe(validationError);
expect(serializeToolError({ reason: "invalid input" })).toBe('{"reason":"invalid input"}');
});
test("text validation errors remain text on history replay", () => {
expect(readStoredToolError({ type: "text", value: validationError })).toEqual({
type: "error-text",
value: validationError,
});
});
test("legacy mislabeled JSON errors cannot break subsequent prompts", () => {
expect(readStoredToolError({ type: "json", value: validationError })).toEqual({
type: "error-text",
value: validationError,
});
});
test("preserves actual JSON errors and handles missing errors", () => {
expect(readStoredToolError({ type: "json", value: '{"reason":"bad input"}' })).toEqual({
type: "error-json",
value: { reason: "bad input" },
});
expect(readStoredToolError(null).type).toBe("error-text");
expect(readStoredToolError(undefined).type).toBe("error-text");
});
+169
View File
@@ -0,0 +1,169 @@
import { describe, expect, test } from "bun:test";
import {
domainInitial,
formatDuration,
formatToolValue,
getDomainColor,
getFaviconUrl,
getToolDisplayInfo,
parseToolValue,
readSearchSources,
} from "../app/utils/tool-rendering";
describe("stored tool values", () => {
test("handles missing, text, valid JSON and streaming JSON", () => {
expect(parseToolValue(null)).toBeNull();
expect(parseToolValue({ type: "text", value: "hello" })).toBe("hello");
expect(parseToolValue({ type: "json", value: '{"query":"gold"}' })).toEqual({ query: "gold" });
expect(formatToolValue({ type: "json", value: '{"query":' })).toBe('{"query":');
});
});
describe("search sources", () => {
test("distinguishes unavailable results from empty results", () => {
expect(readSearchSources(null)).toBeNull();
expect(readSearchSources("partial")).toBeNull();
expect(readSearchSources([])).toEqual([]);
});
test("rejects unsafe links, tolerates malformed entries and deduplicates pages", () => {
expect(readSearchSources([
null,
{ link: "javascript:alert(1)" },
{ link: "data:text/html,hello" },
{ link: "/relative" },
{ link: "https://example.com/page#one", title: "Example", snippet: "Evidence" },
{ link: "https://example.com/page#two", title: "Duplicate" },
{ link: "https://other.example/", title: 1 },
])).toEqual([
{ url: "https://example.com/page", hostname: "example.com", title: "Example", snippet: "Evidence" },
{ url: "https://other.example/", hostname: "other.example", title: "other.example", snippet: "" },
]);
});
});
describe("duration formatting", () => {
test("formats duration in seconds with 1 decimal", () => {
const start = new Date("2026-09-25T12:00:00.000Z");
const end = new Date("2026-09-25T12:00:02.100Z");
expect(formatDuration(start, end)).toBe("2.1s");
});
test("formats numeric timestamps", () => {
const now = 1700000000000;
expect(formatDuration(now, now + 1500)).toBe("1.5s");
expect(formatDuration(new Date(now).toISOString(), now + 3200)).toBe("3.2s");
});
test("formats short duration", () => {
const start = new Date("2026-09-25T12:00:00.000Z");
const end = new Date("2026-09-25T12:00:00.400Z");
expect(formatDuration(start, end)).toBe("0.4s");
});
test("returns null for invalid or negative or sub-threshold duration", () => {
expect(formatDuration(null, null)).toBeNull();
const start = new Date("2026-09-25T12:00:02.000Z");
const end = new Date("2026-09-25T12:00:01.000Z");
expect(formatDuration(start, end)).toBeNull();
});
});
describe("tool display info", () => {
test("handles search with results", () => {
const info = getToolDisplayInfo({
toolName: "search",
status: "completed",
input: { type: "json", value: '{"query":"drizzle pg enum best practices"}' },
output: {
type: "json",
value: JSON.stringify([
{ link: "https://orm.drizzle.team/docs/enum", title: "PostgreSQL enums | Drizzle ORM" },
]),
},
});
expect(info.verb).toBe("Searched 1 site");
expect(info.target).toBe('"drizzle pg enum best practices"');
expect(info.icon).toBe("i-mynaui-search");
});
test("handles read file", () => {
const info = getToolDisplayInfo({
toolName: "read_file",
status: "completed",
input: { type: "json", value: '{"path":"drizzle/schema.ts"}' },
});
expect(info.verb).toBe("Read");
expect(info.target).toBe("drizzle/schema.ts");
expect(info.icon).toBe("i-mynaui-file");
});
test("handles file edit pending", () => {
const info = getToolDisplayInfo({
toolName: "edit_file",
status: "pending",
input: { type: "json", value: '{"path":"server/api/chat/generate.ts"}' },
});
expect(info.verb).toBe("Editing");
expect(info.target).toBe("server/api/chat/generate.ts");
expect(info.icon).toBe("i-mynaui-edit");
});
test("handles query failed", () => {
const info = getToolDisplayInfo({
toolName: "query",
status: "failed",
input: { type: "json", value: '{"query":"select * from tool_calls"}' },
error: { type: "text", value: 'relation "tool_calls" does not exist' },
});
expect(info.verb).toBe("Query failed");
expect(info.target).toBe("select * from tool_calls");
expect(info.icon).toBe("i-mynaui-database");
});
test("handles fetch cancelled", () => {
const info = getToolDisplayInfo({
toolName: "fetchUrl",
status: "cancelled",
input: { type: "json", value: '{"url":"https://orm.drizzle.team/docs/..."}' },
});
expect(info.verb).toBe("Fetch cancelled");
expect(info.target).toBe("https://orm.drizzle.team/docs/...");
expect(info.icon).toBe("i-mynaui-link");
});
test("handles python execution", () => {
const info = getToolDisplayInfo({
toolName: "python",
status: "completed",
input: { type: "json", value: '{"code":"2 + 2"}' },
output: { type: "json", value: '{"output":"4"}' },
});
expect(info.verb).toBe("Ran Python");
expect(info.target).toBe("2 + 2");
expect(info.icon).toBe("i-mynaui-terminal");
});
});
describe("domain utilities", () => {
test("extracts domain initial correctly", () => {
expect(domainInitial("goldprice.org")).toBe("G");
expect(domainInitial("www.kitco.com")).toBe("K");
expect(domainInitial("apmex.com")).toBe("A");
});
test("returns consistent domain colors", () => {
const color1 = getDomainColor("goldprice.org");
const color2 = getDomainColor("goldprice.org");
expect(color1).toBe(color2);
expect(typeof color1).toBe("string");
expect(color1.startsWith("#")).toBe(true);
});
test("formats DuckDuckGo favicon service url", () => {
expect(getFaviconUrl("goldprice.org")).toBe("https://icons.duckduckgo.com/ip3/goldprice.org.ico");
expect(getFaviconUrl("www.kitco.com")).toBe("https://icons.duckduckgo.com/ip3/kitco.com.ico");
});
});
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, test } from "bun:test";
import { addPendingRename, cancelPendingRename, completeRename } from "../server/utils/renames";
import { provisionalTopicTitle } from "../utils/topic-title";
describe("rename attempt ownership", () => {
test("an old completion cannot remove a newer attempt", () => {
addPendingRename("topic", "old");
const current = addPendingRename("topic", "new");
completeRename("topic", "old");
cancelPendingRename("topic", "new");
expect(current.signal.aborted).toBe(true);
});
test("an old cancellation cannot abort a newer attempt", () => {
const current = addPendingRename("topic", "new");
cancelPendingRename("topic", "old");
expect(current.signal.aborted).toBe(false);
completeRename("topic", "new");
});
test("cancellation is safe after completion or restart", () => {
cancelPendingRename("missing", "old");
const current = addPendingRename("topic", "new");
completeRename("topic", "new");
cancelPendingRename("topic", "new");
expect(current.signal.aborted).toBe(false);
});
});
describe("provisional titles", () => {
test("uses the first meaningful line and normalizes whitespace", () => {
expect(provisionalTopicTitle({ content: "\n Help me debug this\nMore details" })).toBe("Help me debug this");
});
test("names attachment-only conversations", () => {
expect(provisionalTopicTitle({ content: "", files: [{ name: "photo.jpg" }] })).toBe("photo.jpg");
expect(provisionalTopicTitle({ content: "", fileIds: ["image"] })).toBe("Attached files");
});
test("bounds title length and handles empty input", () => {
expect(provisionalTopicTitle({ content: "a".repeat(200) })).toHaveLength(100);
expect(provisionalTopicTitle()).toBe("New Topic");
});
});

Some files were not shown because too many files have changed in this diff Show More