fix profile-aware session history actions (#1011)

This commit is contained in:
ekko
2026-05-25 12:32:42 +08:00
committed by GitHub
parent bbb8b1d536
commit 56c6cf3e2d
7 changed files with 211 additions and 58 deletions
+1
View File
@@ -75,6 +75,7 @@ function shouldAttachProfileHeader(path: string, options: RequestInit): boolean
function isProfileWideSessionCollection(pathname: string): boolean {
return pathname === '/api/hermes/sessions' ||
pathname === '/api/hermes/sessions/batch-delete' ||
pathname === '/api/hermes/search/sessions' ||
pathname === '/api/hermes/sessions/search' ||
pathname === '/api/hermes/sessions/conversations'
+15 -2
View File
@@ -122,13 +122,26 @@ export async function deleteSession(id: string, profile?: string | null): Promis
}
}
export async function batchDeleteSessions(ids: string[]): Promise<{ deleted: number; failed: number; errors: Array<{ id: string; error: string }> }> {
export interface BatchDeleteSessionTarget {
id: string
profile?: string | null
}
export async function batchDeleteSessions(targets: Array<string | BatchDeleteSessionTarget>): Promise<{ deleted: number; failed: number; errors: Array<{ id: string; error: string }> }> {
try {
const sessions = targets.map(target =>
typeof target === 'string'
? { id: target }
: { id: target.id, profile: target.profile || undefined },
)
const res = await request<{ deleted: number; failed: number; errors: Array<{ id: string; error: string }> }>(
'/api/hermes/sessions/batch-delete',
{
method: 'POST',
body: JSON.stringify({ ids }),
body: JSON.stringify({
ids: sessions.map(session => session.id),
sessions,
}),
}
)
return res
@@ -43,7 +43,7 @@ const currentMode = ref<"chat" | "live">("chat");
// Batch selection mode
const isBatchMode = ref(false);
const selectedSessionIds = ref<Set<string>>(new Set());
const selectedSessionKeys = ref<Set<string>>(new Set());
const showBatchDeleteConfirm = ref(false);
const isBatchDeleting = ref(false);
@@ -328,39 +328,49 @@ function toggleBatchMode() {
if (isBatchDeleting.value) return;
isBatchMode.value = !isBatchMode.value;
if (!isBatchMode.value) {
selectedSessionIds.value.clear();
selectedSessionKeys.value.clear();
showBatchDeleteConfirm.value = false;
}
}
function toggleSessionSelection(id: string) {
function sessionSelectionKey(session: Pick<Session, "id" | "profile">): string {
return `${session.profile || "default"}\u0000${session.id}`;
}
function toggleSessionSelection(session: Session) {
if (isBatchDeleting.value) return;
if (selectedSessionIds.value.has(id)) {
selectedSessionIds.value.delete(id);
const key = sessionSelectionKey(session);
if (selectedSessionKeys.value.has(key)) {
selectedSessionKeys.value.delete(key);
} else {
selectedSessionIds.value.add(id);
selectedSessionKeys.value.add(key);
}
selectedSessionIds.value = new Set(selectedSessionIds.value);
if (selectedSessionIds.value.size === 0) {
selectedSessionKeys.value = new Set(selectedSessionKeys.value);
if (selectedSessionKeys.value.size === 0) {
showBatchDeleteConfirm.value = false;
}
}
function isSessionSelected(id: string): boolean {
return selectedSessionIds.value.has(id);
function isSessionSelected(session: Session): boolean {
return selectedSessionKeys.value.has(sessionSelectionKey(session));
}
async function handleBatchDelete() {
if (selectedSessionIds.value.size === 0 || isBatchDeleting.value) return;
if (selectedSessionKeys.value.size === 0 || isBatchDeleting.value) return;
const ids = Array.from(selectedSessionIds.value);
const sessionsByKey = new Map(chatStore.sessions.map((session) => [sessionSelectionKey(session), session]));
const targets = Array.from(selectedSessionKeys.value)
.map((key) => sessionsByKey.get(key))
.filter((session): session is Session => Boolean(session))
.map((session) => ({ id: session.id, profile: session.profile || null }));
if (targets.length === 0) return;
isBatchDeleting.value = true;
try {
const result = await batchDeleteSessions(ids);
const result = await batchDeleteSessions(targets);
if (result.deleted > 0) {
// Remove from pinned sessions
for (const id of ids) {
sessionBrowserPrefsStore.removePinned(id);
for (const target of targets) {
sessionBrowserPrefsStore.removePinned(target.id);
}
// Remove deleted sessions from local store (without calling API again)
@@ -380,7 +390,7 @@ async function handleBatchDelete() {
isBatchDeleting.value = false;
showBatchDeleteConfirm.value = false;
isBatchMode.value = false;
selectedSessionIds.value.clear();
selectedSessionKeys.value.clear();
}
}
@@ -391,16 +401,16 @@ function handleBatchDeleteConfirm() {
function selectAllSessions() {
if (isBatchDeleting.value) return;
selectedSessionIds.value.clear();
selectedSessionKeys.value.clear();
for (const session of chatStore.sessions) {
if (session.id !== chatStore.activeSessionId) {
selectedSessionIds.value.add(session.id);
selectedSessionKeys.value.add(sessionSelectionKey(session));
}
}
selectedSessionIds.value = new Set(selectedSessionIds.value);
selectedSessionKeys.value = new Set(selectedSessionKeys.value);
}
const selectedCount = computed(() => selectedSessionIds.value.size);
const selectedCount = computed(() => selectedSessionKeys.value.size);
const canSelectAll = computed(() => {
return chatStore.sessions.some(s => s.id !== chatStore.activeSessionId);
});
@@ -856,13 +866,13 @@ async function handleSessionModelCustomSubmit() {
"
:streaming="chatStore.isSessionLive(s.id)"
:selectable="isBatchMode"
:selected="isSessionSelected(s.id)"
:selected="isSessionSelected(s)"
:show-profile="true"
:to="sessionHref(s.id)"
@select="handleSessionClick(s.id)"
@contextmenu="handleContextMenu($event, s.id)"
@delete="handleDeleteSession(s.id)"
@toggle-select="toggleSessionSelection(s.id)"
@toggle-select="toggleSessionSelection(s)"
/>
</template>
@@ -878,13 +888,13 @@ async function handleSessionModelCustomSubmit() {
"
:streaming="chatStore.isSessionLive(s.id)"
:selectable="isBatchMode"
:selected="isSessionSelected(s.id)"
:selected="isSessionSelected(s)"
:show-profile="true"
:to="sessionHref(s.id)"
@select="handleSessionClick(s.id)"
@contextmenu="handleContextMenu($event, s.id)"
@delete="handleDeleteSession(s.id)"
@toggle-select="toggleSessionSelection(s.id)"
@toggle-select="toggleSessionSelection(s)"
/>
</div>
</aside>
@@ -32,6 +32,8 @@ const routeProfile = computed(() => {
return typeof value === 'string' && value.trim() ? value : null
})
const effectiveHistoryProfile = computed(() => profilesStore.activeProfileName || routeProfile.value || null)
// Hermes history sessions (exclude api_server)
const hermesSessions = ref<SessionSummary[]>([])
const hermesSessionsLoading = ref(false)
@@ -40,17 +42,22 @@ const hermesSessionsLoaded = ref(false)
const historySessionId = ref<string | null>(null)
const historySession = ref<Session | null>(null)
const showOutline = ref(false)
let hermesSessionsRequestId = 0
async function loadHermesSessions() {
if (hermesSessionsLoading.value) return
const requestId = ++hermesSessionsRequestId
hermesSessionsLoading.value = true
try {
hermesSessions.value = await fetchHermesSessions(undefined, undefined, routeProfile.value)
const sessions = await fetchHermesSessions(undefined, undefined, effectiveHistoryProfile.value)
if (requestId !== hermesSessionsRequestId) return
hermesSessions.value = sessions
hermesSessionsLoaded.value = true
} catch (err) {
console.error('Failed to load Hermes sessions:', err)
} finally {
hermesSessionsLoading.value = false
if (requestId === hermesSessionsRequestId) {
hermesSessionsLoading.value = false
}
}
}
@@ -132,6 +139,29 @@ async function handleSessionClick(sessionId: string, profile?: string | null) {
})
}
async function openDefaultHistorySession(replace = false) {
const firstCliSession = hermesSessions.value.find(s => s.source === 'cli')
const firstSession = firstCliSession || hermesSessions.value[0]
if (!firstSession) {
historySessionId.value = null
historySession.value = null
if (routeSessionId.value) await router.replace({ name: 'hermes.history' })
return
}
if (collapsedGroups.value.has(firstSession.source)) {
collapsedGroups.value = new Set([...collapsedGroups.value].filter(source => source !== firstSession.source))
}
const location = {
name: 'hermes.historySession',
params: { sessionId: firstSession.id },
query: firstSession.profile ? { profile: firstSession.profile } : undefined,
}
if (replace) await router.replace(location)
else await router.push(location)
}
async function syncRouteSession() {
const sessionId = routeSessionId.value
if (!sessionId) return
@@ -143,8 +173,10 @@ async function syncRouteSession() {
return
}
if (historySessionId.value !== sessionId) {
await loadHistorySession(sessionId, routeProfile.value)
const sessionProfile = routeProfile.value || findHistorySession(sessionId)?.profile || null
const currentProfile = historySession.value?.profile || null
if (historySessionId.value !== sessionId || currentProfile !== sessionProfile) {
await loadHistorySession(sessionId, sessionProfile)
}
}
@@ -174,7 +206,6 @@ watch([routeSessionId, routeProfile], async ([sessionId]) => {
if (!sessionId) {
historySessionId.value = null
historySession.value = null
if (hermesSessionsLoaded.value) await loadHermesSessions()
return
}
if (!hermesSessionsLoaded.value) return
@@ -184,6 +215,15 @@ watch([routeSessionId, routeProfile], async ([sessionId]) => {
await syncRouteSession()
})
watch(() => profilesStore.activeProfileName, async () => {
if (!hermesSessionsLoaded.value) return
if (profilesStore.switching) return
historySessionId.value = null
historySession.value = null
await loadHermesSessions()
await openDefaultHistorySession(true)
})
const collapsedGroups = ref<Set<string>>(new Set(JSON.parse(localStorage.getItem('hermes_collapsed_groups') || '[]')))
// Convert SessionSummary to Session format
@@ -292,23 +332,10 @@ watch(groupedSessions, groups => {
}
}, { once: true })
// Auto-load first CLI session when Hermes sessions are loaded
// Auto-load the first CLI session when Hermes sessions are loaded.
watch(hermesSessionsLoaded, (loaded) => {
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.
const firstCliSession = hermesSessions.value.find(s => s.source === 'cli')
if (firstCliSession) {
// Ensure the CLI group is expanded
if (collapsedGroups.value.has(firstCliSession.source)) {
collapsedGroups.value = new Set([...collapsedGroups.value].filter(s => s !== firstCliSession.source))
}
// Load session details
void handleSessionClick(firstCliSession.id, firstCliSession.profile)
}
// If no CLI session exists, don't auto-load any session
}
void openDefaultHistorySession(false)
}
}, { once: true })