page history session messages (#1099)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from "vue";
|
import { ref, computed, nextTick, watch } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import VirtualMessageList from "./VirtualMessageList.vue";
|
import VirtualMessageList from "./VirtualMessageList.vue";
|
||||||
import MessageItem from "./MessageItem.vue";
|
import MessageItem from "./MessageItem.vue";
|
||||||
@@ -9,6 +9,7 @@ import type { Session } from "@/stores/hermes/chat";
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
session?: Session | null; // Optional: use this session instead of chatStore.activeSession
|
session?: Session | null; // Optional: use this session instead of chatStore.activeSession
|
||||||
|
loadOlder?: (sessionId: string) => Promise<boolean>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
@@ -45,6 +46,16 @@ function scrollToAnchor(messageId: string, anchorId: string) {
|
|||||||
listRef.value?.scrollToAnchor(messageId, anchorId);
|
listRef.value?.scrollToAnchor(messageId, anchorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleTopReach() {
|
||||||
|
const session = activeSession.value;
|
||||||
|
if (!session?.hasMoreBefore || session.isLoadingOlderMessages || !props.loadOlder) return;
|
||||||
|
const snapshot = listRef.value?.captureScrollPosition() ?? null;
|
||||||
|
const loaded = await props.loadOlder(session.id);
|
||||||
|
if (!loaded) return;
|
||||||
|
await nextTick();
|
||||||
|
listRef.value?.restoreScrollPosition(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
// Scroll to bottom on session switch
|
// Scroll to bottom on session switch
|
||||||
watch(
|
watch(
|
||||||
() => activeSession.value?.id,
|
() => activeSession.value?.id,
|
||||||
@@ -97,6 +108,7 @@ defineExpose({
|
|||||||
<VirtualMessageList
|
<VirtualMessageList
|
||||||
ref="listRef"
|
ref="listRef"
|
||||||
:messages="displayMessages"
|
:messages="displayMessages"
|
||||||
|
@top-reach="handleTopReach"
|
||||||
>
|
>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
@@ -104,6 +116,14 @@ defineExpose({
|
|||||||
<p>{{ t("chat.emptyState") }}</p>
|
<p>{{ t("chat.emptyState") }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template #before>
|
||||||
|
<div
|
||||||
|
v-if="activeSession?.hasMoreBefore || activeSession?.isLoadingOlderMessages"
|
||||||
|
class="history-loader"
|
||||||
|
>
|
||||||
|
<span v-if="activeSession?.isLoadingOlderMessages" class="history-loader-spinner"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #item="{ message: msg }">
|
<template #item="{ message: msg }">
|
||||||
<MessageItem
|
<MessageItem
|
||||||
:message="msg"
|
:message="msg"
|
||||||
@@ -136,6 +156,34 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-loader {
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-loader-spinner {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.16);
|
||||||
|
border-top-color: $accent-primary;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.7s linear infinite;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
border-color: rgba(255, 255, 255, 0.18);
|
||||||
|
border-top-color: $accent-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active {
|
.fade-leave-active {
|
||||||
transition: opacity 0.4s ease;
|
transition: opacity 0.4s ease;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { copyToClipboard } from '@/utils/clipboard'
|
|||||||
import HistoryMessageList from '@/components/hermes/chat/HistoryMessageList.vue'
|
import HistoryMessageList from '@/components/hermes/chat/HistoryMessageList.vue'
|
||||||
import SessionListItem from '@/components/hermes/chat/SessionListItem.vue'
|
import SessionListItem from '@/components/hermes/chat/SessionListItem.vue'
|
||||||
import OutlinePanel from '@/components/hermes/chat/OutlinePanel.vue'
|
import OutlinePanel from '@/components/hermes/chat/OutlinePanel.vue'
|
||||||
import { batchDeleteSessions, deleteSession, fetchHermesSessions, fetchHermesSession, importHermesSession, type SessionSummary } from '@/api/hermes/sessions'
|
import { batchDeleteSessions, deleteSession, fetchHermesSessions, fetchHermesSession, fetchSessionMessagesPage, importHermesSession, type HermesMessage, type SessionSummary } from '@/api/hermes/sessions'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const profilesStore = useProfilesStore()
|
const profilesStore = useProfilesStore()
|
||||||
@@ -53,6 +53,8 @@ const contextMenuX = ref(0)
|
|||||||
const contextMenuY = ref(0)
|
const contextMenuY = ref(0)
|
||||||
let hermesSessionsRequestId = 0
|
let hermesSessionsRequestId = 0
|
||||||
|
|
||||||
|
const HISTORY_PAGE_SIZE = 300
|
||||||
|
|
||||||
function handleOutlineNavigate(target: { messageId: string; anchorId: string }) {
|
function handleOutlineNavigate(target: { messageId: string; anchorId: string }) {
|
||||||
historyMessageListRef.value?.scrollToAnchor(target.messageId, target.anchorId)
|
historyMessageListRef.value?.scrollToAnchor(target.messageId, target.anchorId)
|
||||||
}
|
}
|
||||||
@@ -107,56 +109,98 @@ const contextMenuOptions = computed<DropdownOption[]>(() => {
|
|||||||
return options
|
return options
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function mapHistoryMessages(messages: HermesMessage[]): Session['messages'] {
|
||||||
|
return messages.map(m => {
|
||||||
|
const msg: Session['messages'][number] = {
|
||||||
|
id: String(m.id),
|
||||||
|
role: m.role,
|
||||||
|
content: m.content || '',
|
||||||
|
timestamp: m.timestamp * 1000,
|
||||||
|
reasoning: m.reasoning || undefined,
|
||||||
|
systemType: m.role === 'command' ? 'command' : undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.role === 'tool') {
|
||||||
|
msg.toolName = m.tool_name || undefined
|
||||||
|
msg.toolCallId = m.tool_call_id || undefined
|
||||||
|
msg.toolArgs = m.tool_calls?.[0]?.function?.arguments
|
||||||
|
? JSON.stringify(m.tool_calls[0].function.arguments)
|
||||||
|
: undefined
|
||||||
|
msg.toolStatus = 'done'
|
||||||
|
msg.toolResult = m.content || undefined
|
||||||
|
msg.content = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionFromSummary(summary: SessionSummary, messages: Session['messages'] = []): Session {
|
||||||
|
return {
|
||||||
|
id: summary.id,
|
||||||
|
profile: summary.profile || undefined,
|
||||||
|
title: summary.title || '',
|
||||||
|
source: summary.source,
|
||||||
|
createdAt: summary.started_at * 1000,
|
||||||
|
updatedAt: (summary.last_active || summary.ended_at || summary.started_at) * 1000,
|
||||||
|
model: summary.model,
|
||||||
|
provider: summary.provider,
|
||||||
|
messageCount: summary.message_count,
|
||||||
|
messageTotal: summary.message_count,
|
||||||
|
loadedMessageCount: messages.length,
|
||||||
|
hasMoreBefore: false,
|
||||||
|
inputTokens: summary.input_tokens,
|
||||||
|
outputTokens: summary.output_tokens,
|
||||||
|
endedAt: summary.ended_at ? summary.ended_at * 1000 : undefined,
|
||||||
|
lastActiveAt: summary.last_active ? summary.last_active * 1000 : undefined,
|
||||||
|
workspace: summary.workspace || undefined,
|
||||||
|
messages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadHistorySession(sessionId: string, profile?: string | null) {
|
async function loadHistorySession(sessionId: string, profile?: string | null) {
|
||||||
const summary = findHistorySession(sessionId)
|
const summary = findHistorySession(sessionId)
|
||||||
const sessionProfile = profile || summary?.profile || null
|
const sessionProfile = profile || summary?.profile || null
|
||||||
// First, fetch the Hermes session detail
|
const page = await fetchSessionMessagesPage(sessionId, 0, HISTORY_PAGE_SIZE, sessionProfile)
|
||||||
const sessionDetail = await fetchHermesSession(sessionId, sessionProfile)
|
let sessionData: Session | null = null
|
||||||
if (!sessionDetail) {
|
|
||||||
message.error(t('chat.sessionNotFound'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert SessionDetail to Session format and add to chatStore
|
if (page) {
|
||||||
const sessionData: Session = {
|
const base = summary || page.session
|
||||||
id: sessionDetail.id,
|
sessionData = sessionFromSummary(base, mapHistoryMessages(page.messages))
|
||||||
profile: sessionDetail.profile || sessionProfile || undefined,
|
sessionData.profile = summary?.profile || sessionProfile || undefined
|
||||||
title: sessionDetail.title || '',
|
sessionData.messageCount = page.total
|
||||||
source: sessionDetail.source,
|
sessionData.messageTotal = page.total
|
||||||
createdAt: sessionDetail.started_at * 1000,
|
sessionData.loadedMessageCount = page.messages.length
|
||||||
updatedAt: (sessionDetail.last_active || sessionDetail.started_at) * 1000,
|
sessionData.hasMoreBefore = page.hasMore
|
||||||
model: sessionDetail.model,
|
} else {
|
||||||
messageCount: sessionDetail.message_count,
|
// Some imported/legacy Hermes sessions may only exist in Hermes state.db.
|
||||||
inputTokens: sessionDetail.input_tokens,
|
// Keep the old full-detail path as a compatibility fallback.
|
||||||
outputTokens: sessionDetail.output_tokens,
|
const sessionDetail = await fetchHermesSession(sessionId, sessionProfile)
|
||||||
endedAt: sessionDetail.ended_at ? sessionDetail.ended_at * 1000 : undefined,
|
if (!sessionDetail) {
|
||||||
lastActiveAt: sessionDetail.last_active ? sessionDetail.last_active * 1000 : undefined,
|
message.error(t('chat.sessionNotFound'))
|
||||||
workspace: sessionDetail.workspace || undefined,
|
return
|
||||||
messages: sessionDetail.messages.map(m => {
|
}
|
||||||
const msg: any = {
|
|
||||||
id: String(m.id),
|
|
||||||
sessionId: m.session_id,
|
|
||||||
role: m.role,
|
|
||||||
content: m.content || '',
|
|
||||||
timestamp: m.timestamp * 1000,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve tool-related fields
|
sessionData = {
|
||||||
if (m.role === 'tool') {
|
id: sessionDetail.id,
|
||||||
msg.toolName = m.tool_name
|
profile: sessionDetail.profile || sessionProfile || undefined,
|
||||||
msg.toolArgs = m.tool_calls?.[0]?.function?.arguments
|
title: sessionDetail.title || '',
|
||||||
? JSON.stringify(m.tool_calls[0].function.arguments)
|
source: sessionDetail.source,
|
||||||
: undefined
|
createdAt: sessionDetail.started_at * 1000,
|
||||||
msg.toolStatus = 'done'
|
updatedAt: (sessionDetail.last_active || sessionDetail.started_at) * 1000,
|
||||||
}
|
model: sessionDetail.model,
|
||||||
|
provider: sessionDetail.provider,
|
||||||
// Preserve reasoning field
|
messageCount: sessionDetail.message_count,
|
||||||
if (m.reasoning) {
|
messageTotal: sessionDetail.message_count,
|
||||||
msg.reasoning = m.reasoning
|
loadedMessageCount: sessionDetail.messages.length,
|
||||||
}
|
hasMoreBefore: false,
|
||||||
|
inputTokens: sessionDetail.input_tokens,
|
||||||
return msg
|
outputTokens: sessionDetail.output_tokens,
|
||||||
}),
|
endedAt: sessionDetail.ended_at ? sessionDetail.ended_at * 1000 : undefined,
|
||||||
|
lastActiveAt: sessionDetail.last_active ? sessionDetail.last_active * 1000 : undefined,
|
||||||
|
workspace: sessionDetail.workspace || undefined,
|
||||||
|
messages: mapHistoryMessages(sessionDetail.messages),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set history page's own session state (independent from chatStore)
|
// Set history page's own session state (independent from chatStore)
|
||||||
@@ -166,6 +210,34 @@ async function loadHistorySession(sessionId: string, profile?: string | null) {
|
|||||||
if (mobileQuery?.matches) showSessions.value = false
|
if (mobileQuery?.matches) showSessions.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadOlderHistoryMessages(sessionId: string): Promise<boolean> {
|
||||||
|
const target = historySession.value
|
||||||
|
if (!target || target.id !== sessionId || target.isLoadingOlderMessages || !target.hasMoreBefore) return false
|
||||||
|
const offset = target.loadedMessageCount || 0
|
||||||
|
target.isLoadingOlderMessages = true
|
||||||
|
try {
|
||||||
|
const page = await fetchSessionMessagesPage(sessionId, offset, HISTORY_PAGE_SIZE, target.profile)
|
||||||
|
if (!page || page.messages.length === 0) {
|
||||||
|
target.hasMoreBefore = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingIds = new Set(target.messages.map(message => message.id))
|
||||||
|
const olderMessages = mapHistoryMessages(page.messages).filter(message => !existingIds.has(message.id))
|
||||||
|
target.messages = [...olderMessages, ...target.messages]
|
||||||
|
target.loadedMessageCount = offset + page.messages.length
|
||||||
|
target.messageTotal = page.total
|
||||||
|
target.messageCount = page.total
|
||||||
|
target.hasMoreBefore = page.hasMore
|
||||||
|
return olderMessages.length > 0
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load older history messages:', err)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
target.isLoadingOlderMessages = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSessionClick(sessionId: string, profile?: string | null) {
|
async function handleSessionClick(sessionId: string, profile?: string | null) {
|
||||||
await router.push({
|
await router.push({
|
||||||
name: 'hermes.historySession',
|
name: 'hermes.historySession',
|
||||||
@@ -764,7 +836,11 @@ function handleBatchDeleteConfirm() {
|
|||||||
|
|
||||||
<div class="history-content-wrapper">
|
<div class="history-content-wrapper">
|
||||||
<div class="history-main-content">
|
<div class="history-main-content">
|
||||||
<HistoryMessageList ref="historyMessageListRef" :session="historySession" />
|
<HistoryMessageList
|
||||||
|
ref="historyMessageListRef"
|
||||||
|
:session="historySession"
|
||||||
|
:load-older="loadOlderHistoryMessages"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<OutlinePanel
|
<OutlinePanel
|
||||||
v-if="showOutline && historySession"
|
v-if="showOutline && historySession"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as hermesCli from '../../services/hermes/hermes-cli'
|
import * as hermesCli from '../../services/hermes/hermes-cli'
|
||||||
import { listSessionSummaries, getUsageStatsFromDb, getSessionDetailFromDb, getSessionDetailFromDbWithProfile, getExactSessionDetailFromDbWithProfile } from '../../db/hermes/sessions-db'
|
import { listSessionSummaries, getUsageStatsFromDb, getSessionDetailFromDb, getSessionDetailFromDbWithProfile, getSessionDetailPaginatedFromDbWithProfile, getExactSessionDetailFromDbWithProfile } from '../../db/hermes/sessions-db'
|
||||||
import {
|
import {
|
||||||
listSessions as localListSessions,
|
listSessions as localListSessions,
|
||||||
searchSessions as localSearchSessions,
|
searchSessions as localSearchSessions,
|
||||||
@@ -872,29 +872,35 @@ function serializeAsText(title: string | null, messages: any[]): string {
|
|||||||
export async function getConversationMessagesPaginated(ctx: any) {
|
export async function getConversationMessagesPaginated(ctx: any) {
|
||||||
const offset = ctx.query.offset ? parseInt(ctx.query.offset as string, 10) : 0
|
const offset = ctx.query.offset ? parseInt(ctx.query.offset as string, 10) : 0
|
||||||
const limit = ctx.query.limit ? parseInt(ctx.query.limit as string, 10) : 50
|
const limit = ctx.query.limit ? parseInt(ctx.query.limit as string, 10) : 50
|
||||||
|
const profile = requestedProfile(ctx)
|
||||||
|
|
||||||
const { getSessionDetailPaginated } = await import('../../db/hermes/session-store')
|
const { getSessionDetailPaginated } = await import('../../db/hermes/session-store')
|
||||||
const result = getSessionDetailPaginated(ctx.params.id, offset, limit)
|
const localResult = getSessionDetailPaginated(ctx.params.id, offset, limit)
|
||||||
|
const result = localResult && (!profile || localResult.session.profile === profile)
|
||||||
|
? localResult
|
||||||
|
: await getSessionDetailPaginatedFromDbWithProfile(ctx.params.id, profile || 'default', offset, limit)
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
ctx.status = 404
|
ctx.status = 404
|
||||||
ctx.body = { error: 'Conversation not found' }
|
ctx.body = { error: 'Conversation not found' }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (denySessionAccess(ctx, result.session)) return
|
const session = { ...result.session, profile: (result.session as any).profile || profile || 'default' }
|
||||||
|
if (denySessionAccess(ctx, session)) return
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
session: {
|
session: {
|
||||||
id: result.session.id,
|
id: session.id,
|
||||||
source: result.session.source,
|
profile: session.profile,
|
||||||
model: result.session.model,
|
source: session.source,
|
||||||
title: result.session.title,
|
model: session.model,
|
||||||
started_at: result.session.started_at,
|
title: session.title,
|
||||||
ended_at: result.session.ended_at,
|
started_at: session.started_at,
|
||||||
last_active: result.session.last_active,
|
ended_at: session.ended_at,
|
||||||
message_count: result.session.message_count,
|
last_active: session.last_active,
|
||||||
input_tokens: result.session.input_tokens,
|
message_count: session.message_count,
|
||||||
output_tokens: result.session.output_tokens,
|
input_tokens: session.input_tokens,
|
||||||
|
output_tokens: session.output_tokens,
|
||||||
},
|
},
|
||||||
messages: result.messages,
|
messages: result.messages,
|
||||||
total: result.total,
|
total: result.total,
|
||||||
|
|||||||
@@ -62,6 +62,15 @@ export interface HermesSessionDetailRow extends HermesSessionRow {
|
|||||||
thread_session_count: number
|
thread_session_count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaginatedHermesSessionDetailResult {
|
||||||
|
session: HermesSessionDetailRow
|
||||||
|
messages: HermesMessageRow[]
|
||||||
|
total: number
|
||||||
|
offset: number
|
||||||
|
limit: number
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface HermesSessionInternalRow extends HermesSessionRow {
|
interface HermesSessionInternalRow extends HermesSessionRow {
|
||||||
parent_session_id: string | null
|
parent_session_id: string | null
|
||||||
}
|
}
|
||||||
@@ -669,6 +678,52 @@ export async function getSessionDetailFromDbWithProfile(sessionId: string, profi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSessionDetailPaginatedFromDbWithProfile(
|
||||||
|
sessionId: string,
|
||||||
|
profile: string,
|
||||||
|
offset = 0,
|
||||||
|
limit = 300,
|
||||||
|
): Promise<PaginatedHermesSessionDetailResult | null> {
|
||||||
|
const db = await openSessionDb(profile)
|
||||||
|
try {
|
||||||
|
const idx = loadAllSessions(db)
|
||||||
|
const requested = idx.byId.get(sessionId) || null
|
||||||
|
if (!requested) return null
|
||||||
|
|
||||||
|
const chain = collectSessionChainForMatchedSession(requested, idx)
|
||||||
|
if (!chain.length) return null
|
||||||
|
|
||||||
|
const ids = chain.map(session => session.id)
|
||||||
|
const placeholders = ids.map(() => '?').join(', ')
|
||||||
|
const orderSql = chainOrderSql(ids)
|
||||||
|
const totalRow = db.prepare(`
|
||||||
|
SELECT COUNT(*) AS total
|
||||||
|
FROM messages
|
||||||
|
WHERE session_id IN (${placeholders})
|
||||||
|
`).get(...ids) as { total: number } | undefined
|
||||||
|
const total = Number(totalRow?.total || 0)
|
||||||
|
|
||||||
|
const messageRows = db.prepare(`
|
||||||
|
SELECT * FROM messages
|
||||||
|
WHERE session_id IN (${placeholders})
|
||||||
|
ORDER BY CASE session_id ${orderSql} ELSE ${ids.length} END DESC, id DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
`).all(...ids, ...ids, limit, offset) as Record<string, unknown>[]
|
||||||
|
const messages = messageRows.map(mapMessageRow).reverse()
|
||||||
|
|
||||||
|
return {
|
||||||
|
session: aggregateSessionDetail(chain, messages, sessionId),
|
||||||
|
messages,
|
||||||
|
total,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
hasMore: offset + messages.length < total,
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getExactSessionDetailFromDbWithProfile(sessionId: string, profile: string): Promise<HermesSessionDetailRow | null> {
|
export async function getExactSessionDetailFromDbWithProfile(sessionId: string, profile: string): Promise<HermesSessionDetailRow | null> {
|
||||||
const { DatabaseSync } = await import('node:sqlite')
|
const { DatabaseSync } = await import('node:sqlite')
|
||||||
const dbPath = join(getProfileDir(profile), 'state.db')
|
const dbPath = join(getProfileDir(profile), 'state.db')
|
||||||
|
|||||||
Reference in New Issue
Block a user