mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat(web): add tool message ui for built-in tools
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
"@memoh/sdk": "workspace:*",
|
||||
"@memoh/ui": "workspace:*",
|
||||
"@pinia/colada": "^0.21.1",
|
||||
"@shikijs/transformers": "^4.0.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
|
||||
@@ -253,6 +253,22 @@ watch(() => props.botId, () => {
|
||||
onMounted(() => {
|
||||
restoreFileFromUrl()
|
||||
})
|
||||
|
||||
function navigateTo(dirPath: string) {
|
||||
openFile.value = null
|
||||
syncFileToUrl(null)
|
||||
void loadDirectory(dirPath)
|
||||
}
|
||||
|
||||
function openFileByPath(filePath: string) {
|
||||
const name = filePath.split('/').pop() ?? ''
|
||||
const dir = filePath.substring(0, filePath.lastIndexOf('/')) || '/'
|
||||
openFile.value = { path: filePath, name, isDir: false }
|
||||
syncFileToUrl(filePath)
|
||||
void loadDirectory(dir)
|
||||
}
|
||||
|
||||
defineExpose({ navigateTo, openFileByPath })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ref } from 'vue'
|
||||
import { getLanguageByFilename } from '@/components/file-manager/utils'
|
||||
|
||||
import type { HighlighterGeneric, BundledLanguage, BundledTheme } from 'shiki'
|
||||
|
||||
type Highlighter = HighlighterGeneric<BundledLanguage, BundledTheme>
|
||||
|
||||
let highlighterPromise: Promise<Highlighter> | null = null
|
||||
const loadedLangs = new Set<string>(['plaintext'])
|
||||
|
||||
async function getHighlighter(): Promise<Highlighter> {
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = import('shiki').then((m) =>
|
||||
m.createHighlighter({ themes: ['github-dark', 'github-light'], langs: [] }),
|
||||
)
|
||||
}
|
||||
return highlighterPromise
|
||||
}
|
||||
|
||||
async function ensureLang(hl: Highlighter, lang: string) {
|
||||
if (loadedLangs.has(lang)) return
|
||||
try {
|
||||
await hl.loadLanguage(lang as BundledLanguage)
|
||||
loadedLangs.add(lang)
|
||||
} catch {
|
||||
loadedLangs.add(lang)
|
||||
}
|
||||
}
|
||||
|
||||
export function useShikiHighlighter() {
|
||||
const html = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
async function highlight(code: string, filename: string) {
|
||||
loading.value = true
|
||||
try {
|
||||
const lang = getLanguageByFilename(filename)
|
||||
const hl = await getHighlighter()
|
||||
await ensureLang(hl, lang)
|
||||
html.value = hl.codeToHtml(code, {
|
||||
lang: loadedLangs.has(lang) ? lang : 'plaintext',
|
||||
themes: { light: 'github-light', dark: 'github-dark' },
|
||||
})
|
||||
} catch {
|
||||
html.value = `<pre>${escapeHtml(code)}</pre>`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function highlightDiff(oldText: string, newText: string, filename: string) {
|
||||
loading.value = true
|
||||
try {
|
||||
const lang = getLanguageByFilename(filename)
|
||||
const hl = await getHighlighter()
|
||||
await ensureLang(hl, lang)
|
||||
const effectiveLang = loadedLangs.has(lang) ? lang : 'plaintext'
|
||||
const themes = { light: 'github-light', dark: 'github-dark' }
|
||||
|
||||
const oldHtml = oldText
|
||||
? hl.codeToHtml(oldText, { lang: effectiveLang, themes })
|
||||
: ''
|
||||
const newHtml = newText
|
||||
? hl.codeToHtml(newText, { lang: effectiveLang, themes })
|
||||
: ''
|
||||
|
||||
html.value =
|
||||
(oldHtml ? `<div class="diff-block diff-remove">${oldHtml}</div>` : '') +
|
||||
(newHtml ? `<div class="diff-block diff-add">${newHtml}</div>` : '')
|
||||
} catch {
|
||||
html.value = `<pre>${escapeHtml(`- ${oldText}\n+ ${newText}`)}</pre>`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { html, loading, highlight, highlightDiff }
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
export function resolveLanguage(filename: string): string {
|
||||
return getLanguageByFilename(filename)
|
||||
}
|
||||
|
||||
export function extractFilename(path: string): string {
|
||||
return path.split('/').pop() ?? path
|
||||
}
|
||||
@@ -136,6 +136,14 @@
|
||||
"toolRunning": "Running",
|
||||
"toolInput": "Input",
|
||||
"toolResult": "Result",
|
||||
"toolWriteContent": "Content",
|
||||
"toolEditChanges": "Changes",
|
||||
"toolSearchResults": "{count} results",
|
||||
"toolSearchResultsLabel": "Results",
|
||||
"toolExecOutput": "Output",
|
||||
"toolExecExit": "exit: {code}",
|
||||
"toolExecError": "Error",
|
||||
"toolScheduleItems": "{count} items",
|
||||
"unknownUser": "{platform} User",
|
||||
"files": "Files"
|
||||
},
|
||||
|
||||
@@ -132,6 +132,14 @@
|
||||
"toolRunning": "运行中",
|
||||
"toolInput": "输入",
|
||||
"toolResult": "结果",
|
||||
"toolWriteContent": "内容",
|
||||
"toolEditChanges": "变更",
|
||||
"toolSearchResults": "{count} 条结果",
|
||||
"toolSearchResultsLabel": "结果",
|
||||
"toolExecOutput": "输出",
|
||||
"toolExecExit": "退出: {code}",
|
||||
"toolExecError": "错误",
|
||||
"toolScheduleItems": "{count} 条",
|
||||
"unknownUser": "{platform}用户",
|
||||
"files": "文件管理"
|
||||
},
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
</SheetHeader>
|
||||
<FileManager
|
||||
v-if="currentBotId"
|
||||
ref="fileManagerRef"
|
||||
:bot-id="currentBotId"
|
||||
:sync-url="false"
|
||||
class="flex-1 min-h-0"
|
||||
@@ -198,7 +199,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick, onMounted } from 'vue'
|
||||
import { ref, computed, watch, nextTick, onMounted, provide } from 'vue'
|
||||
import { Textarea, Button, Avatar, AvatarImage, AvatarFallback, Badge, InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupTextarea, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@memoh/ui'
|
||||
import { useChatStore } from '@/store/chat-list'
|
||||
import { storeToRefs } from 'pinia'
|
||||
@@ -206,12 +207,26 @@ import MessageItem from './message-item.vue'
|
||||
import MediaGalleryLightbox from './media-gallery-lightbox.vue'
|
||||
import FileManager from '@/components/file-manager/index.vue'
|
||||
import { useMediaGallery } from '../composables/useMediaGallery'
|
||||
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
|
||||
import type { ChatAttachment } from '@/composables/api/useChat'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const pendingFiles = ref<File[]>([])
|
||||
const fileManagerOpen = ref(false)
|
||||
const fileManagerRef = ref<InstanceType<typeof FileManager> | null>(null)
|
||||
|
||||
provide(openInFileManagerKey, (path: string, isDir = false) => {
|
||||
fileManagerOpen.value = true
|
||||
nextTick(() => {
|
||||
if (!fileManagerRef.value) return
|
||||
if (isDir) {
|
||||
fileManagerRef.value.navigateTo(path)
|
||||
} else {
|
||||
fileManagerRef.value.openFileByPath(path)
|
||||
}
|
||||
})
|
||||
})
|
||||
const {
|
||||
messages,
|
||||
streaming,
|
||||
|
||||
@@ -1,87 +1,58 @@
|
||||
<template>
|
||||
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||
class="size-3"
|
||||
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||
<ToolCallRead
|
||||
v-if="block.toolName === 'read'"
|
||||
:block="block"
|
||||
/>
|
||||
<span class="font-mono font-medium text-xs">
|
||||
{{ block.toolName }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="block.done"
|
||||
variant="secondary"
|
||||
class="text-[10px] ml-auto"
|
||||
>
|
||||
{{ $t('chat.toolDone') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
<ToolCallWrite
|
||||
v-else-if="block.toolName === 'write'"
|
||||
:block="block"
|
||||
/>
|
||||
<ToolCallEdit
|
||||
v-else-if="block.toolName === 'edit'"
|
||||
:block="block"
|
||||
/>
|
||||
<ToolCallList
|
||||
v-else-if="block.toolName === 'list'"
|
||||
:block="block"
|
||||
/>
|
||||
<ToolCallExec
|
||||
v-else-if="block.toolName === 'exec'"
|
||||
:block="block"
|
||||
/>
|
||||
<ToolCallWebSearch
|
||||
v-else-if="block.toolName === 'web_search'"
|
||||
:block="block"
|
||||
/>
|
||||
<ToolCallSchedule
|
||||
v-else-if="scheduleTools.has(block.toolName)"
|
||||
:block="block"
|
||||
/>
|
||||
<ToolCallGeneric
|
||||
v-else
|
||||
variant="outline"
|
||||
class="text-[10px] ml-auto"
|
||||
>
|
||||
{{ $t('chat.toolRunning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Input (collapsible) -->
|
||||
<Collapsible
|
||||
v-if="block.input"
|
||||
v-model:open="inputOpen"
|
||||
>
|
||||
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'chevron-right']"
|
||||
class="size-2.5 transition-transform"
|
||||
:class="{ 'rotate-90': inputOpen }"
|
||||
:block="block"
|
||||
/>
|
||||
{{ $t('chat.toolInput') }}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<pre class="px-3 pb-2 text-xs text-muted-foreground overflow-x-auto whitespace-pre-wrap break-all">{{ formatJson(block.input) }}</pre>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<!-- Result (collapsible) -->
|
||||
<Collapsible
|
||||
v-if="block.done && block.result != null"
|
||||
v-model:open="resultOpen"
|
||||
>
|
||||
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full border-t border-muted">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'chevron-right']"
|
||||
class="size-2.5 transition-transform"
|
||||
:class="{ 'rotate-90': resultOpen }"
|
||||
/>
|
||||
{{ $t('chat.toolResult') }}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<pre class="px-3 pb-2 text-xs text-muted-foreground overflow-x-auto whitespace-pre-wrap break-all">{{ formatJson(block.result) }}</pre>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||
import type { ToolCallBlock } from '@/store/chat-list'
|
||||
import ToolCallRead from './tool-call-read.vue'
|
||||
import ToolCallWrite from './tool-call-write.vue'
|
||||
import ToolCallEdit from './tool-call-edit.vue'
|
||||
import ToolCallList from './tool-call-list.vue'
|
||||
import ToolCallExec from './tool-call-exec.vue'
|
||||
import ToolCallWebSearch from './tool-call-web-search.vue'
|
||||
import ToolCallSchedule from './tool-call-schedule.vue'
|
||||
import ToolCallGeneric from './tool-call-generic.vue'
|
||||
|
||||
defineProps<{
|
||||
block: ToolCallBlock
|
||||
}>()
|
||||
|
||||
const inputOpen = ref(false)
|
||||
const resultOpen = ref(false)
|
||||
|
||||
function formatJson(val: unknown): string {
|
||||
if (typeof val === 'string') return val
|
||||
try {
|
||||
return JSON.stringify(val, null, 2)
|
||||
} catch {
|
||||
return String(val)
|
||||
}
|
||||
}
|
||||
const scheduleTools = new Set([
|
||||
'list_schedule',
|
||||
'get_schedule',
|
||||
'create_schedule',
|
||||
'update_schedule',
|
||||
'delete_schedule',
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||
class="size-3"
|
||||
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'pen-to-square']"
|
||||
class="size-3 text-muted-foreground"
|
||||
/>
|
||||
<button
|
||||
class="font-mono text-xs truncate hover:underline text-foreground cursor-pointer"
|
||||
:title="filePath"
|
||||
@click="handleOpenFile"
|
||||
>
|
||||
{{ filePath }}
|
||||
</button>
|
||||
<Badge
|
||||
v-if="block.done"
|
||||
variant="secondary"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolDone') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolRunning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Collapsible
|
||||
v-if="hasChanges"
|
||||
v-model:open="diffOpen"
|
||||
>
|
||||
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'chevron-right']"
|
||||
class="size-2.5 transition-transform"
|
||||
:class="{ 'rotate-90': diffOpen }"
|
||||
/>
|
||||
{{ $t('chat.toolEditChanges') }}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div
|
||||
v-if="shiki.loading.value"
|
||||
class="px-3 pb-2 text-xs text-muted-foreground"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'spinner']"
|
||||
class="size-3 animate-spin"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="shiki-diff-container overflow-x-auto text-xs [&_pre]:bg-transparent! [&_pre]:p-3 [&_pre]:m-0 [&_code]:text-xs"
|
||||
v-html="shiki.html.value"
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject, watch } from 'vue'
|
||||
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||
import type { ToolCallBlock } from '@/store/chat-list'
|
||||
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
|
||||
import { useShikiHighlighter, extractFilename } from '@/composables/useShikiHighlighter'
|
||||
|
||||
const props = defineProps<{ block: ToolCallBlock }>()
|
||||
|
||||
const openInFileManager = inject(openInFileManagerKey, undefined)
|
||||
const shiki = useShikiHighlighter()
|
||||
const diffOpen = ref(false)
|
||||
|
||||
const filePath = computed(() => {
|
||||
const input = props.block.input as Record<string, unknown> | undefined
|
||||
return (input?.path as string) ?? ''
|
||||
})
|
||||
|
||||
const oldText = computed(() => {
|
||||
const input = props.block.input as Record<string, unknown> | undefined
|
||||
return (input?.old_text as string) ?? ''
|
||||
})
|
||||
|
||||
const newText = computed(() => {
|
||||
const input = props.block.input as Record<string, unknown> | undefined
|
||||
return (input?.new_text as string) ?? ''
|
||||
})
|
||||
|
||||
const hasChanges = computed(() => oldText.value || newText.value)
|
||||
|
||||
watch(diffOpen, (open) => {
|
||||
if (open && hasChanges.value && !shiki.html.value) {
|
||||
void shiki.highlightDiff(oldText.value, newText.value, extractFilename(filePath.value))
|
||||
}
|
||||
})
|
||||
|
||||
function handleOpenFile() {
|
||||
if (filePath.value && openInFileManager) {
|
||||
openInFileManager(filePath.value, false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.shiki-diff-container .diff-block pre {
|
||||
margin: 0 !important;
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
.shiki-diff-container .diff-remove {
|
||||
background-color: oklch(0.55 0.12 25 / 0.12);
|
||||
border-left: 3px solid oklch(0.55 0.12 25 / 0.5);
|
||||
}
|
||||
.shiki-diff-container .diff-add {
|
||||
background-color: oklch(0.55 0.12 145 / 0.12);
|
||||
border-left: 3px solid oklch(0.55 0.12 145 / 0.5);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||
class="size-3"
|
||||
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'terminal']"
|
||||
class="size-3 text-muted-foreground"
|
||||
/>
|
||||
<span
|
||||
class="font-mono text-xs truncate text-foreground"
|
||||
:title="command"
|
||||
>
|
||||
$ {{ displayCommand }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="block.done && exitCode !== null"
|
||||
:variant="exitCode === 0 ? 'secondary' : 'destructive'"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolExecExit', { code: exitCode }) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="block.done && isError"
|
||||
variant="destructive"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolExecError') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="block.done"
|
||||
variant="secondary"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolDone') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolRunning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Collapsible
|
||||
v-if="hasOutput"
|
||||
v-model:open="outputOpen"
|
||||
>
|
||||
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'chevron-right']"
|
||||
class="size-2.5 transition-transform"
|
||||
:class="{ 'rotate-90': outputOpen }"
|
||||
/>
|
||||
{{ $t('chat.toolExecOutput') }}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<pre
|
||||
v-if="stdout"
|
||||
class="px-3 pb-2 text-xs text-muted-foreground overflow-x-auto whitespace-pre-wrap break-all"
|
||||
>{{ stdout }}</pre>
|
||||
<pre
|
||||
v-if="stderr"
|
||||
class="px-3 pb-2 text-xs text-destructive/80 overflow-x-auto whitespace-pre-wrap break-all"
|
||||
>{{ stderr }}</pre>
|
||||
<pre
|
||||
v-if="errorText"
|
||||
class="px-3 pb-2 text-xs text-destructive overflow-x-auto whitespace-pre-wrap break-all"
|
||||
>{{ errorText }}</pre>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||
import type { ToolCallBlock } from '@/store/chat-list'
|
||||
|
||||
const props = defineProps<{ block: ToolCallBlock }>()
|
||||
|
||||
const outputOpen = ref(false)
|
||||
|
||||
const command = computed(() => {
|
||||
const input = props.block.input as Record<string, unknown> | undefined
|
||||
return (input?.command as string) ?? ''
|
||||
})
|
||||
|
||||
const displayCommand = computed(() => {
|
||||
const lines = command.value.split('\n')
|
||||
if (lines.length <= 1) return command.value
|
||||
return lines[0] + ' ...'
|
||||
})
|
||||
|
||||
function resolveResult() {
|
||||
if (!props.block.result) return null
|
||||
const result = props.block.result as Record<string, unknown>
|
||||
const sc = result.structuredContent as Record<string, unknown> | undefined
|
||||
return sc ?? result
|
||||
}
|
||||
|
||||
const isError = computed(() => {
|
||||
if (!props.block.result) return false
|
||||
const result = props.block.result as Record<string, unknown>
|
||||
return result.isError === true
|
||||
})
|
||||
|
||||
const errorText = computed(() => {
|
||||
if (!isError.value) return ''
|
||||
const result = props.block.result as Record<string, unknown>
|
||||
const content = result.content as Array<Record<string, unknown>> | undefined
|
||||
if (!Array.isArray(content)) return ''
|
||||
return content
|
||||
.filter((c) => c.type === 'text')
|
||||
.map((c) => c.text as string)
|
||||
.join('\n')
|
||||
})
|
||||
|
||||
const exitCode = computed(() => {
|
||||
const r = resolveResult()
|
||||
if (!r) return null
|
||||
const code = r.exit_code
|
||||
return typeof code === 'number' ? code : null
|
||||
})
|
||||
|
||||
const stdout = computed(() => {
|
||||
const r = resolveResult()
|
||||
return (r?.stdout as string) ?? ''
|
||||
})
|
||||
|
||||
const stderr = computed(() => {
|
||||
const r = resolveResult()
|
||||
return (r?.stderr as string) ?? ''
|
||||
})
|
||||
|
||||
const hasOutput = computed(() =>
|
||||
props.block.done && (stdout.value || stderr.value || errorText.value),
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||
class="size-3"
|
||||
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||
/>
|
||||
<span class="font-mono font-medium text-xs">
|
||||
{{ block.toolName }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="block.done"
|
||||
variant="secondary"
|
||||
class="text-[10px] ml-auto"
|
||||
>
|
||||
{{ $t('chat.toolDone') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
class="text-[10px] ml-auto"
|
||||
>
|
||||
{{ $t('chat.toolRunning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Collapsible
|
||||
v-if="block.input"
|
||||
v-model:open="inputOpen"
|
||||
>
|
||||
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'chevron-right']"
|
||||
class="size-2.5 transition-transform"
|
||||
:class="{ 'rotate-90': inputOpen }"
|
||||
/>
|
||||
{{ $t('chat.toolInput') }}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<pre class="px-3 pb-2 text-xs text-muted-foreground overflow-x-auto whitespace-pre-wrap break-all">{{ formatJson(block.input) }}</pre>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible
|
||||
v-if="block.done && block.result != null"
|
||||
v-model:open="resultOpen"
|
||||
>
|
||||
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full border-t border-muted">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'chevron-right']"
|
||||
class="size-2.5 transition-transform"
|
||||
:class="{ 'rotate-90': resultOpen }"
|
||||
/>
|
||||
{{ $t('chat.toolResult') }}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<pre class="px-3 pb-2 text-xs text-muted-foreground overflow-x-auto whitespace-pre-wrap break-all">{{ formatJson(block.result) }}</pre>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||
import type { ToolCallBlock } from '@/store/chat-list'
|
||||
|
||||
defineProps<{
|
||||
block: ToolCallBlock
|
||||
}>()
|
||||
|
||||
const inputOpen = ref(false)
|
||||
const resultOpen = ref(false)
|
||||
|
||||
function formatJson(val: unknown): string {
|
||||
if (typeof val === 'string') return val
|
||||
try {
|
||||
return JSON.stringify(val, null, 2)
|
||||
} catch {
|
||||
return String(val)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||
class="size-3"
|
||||
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'folder']"
|
||||
class="size-3 text-muted-foreground"
|
||||
/>
|
||||
<button
|
||||
class="font-mono text-xs truncate hover:underline text-foreground cursor-pointer"
|
||||
:title="dirPath"
|
||||
@click="handleOpenDir"
|
||||
>
|
||||
{{ dirPath }}
|
||||
</button>
|
||||
<Badge
|
||||
v-if="block.done"
|
||||
variant="secondary"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolDone') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolRunning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import { Badge } from '@memoh/ui'
|
||||
import type { ToolCallBlock } from '@/store/chat-list'
|
||||
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
|
||||
|
||||
const props = defineProps<{ block: ToolCallBlock }>()
|
||||
|
||||
const openInFileManager = inject(openInFileManagerKey, undefined)
|
||||
|
||||
const dirPath = computed(() => {
|
||||
const input = props.block.input as Record<string, unknown> | undefined
|
||||
return (input?.path as string) ?? ''
|
||||
})
|
||||
|
||||
function handleOpenDir() {
|
||||
if (dirPath.value && openInFileManager) {
|
||||
openInFileManager(dirPath.value, true)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||
class="size-3"
|
||||
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'file-lines']"
|
||||
class="size-3 text-muted-foreground"
|
||||
/>
|
||||
<button
|
||||
class="font-mono text-xs truncate hover:underline text-foreground cursor-pointer"
|
||||
:title="filePath"
|
||||
@click="handleOpenFile"
|
||||
>
|
||||
{{ filePath }}
|
||||
</button>
|
||||
<Badge
|
||||
v-if="block.done"
|
||||
variant="secondary"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolDone') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolRunning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import { Badge } from '@memoh/ui'
|
||||
import type { ToolCallBlock } from '@/store/chat-list'
|
||||
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
|
||||
|
||||
const props = defineProps<{ block: ToolCallBlock }>()
|
||||
|
||||
const openInFileManager = inject(openInFileManagerKey, undefined)
|
||||
|
||||
const filePath = computed(() => {
|
||||
const input = props.block.input as Record<string, unknown> | undefined
|
||||
return (input?.path as string) ?? ''
|
||||
})
|
||||
|
||||
function handleOpenFile() {
|
||||
if (filePath.value && openInFileManager) {
|
||||
openInFileManager(filePath.value, false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||
class="size-3"
|
||||
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'clock']"
|
||||
class="size-3 text-muted-foreground"
|
||||
/>
|
||||
<span class="font-mono font-medium text-xs text-muted-foreground">
|
||||
{{ block.toolName }}
|
||||
</span>
|
||||
<span
|
||||
v-if="label"
|
||||
class="text-xs truncate text-foreground"
|
||||
:title="label"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="block.done && isList && itemCount !== null"
|
||||
variant="secondary"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolScheduleItems', { count: itemCount }) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="block.done"
|
||||
variant="secondary"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolDone') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolRunning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Badge } from '@memoh/ui'
|
||||
import type { ToolCallBlock } from '@/store/chat-list'
|
||||
|
||||
const props = defineProps<{ block: ToolCallBlock }>()
|
||||
|
||||
const isList = computed(() => props.block.toolName === 'list_schedule')
|
||||
|
||||
const label = computed(() => {
|
||||
const input = props.block.input as Record<string, unknown> | undefined
|
||||
if (!input) return ''
|
||||
|
||||
const name = input.name as string | undefined
|
||||
const id = input.id as string | undefined
|
||||
const pattern = input.pattern as string | undefined
|
||||
|
||||
switch (props.block.toolName) {
|
||||
case 'create_schedule': {
|
||||
const parts = [name, pattern].filter(Boolean)
|
||||
return parts.join(' ')
|
||||
}
|
||||
case 'update_schedule':
|
||||
return name ?? id ?? ''
|
||||
case 'get_schedule':
|
||||
case 'delete_schedule':
|
||||
return id ?? ''
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const itemCount = computed(() => {
|
||||
if (!props.block.done || !props.block.result) return null
|
||||
const result = props.block.result as Record<string, unknown>
|
||||
const sc = result.structuredContent as Record<string, unknown> | undefined
|
||||
const items = (sc ?? result).items
|
||||
if (Array.isArray(items)) return items.length
|
||||
return null
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||
class="size-3"
|
||||
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'magnifying-glass']"
|
||||
class="size-3 text-muted-foreground"
|
||||
/>
|
||||
<span class="text-xs truncate text-foreground">
|
||||
{{ query }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="block.done && results.length"
|
||||
variant="secondary"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolSearchResults', { count: results.length }) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="block.done"
|
||||
variant="secondary"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolDone') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolRunning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Collapsible
|
||||
v-if="block.done && results.length"
|
||||
v-model:open="resultsOpen"
|
||||
>
|
||||
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'chevron-right']"
|
||||
class="size-2.5 transition-transform"
|
||||
:class="{ 'rotate-90': resultsOpen }"
|
||||
/>
|
||||
{{ $t('chat.toolSearchResultsLabel') }}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div class="px-3 pb-2 space-y-1.5">
|
||||
<div
|
||||
v-for="(item, i) in results"
|
||||
:key="i"
|
||||
class="flex flex-col gap-0.5"
|
||||
>
|
||||
<a
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-xs text-primary hover:underline truncate"
|
||||
:title="item.title"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
<span
|
||||
class="text-[10px] text-muted-foreground truncate"
|
||||
:title="item.url"
|
||||
>
|
||||
{{ item.url }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||
import type { ToolCallBlock } from '@/store/chat-list'
|
||||
|
||||
interface SearchResult {
|
||||
title: string
|
||||
url: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{ block: ToolCallBlock }>()
|
||||
|
||||
const resultsOpen = ref(false)
|
||||
|
||||
const query = computed(() => {
|
||||
const input = props.block.input as Record<string, unknown> | undefined
|
||||
return (input?.query as string) ?? ''
|
||||
})
|
||||
|
||||
const results = computed<SearchResult[]>(() => {
|
||||
if (!props.block.done || !props.block.result) return []
|
||||
const result = props.block.result as Record<string, unknown>
|
||||
const sc = result.structuredContent as Record<string, unknown> | undefined
|
||||
const items = (sc?.results ?? result.results) as SearchResult[] | undefined
|
||||
return Array.isArray(items) ? items : []
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="rounded-lg border bg-muted/30 text-sm overflow-hidden">
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-muted/50">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', block.done ? 'check' : 'spinner']"
|
||||
class="size-3"
|
||||
:class="block.done ? 'text-green-600 dark:text-green-400' : 'animate-spin text-muted-foreground'"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'pen']"
|
||||
class="size-3 text-muted-foreground"
|
||||
/>
|
||||
<button
|
||||
class="font-mono text-xs truncate hover:underline text-foreground cursor-pointer"
|
||||
:title="filePath"
|
||||
@click="handleOpenFile"
|
||||
>
|
||||
{{ filePath }}
|
||||
</button>
|
||||
<Badge
|
||||
v-if="block.done"
|
||||
variant="secondary"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolDone') }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
class="text-[10px] ml-auto shrink-0"
|
||||
>
|
||||
{{ $t('chat.toolRunning') }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Collapsible
|
||||
v-if="content"
|
||||
v-model:open="contentOpen"
|
||||
>
|
||||
<CollapsibleTrigger class="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground cursor-pointer w-full">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'chevron-right']"
|
||||
class="size-2.5 transition-transform"
|
||||
:class="{ 'rotate-90': contentOpen }"
|
||||
/>
|
||||
{{ $t('chat.toolWriteContent') }}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div
|
||||
v-if="shiki.loading.value"
|
||||
class="px-3 pb-2 text-xs text-muted-foreground"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'spinner']"
|
||||
class="size-3 animate-spin"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="shiki-container overflow-x-auto text-xs [&_pre]:bg-transparent! [&_pre]:p-3 [&_pre]:m-0 [&_code]:text-xs"
|
||||
v-html="shiki.html.value"
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject, watch } from 'vue'
|
||||
import { Badge, Collapsible, CollapsibleTrigger, CollapsibleContent } from '@memoh/ui'
|
||||
import type { ToolCallBlock } from '@/store/chat-list'
|
||||
import { openInFileManagerKey } from '../composables/useFileManagerProvider'
|
||||
import { useShikiHighlighter, extractFilename } from '@/composables/useShikiHighlighter'
|
||||
|
||||
const props = defineProps<{ block: ToolCallBlock }>()
|
||||
|
||||
const openInFileManager = inject(openInFileManagerKey, undefined)
|
||||
const shiki = useShikiHighlighter()
|
||||
const contentOpen = ref(false)
|
||||
|
||||
const filePath = computed(() => {
|
||||
const input = props.block.input as Record<string, unknown> | undefined
|
||||
return (input?.path as string) ?? ''
|
||||
})
|
||||
|
||||
const content = computed(() => {
|
||||
const input = props.block.input as Record<string, unknown> | undefined
|
||||
return (input?.content as string) ?? ''
|
||||
})
|
||||
|
||||
watch(contentOpen, (open) => {
|
||||
if (open && content.value && !shiki.html.value) {
|
||||
void shiki.highlight(content.value, extractFilename(filePath.value))
|
||||
}
|
||||
})
|
||||
|
||||
function handleOpenFile() {
|
||||
if (filePath.value && openInFileManager) {
|
||||
openInFileManager(filePath.value, false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
export type OpenInFileManager = (path: string, isDir?: boolean) => void
|
||||
|
||||
export const openInFileManagerKey: InjectionKey<OpenInFileManager> = Symbol('openInFileManager')
|
||||
@@ -11,6 +11,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dark .shiki,
|
||||
.dark .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities{
|
||||
|
||||
Generated
+43
@@ -315,6 +315,9 @@ importers:
|
||||
'@pinia/colada':
|
||||
specifier: ^0.21.1
|
||||
version: 0.21.2(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
|
||||
'@shikijs/transformers':
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
|
||||
@@ -1914,6 +1917,10 @@ packages:
|
||||
'@shikijs/core@3.21.0':
|
||||
resolution: {integrity: sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==}
|
||||
|
||||
'@shikijs/core@4.0.1':
|
||||
resolution: {integrity: sha512-vWvqi9JNgz1dRL9Nvog5wtx7RuNkf7MEPl2mU/cyUUxJeH1CAr3t+81h8zO8zs7DK6cKLMoU9TvukWIDjP4Lzg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/engine-javascript@2.5.0':
|
||||
resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==}
|
||||
|
||||
@@ -1935,6 +1942,10 @@ packages:
|
||||
'@shikijs/monaco@3.21.0':
|
||||
resolution: {integrity: sha512-vYUBk5e89vlJbrGuLXaL+gQNGRVYUQOTEeWOnWb89ZL4hwDOBdbPftm0m8cPuD6H6HOdw+C48rGnATuMmGTaJg==}
|
||||
|
||||
'@shikijs/primitive@4.0.1':
|
||||
resolution: {integrity: sha512-ns0hHZc5eWZuvuIEJz2pTx3Qecz0aRVYumVQJ8JgWY2tq/dH8WxdcVM49Fc2NsHEILNIT6vfdW9MF26RANWiTA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/themes@2.5.0':
|
||||
resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==}
|
||||
|
||||
@@ -1944,12 +1955,20 @@ packages:
|
||||
'@shikijs/transformers@2.5.0':
|
||||
resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==}
|
||||
|
||||
'@shikijs/transformers@4.0.1':
|
||||
resolution: {integrity: sha512-oE46W2eHpvD06+C0MBthd2YrDM6cktvJDFl764tOEXxfr3dAJhxMc0uNZ2tQXp+bkMgl4E7IL88Mj9RnSqiayw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/types@2.5.0':
|
||||
resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==}
|
||||
|
||||
'@shikijs/types@3.21.0':
|
||||
resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==}
|
||||
|
||||
'@shikijs/types@4.0.1':
|
||||
resolution: {integrity: sha512-EaygPEn57+jJ76mw+nTLvIpJMAcMPokFbrF8lufsZP7Ukk+ToJYEcswN1G0e49nUZAq7aCQtoeW219A8HK1ZOw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
|
||||
@@ -6584,6 +6603,14 @@ snapshots:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-html: 9.0.5
|
||||
|
||||
'@shikijs/core@4.0.1':
|
||||
dependencies:
|
||||
'@shikijs/primitive': 4.0.1
|
||||
'@shikijs/types': 4.0.1
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-to-html: 9.0.5
|
||||
|
||||
'@shikijs/engine-javascript@2.5.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 2.5.0
|
||||
@@ -6620,6 +6647,12 @@ snapshots:
|
||||
'@shikijs/types': 3.21.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@shikijs/primitive@4.0.1':
|
||||
dependencies:
|
||||
'@shikijs/types': 4.0.1
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
'@shikijs/themes@2.5.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 2.5.0
|
||||
@@ -6633,6 +6666,11 @@ snapshots:
|
||||
'@shikijs/core': 2.5.0
|
||||
'@shikijs/types': 2.5.0
|
||||
|
||||
'@shikijs/transformers@4.0.1':
|
||||
dependencies:
|
||||
'@shikijs/core': 4.0.1
|
||||
'@shikijs/types': 4.0.1
|
||||
|
||||
'@shikijs/types@2.5.0':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
@@ -6643,6 +6681,11 @@ snapshots:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
'@shikijs/types@4.0.1':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2': {}
|
||||
|
||||
'@sinclair/typebox@0.34.47': {}
|
||||
|
||||
Reference in New Issue
Block a user