[codex] Fix profile-aware session deep links (#962)

* feat: add session deep links for chats

* feat: add deep links for history and group chat

* Fix profile-aware session deep links

---------

Co-authored-by: Maxim Kirilyuk <werserk@inbox.ru>
This commit is contained in:
ekko
2026-05-24 10:55:55 +08:00
committed by GitHub
parent 8d261c3fa6
commit df41d6b051
22 changed files with 871 additions and 63 deletions
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useChatStore, type Session } from '@/stores/hermes/chat'
import { useRoute, useRouter } from 'vue-router'
import { type Session } from '@/stores/hermes/chat'
import { useAppStore } from '@/stores/hermes/app'
import { useProfilesStore } from '@/stores/hermes/profiles'
import { useSessionBrowserPrefsStore } from '@/stores/hermes/session-browser-prefs'
@@ -13,12 +14,23 @@ import SessionListItem from '@/components/hermes/chat/SessionListItem.vue'
import OutlinePanel from '@/components/hermes/chat/OutlinePanel.vue'
import { deleteSession, fetchHermesSessions, fetchHermesSession, type SessionSummary } from '@/api/hermes/sessions'
const chatStore = useChatStore()
const appStore = useAppStore()
const profilesStore = useProfilesStore()
const sessionBrowserPrefsStore = useSessionBrowserPrefsStore()
const message = useMessage()
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const routeSessionId = computed(() => {
const value = route.params.sessionId
return typeof value === 'string' && value.trim() ? value : null
})
const routeProfile = computed(() => {
const value = route.query.profile
return typeof value === 'string' && value.trim() ? value : null
})
// Hermes history sessions (exclude api_server)
const hermesSessions = ref<SessionSummary[]>([])
@@ -33,7 +45,7 @@ async function loadHermesSessions() {
if (hermesSessionsLoading.value) return
hermesSessionsLoading.value = true
try {
hermesSessions.value = await fetchHermesSessions()
hermesSessions.value = await fetchHermesSessions(undefined, undefined, routeProfile.value)
hermesSessionsLoaded.value = true
} catch (err) {
console.error('Failed to load Hermes sessions:', err)
@@ -53,7 +65,7 @@ function findHistorySession(sessionId: string): SessionSummary | undefined {
return hermesSessions.value.find(session => session.id === sessionId)
}
async function handleSessionClick(sessionId: string, profile?: string | null) {
async function loadHistorySession(sessionId: string, profile?: string | null) {
const summary = findHistorySession(sessionId)
const sessionProfile = profile || summary?.profile || null
// First, fetch the Hermes session detail
@@ -112,6 +124,30 @@ async function handleSessionClick(sessionId: string, profile?: string | null) {
if (mobileQuery?.matches) showSessions.value = false
}
async function handleSessionClick(sessionId: string, profile?: string | null) {
await router.push({
name: 'hermes.historySession',
params: { sessionId },
query: profile ? { profile } : undefined,
})
}
async function syncRouteSession() {
const sessionId = routeSessionId.value
if (!sessionId) return
if (!hermesSessions.value.some(s => s.id === sessionId)) {
historySessionId.value = null
historySession.value = null
await router.replace({ name: 'hermes.history' })
return
}
if (historySessionId.value !== sessionId) {
await loadHistorySession(sessionId, routeProfile.value)
}
}
function handleMobileChange(e: MediaQueryListEvent | MediaQueryList) {
isMobile.value = e.matches
if (e.matches && showSessions.value) {
@@ -123,6 +159,7 @@ onMounted(async () => {
appStore.loadModels()
await profilesStore.fetchProfiles()
await loadHermesSessions()
await syncRouteSession()
mobileQuery = window.matchMedia('(max-width: 768px)')
handleMobileChange(mobileQuery)
@@ -133,6 +170,20 @@ onUnmounted(() => {
mobileQuery?.removeEventListener('change', handleMobileChange)
})
watch([routeSessionId, routeProfile], async ([sessionId]) => {
if (!sessionId) {
historySessionId.value = null
historySession.value = null
if (hermesSessionsLoaded.value) await loadHermesSessions()
return
}
if (!hermesSessionsLoaded.value) return
if (routeProfile.value && !hermesSessions.value.some(s => s.profile === routeProfile.value)) {
await loadHermesSessions()
}
await syncRouteSession()
})
const collapsedGroups = ref<Set<string>>(new Set(JSON.parse(localStorage.getItem('hermes_collapsed_groups') || '[]')))
// Convert SessionSummary to Session format
@@ -219,7 +270,7 @@ function toggleGroup(source: string) {
const group = groupedSessions.value.find(g => g.source === source)
if (group?.sessions.length) {
// Auto-select and load first session when expanding group
handleSessionClick(group.sessions[0].id, group.sessions[0].profile)
void handleSessionClick(group.sessions[0].id, group.sessions[0].profile)
}
}
localStorage.setItem('hermes_collapsed_groups', JSON.stringify([...collapsedGroups.value]))
@@ -227,7 +278,7 @@ function toggleGroup(source: string) {
watch(groupedSessions, groups => {
if (localStorage.getItem('hermes_collapsed_groups') !== null) {
const activeSource = chatStore.activeSession?.source
const activeSource = historySession.value?.source
if (activeSource && collapsedGroups.value.has(activeSource)) {
collapsedGroups.value = new Set([...collapsedGroups.value].filter(source => source !== activeSource))
localStorage.setItem('hermes_collapsed_groups', JSON.stringify([...collapsedGroups.value]))
@@ -243,7 +294,7 @@ watch(groupedSessions, groups => {
// Auto-load first CLI session when Hermes sessions are loaded
watch(hermesSessionsLoaded, (loaded) => {
if (loaded && hermesSessions.value.length > 0) {
if (loaded && hermesSessions.value.length > 0 && !routeSessionId.value) {
// Only auto-load if no session is currently active
if (!historySessionId.value || !hermesSessions.value.find(s => s.id === historySessionId.value)) {
// Find first CLI session.
@@ -254,7 +305,7 @@ watch(hermesSessionsLoaded, (loaded) => {
collapsedGroups.value = new Set([...collapsedGroups.value].filter(s => s !== firstCliSession.source))
}
// Load session details
handleSessionClick(firstCliSession.id, firstCliSession.profile)
void handleSessionClick(firstCliSession.id, firstCliSession.profile)
}
// If no CLI session exists, don't auto-load any session
}
@@ -278,6 +329,30 @@ async function copySessionId(id?: string) {
}
}
function historySessionProfile(sessionId: string): string | null {
return historySession.value?.id === sessionId
? historySession.value.profile || null
: findHistorySession(sessionId)?.profile || null
}
function buildHistorySessionUrl(sessionId: string, profile?: string | null) {
const href = router.resolve({
name: 'hermes.historySession',
params: { sessionId },
query: profile ? { profile } : undefined,
}).href
return `${window.location.origin}${window.location.pathname}${href}`
}
async function copySessionLink(id?: string) {
const sessionId = id || historySessionId.value
if (sessionId) {
const ok = await copyToClipboard(buildHistorySessionUrl(sessionId, historySessionProfile(sessionId)))
if (ok) message.success(t('common.copied'))
else message.error(t('common.copied') + ' ✗')
}
}
async function handleDeleteSession(id: string, profile?: string | null) {
const summary = findHistorySession(id)
const sessionProfile = profile || summary?.profile || null
@@ -295,6 +370,7 @@ async function handleDeleteSession(id: string, profile?: string | null) {
historySession.value = null
const next = historySessions.value[0]
if (next) await handleSessionClick(next.id, next.profile)
else await router.replace({ name: 'hermes.history' })
}
message.success(t('chat.sessionDeleted'))
@@ -387,6 +463,16 @@ async function handleDeleteSession(id: string, profile?: string | null) {
</template>
{{ t('chat.outlineTitle') }}
</NTooltip>
<NTooltip trigger="hover">
<template #trigger>
<NButton quaternary size="small" @click="copySessionLink()" circle>
<template #icon>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
</template>
</NButton>
</template>
{{ t('chat.copySessionLink') }}
</NTooltip>
<NTooltip trigger="hover">
<template #trigger>
<NButton quaternary size="small" @click="copySessionId()" circle>