fix profile-aware session history actions (#1011)
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user