759 lines
23 KiB
TypeScript
759 lines
23 KiB
TypeScript
import { batch, createSignal } from "solid-js"
|
|
|
|
import type { Session, SessionStatus, Agent, Provider } from "../types/session"
|
|
import { deleteSession, loadMessages } from "./session-api"
|
|
import { showToastNotification } from "../lib/notifications"
|
|
import { messageStoreBus } from "./message-v2/bus"
|
|
import { instances } from "./instances"
|
|
import { showConfirmDialog } from "./alerts"
|
|
import { getLogger } from "../lib/logger"
|
|
import { requestData } from "../lib/opencode-api"
|
|
import { getOrCreateWorktreeClient, getWorktreeSlugForSession } from "./worktrees"
|
|
import { tGlobal } from "../lib/i18n"
|
|
|
|
const log = getLogger("session")
|
|
|
|
export interface SessionInfo {
|
|
cost: number
|
|
contextWindow: number
|
|
isSubscriptionModel: boolean
|
|
inputTokens: number
|
|
outputTokens: number
|
|
reasoningTokens: number
|
|
actualUsageTokens: number
|
|
modelOutputLimit: number
|
|
contextAvailableTokens: number | null
|
|
}
|
|
|
|
export type SessionThread = {
|
|
parent: Session
|
|
children: Session[]
|
|
latestUpdated: number
|
|
}
|
|
|
|
const [sessions, setSessions] = createSignal<Map<string, Map<string, Session>>>(new Map())
|
|
const [activeSessionId, setActiveSessionId] = createSignal<Map<string, string>>(new Map())
|
|
const [activeParentSessionId, setActiveParentSessionId] = createSignal<Map<string, string>>(new Map())
|
|
const [agents, setAgents] = createSignal<Map<string, Agent[]>>(new Map())
|
|
const [providers, setProviders] = createSignal<Map<string, Provider[]>>(new Map())
|
|
const [sessionDraftPrompts, setSessionDraftPrompts] = createSignal<Map<string, string>>(new Map())
|
|
|
|
const [loading, setLoading] = createSignal({
|
|
fetchingSessions: new Map<string, boolean>(),
|
|
creatingSession: new Map<string, boolean>(),
|
|
deletingSession: new Map<string, Set<string>>(),
|
|
loadingMessages: new Map<string, Set<string>>(),
|
|
})
|
|
|
|
const [messagesLoaded, setMessagesLoaded] = createSignal<Map<string, Set<string>>>(new Map())
|
|
const [sessionInfoByInstance, setSessionInfoByInstance] = createSignal<Map<string, Map<string, SessionInfo>>>(new Map())
|
|
|
|
const [expandedSessionParents, setExpandedSessionParents] = createSignal<Map<string, Set<string>>>(new Map())
|
|
|
|
export type InstanceSessionIndicatorStatus = "permission" | SessionStatus
|
|
|
|
type InstanceIndicatorCounts = {
|
|
permission: number
|
|
working: number
|
|
compacting: number
|
|
}
|
|
|
|
const [instanceIndicatorCounts, setInstanceIndicatorCounts] = createSignal<Map<string, InstanceIndicatorCounts>>(new Map())
|
|
|
|
function getIndicatorBucket(session: Pick<Session, "status" | "pendingPermission" | "pendingQuestion">): InstanceSessionIndicatorStatus | "idle" {
|
|
if (session.pendingPermission || session.pendingQuestion) {
|
|
return "permission"
|
|
}
|
|
const status = session.status ?? "idle"
|
|
return status
|
|
}
|
|
|
|
function adjustIndicatorCounts(
|
|
instanceId: string,
|
|
previous: InstanceSessionIndicatorStatus | "idle",
|
|
next: InstanceSessionIndicatorStatus | "idle",
|
|
): void {
|
|
if (previous === next) return
|
|
|
|
const decKey = previous === "idle" ? null : previous
|
|
const incKey = next === "idle" ? null : next
|
|
|
|
setInstanceIndicatorCounts((prev) => {
|
|
const current = prev.get(instanceId) ?? { permission: 0, working: 0, compacting: 0 }
|
|
const updated: InstanceIndicatorCounts = { ...current }
|
|
|
|
if (decKey) {
|
|
updated[decKey] = Math.max(0, updated[decKey] - 1)
|
|
}
|
|
|
|
if (incKey) {
|
|
updated[incKey] = updated[incKey] + 1
|
|
}
|
|
|
|
const hasAny = updated.permission > 0 || updated.working > 0 || updated.compacting > 0
|
|
if (!hasAny) {
|
|
if (!prev.has(instanceId)) return prev
|
|
const nextMap = new Map(prev)
|
|
nextMap.delete(instanceId)
|
|
return nextMap
|
|
}
|
|
|
|
const same =
|
|
current.permission === updated.permission &&
|
|
current.working === updated.working &&
|
|
current.compacting === updated.compacting
|
|
if (same && prev.has(instanceId)) {
|
|
return prev
|
|
}
|
|
|
|
const nextMap = new Map(prev)
|
|
nextMap.set(instanceId, updated)
|
|
return nextMap
|
|
})
|
|
}
|
|
|
|
function recomputeIndicatorCounts(instanceId: string, instanceSessions: Map<string, Session> | undefined): void {
|
|
if (!instanceSessions || instanceSessions.size === 0) {
|
|
setInstanceIndicatorCounts((prev) => {
|
|
if (!prev.has(instanceId)) return prev
|
|
const next = new Map(prev)
|
|
next.delete(instanceId)
|
|
return next
|
|
})
|
|
return
|
|
}
|
|
|
|
let permission = 0
|
|
let working = 0
|
|
let compacting = 0
|
|
|
|
for (const session of instanceSessions.values()) {
|
|
if (session.pendingPermission || session.pendingQuestion) {
|
|
permission += 1
|
|
continue
|
|
}
|
|
const status = session.status ?? "idle"
|
|
if (status === "compacting") {
|
|
compacting += 1
|
|
} else if (status === "working") {
|
|
working += 1
|
|
}
|
|
}
|
|
|
|
if (permission === 0 && working === 0 && compacting === 0) {
|
|
setInstanceIndicatorCounts((prev) => {
|
|
if (!prev.has(instanceId)) return prev
|
|
const next = new Map(prev)
|
|
next.delete(instanceId)
|
|
return next
|
|
})
|
|
return
|
|
}
|
|
|
|
setInstanceIndicatorCounts((prev) => {
|
|
const current = prev.get(instanceId)
|
|
if (current && current.permission === permission && current.working === working && current.compacting === compacting) {
|
|
return prev
|
|
}
|
|
const next = new Map(prev)
|
|
next.set(instanceId, { permission, working, compacting })
|
|
return next
|
|
})
|
|
}
|
|
|
|
export function getInstanceSessionIndicatorStatusCached(instanceId: string): InstanceSessionIndicatorStatus {
|
|
const counts = instanceIndicatorCounts().get(instanceId)
|
|
if (!counts) return "idle"
|
|
if (counts.permission > 0) return "permission"
|
|
if (counts.compacting > 0) return "compacting"
|
|
if (counts.working > 0) return "working"
|
|
return "idle"
|
|
}
|
|
|
|
export function syncInstanceSessionIndicator(instanceId: string, instanceSessions?: Map<string, Session>): void {
|
|
recomputeIndicatorCounts(instanceId, instanceSessions ?? sessions().get(instanceId))
|
|
}
|
|
|
|
function clearLoadedFlag(instanceId: string, sessionId: string) {
|
|
if (!instanceId || !sessionId) return
|
|
setMessagesLoaded((prev) => {
|
|
const existing = prev.get(instanceId)
|
|
if (!existing || !existing.has(sessionId)) {
|
|
return prev
|
|
}
|
|
const next = new Map(prev)
|
|
const updated = new Set(existing)
|
|
updated.delete(sessionId)
|
|
if (updated.size === 0) {
|
|
next.delete(instanceId)
|
|
} else {
|
|
next.set(instanceId, updated)
|
|
}
|
|
return next
|
|
})
|
|
}
|
|
|
|
messageStoreBus.onSessionCleared((instanceId, sessionId) => {
|
|
clearLoadedFlag(instanceId, sessionId)
|
|
})
|
|
|
|
function getDraftKey(instanceId: string, sessionId: string): string {
|
|
|
|
return `${instanceId}:${sessionId}`
|
|
}
|
|
|
|
function getSessionDraftPrompt(instanceId: string, sessionId: string): string {
|
|
if (!instanceId || !sessionId) return ""
|
|
const key = getDraftKey(instanceId, sessionId)
|
|
return sessionDraftPrompts().get(key) ?? ""
|
|
}
|
|
|
|
function setSessionDraftPrompt(instanceId: string, sessionId: string, value: string) {
|
|
const key = getDraftKey(instanceId, sessionId)
|
|
setSessionDraftPrompts((prev) => {
|
|
const next = new Map(prev)
|
|
if (!value) {
|
|
next.delete(key)
|
|
} else {
|
|
next.set(key, value)
|
|
}
|
|
return next
|
|
})
|
|
}
|
|
|
|
function clearSessionDraftPrompt(instanceId: string, sessionId: string) {
|
|
const key = getDraftKey(instanceId, sessionId)
|
|
setSessionDraftPrompts((prev) => {
|
|
if (!prev.has(key)) return prev
|
|
const next = new Map(prev)
|
|
next.delete(key)
|
|
return next
|
|
})
|
|
}
|
|
|
|
function clearInstanceDraftPrompts(instanceId: string) {
|
|
if (!instanceId) return
|
|
setSessionDraftPrompts((prev) => {
|
|
let changed = false
|
|
const next = new Map(prev)
|
|
const prefix = `${instanceId}:`
|
|
for (const key of Array.from(next.keys())) {
|
|
if (key.startsWith(prefix)) {
|
|
next.delete(key)
|
|
changed = true
|
|
}
|
|
}
|
|
return changed ? next : prev
|
|
})
|
|
}
|
|
|
|
function pruneDraftPrompts(instanceId: string, validSessionIds: Set<string>) {
|
|
setSessionDraftPrompts((prev) => {
|
|
let changed = false
|
|
const next = new Map(prev)
|
|
const prefix = `${instanceId}:`
|
|
for (const key of Array.from(next.keys())) {
|
|
if (key.startsWith(prefix)) {
|
|
const sessionId = key.slice(prefix.length)
|
|
if (!validSessionIds.has(sessionId)) {
|
|
next.delete(key)
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
return changed ? next : prev
|
|
})
|
|
}
|
|
|
|
function withSession(instanceId: string, sessionId: string, updater: (session: Session) => void | boolean) {
|
|
let previousBucket: InstanceSessionIndicatorStatus | "idle" | null = null
|
|
let nextBucket: InstanceSessionIndicatorStatus | "idle" | null = null
|
|
let didUpdate = false
|
|
|
|
setSessions((prev) => {
|
|
const instanceSessions = prev.get(instanceId)
|
|
if (!instanceSessions) return prev
|
|
|
|
const current = instanceSessions.get(sessionId)
|
|
if (!current) return prev
|
|
|
|
previousBucket = getIndicatorBucket(current)
|
|
|
|
const updatedSession: Session = { ...current }
|
|
const result = updater(updatedSession)
|
|
if (result === false) {
|
|
return prev
|
|
}
|
|
|
|
nextBucket = getIndicatorBucket(updatedSession)
|
|
|
|
instanceSessions.set(sessionId, updatedSession)
|
|
didUpdate = true
|
|
|
|
const next = new Map(prev)
|
|
next.set(instanceId, instanceSessions)
|
|
return next
|
|
})
|
|
|
|
if (didUpdate && previousBucket && nextBucket) {
|
|
adjustIndicatorCounts(instanceId, previousBucket, nextBucket)
|
|
}
|
|
}
|
|
|
|
function setSessionPendingPermission(instanceId: string, sessionId: string, pending: boolean): void {
|
|
withSession(instanceId, sessionId, (session) => {
|
|
if (session.pendingPermission === pending) return false
|
|
session.pendingPermission = pending
|
|
})
|
|
}
|
|
|
|
function setSessionPendingQuestion(instanceId: string, sessionId: string, pending: boolean): void {
|
|
withSession(instanceId, sessionId, (session) => {
|
|
if (session.pendingQuestion === pending) return false
|
|
session.pendingQuestion = pending
|
|
})
|
|
}
|
|
|
|
function setActiveSession(instanceId: string, sessionId: string): void {
|
|
setActiveSessionId((prev) => {
|
|
const next = new Map(prev)
|
|
next.set(instanceId, sessionId)
|
|
return next
|
|
})
|
|
}
|
|
|
|
function setActiveParentSession(instanceId: string, parentSessionId: string): void {
|
|
setActiveParentSessionId((prev) => {
|
|
const next = new Map(prev)
|
|
next.set(instanceId, parentSessionId)
|
|
return next
|
|
})
|
|
|
|
setActiveSession(instanceId, parentSessionId)
|
|
}
|
|
|
|
function clearActiveParentSession(instanceId: string): void {
|
|
setActiveParentSessionId((prev) => {
|
|
const next = new Map(prev)
|
|
next.delete(instanceId)
|
|
return next
|
|
})
|
|
|
|
setActiveSessionId((prev) => {
|
|
const next = new Map(prev)
|
|
next.delete(instanceId)
|
|
return next
|
|
})
|
|
}
|
|
|
|
function setSessionStatus(instanceId: string, sessionId: string, status: SessionStatus): void {
|
|
let parentToExpand: string | null = null
|
|
|
|
withSession(instanceId, sessionId, (session) => {
|
|
if (session.status === status) return false
|
|
const previous = session.status
|
|
session.status = status
|
|
|
|
// If a child session starts working, auto-expand its parent thread once.
|
|
// Users can still collapse it afterwards; we only expand on the transition.
|
|
if (session.parentId && status === "working" && previous !== "working") {
|
|
parentToExpand = session.parentId
|
|
}
|
|
})
|
|
|
|
if (parentToExpand) {
|
|
ensureSessionParentExpanded(instanceId, parentToExpand)
|
|
}
|
|
}
|
|
|
|
function getActiveParentSession(instanceId: string): Session | null {
|
|
const parentId = activeParentSessionId().get(instanceId)
|
|
if (!parentId) return null
|
|
|
|
const instanceSessions = sessions().get(instanceId)
|
|
return instanceSessions?.get(parentId) || null
|
|
}
|
|
|
|
function getActiveSession(instanceId: string): Session | null {
|
|
const sessionId = activeSessionId().get(instanceId)
|
|
if (!sessionId) return null
|
|
|
|
const instanceSessions = sessions().get(instanceId)
|
|
return instanceSessions?.get(sessionId) || null
|
|
}
|
|
|
|
function getSessions(instanceId: string): Session[] {
|
|
const instanceSessions = sessions().get(instanceId)
|
|
return instanceSessions ? Array.from(instanceSessions.values()) : []
|
|
}
|
|
|
|
function getParentSessions(instanceId: string): Session[] {
|
|
const allSessions = getSessions(instanceId)
|
|
return allSessions.filter((s) => s.parentId === null)
|
|
}
|
|
|
|
function getChildSessions(instanceId: string, parentId: string): Session[] {
|
|
const allSessions = getSessions(instanceId)
|
|
return allSessions.filter((s) => s.parentId === parentId)
|
|
}
|
|
|
|
function getSessionFamily(instanceId: string, parentId: string): Session[] {
|
|
const parent = sessions().get(instanceId)?.get(parentId)
|
|
if (!parent) return []
|
|
|
|
const children = getChildSessions(instanceId, parentId)
|
|
return [parent, ...children]
|
|
}
|
|
|
|
type SessionThreadCacheEntry = {
|
|
signature: string
|
|
thread: SessionThread
|
|
}
|
|
|
|
type SessionThreadCache = {
|
|
byParentId: Map<string, SessionThreadCacheEntry>
|
|
}
|
|
|
|
const sessionThreadCache = new Map<string, SessionThreadCache>()
|
|
|
|
function getOrCreateSessionThreadCache(instanceId: string): SessionThreadCache {
|
|
let cache = sessionThreadCache.get(instanceId)
|
|
if (!cache) {
|
|
cache = { byParentId: new Map() }
|
|
sessionThreadCache.set(instanceId, cache)
|
|
}
|
|
return cache
|
|
}
|
|
|
|
function getSessionThreads(instanceId: string): SessionThread[] {
|
|
const instanceSessions = sessions().get(instanceId)
|
|
if (!instanceSessions || instanceSessions.size === 0) {
|
|
sessionThreadCache.delete(instanceId)
|
|
return []
|
|
}
|
|
|
|
const cache = getOrCreateSessionThreadCache(instanceId)
|
|
const seenParents = new Set<string>()
|
|
|
|
const parents: Session[] = []
|
|
const childrenByParent = new Map<string, Session[]>()
|
|
|
|
for (const session of instanceSessions.values()) {
|
|
if (session.parentId === null) {
|
|
parents.push(session)
|
|
continue
|
|
}
|
|
|
|
const parentId = session.parentId
|
|
if (!parentId) continue
|
|
const children = childrenByParent.get(parentId)
|
|
if (children) {
|
|
children.push(session)
|
|
} else {
|
|
childrenByParent.set(parentId, [session])
|
|
}
|
|
}
|
|
|
|
const threads: SessionThread[] = []
|
|
|
|
for (const parent of parents) {
|
|
seenParents.add(parent.id)
|
|
|
|
const children = childrenByParent.get(parent.id) ?? []
|
|
if (children.length > 1) {
|
|
children.sort((a, b) => (b.time.updated ?? 0) - (a.time.updated ?? 0))
|
|
}
|
|
|
|
const parentUpdated = parent.time.updated ?? 0
|
|
const latestChild = children[0]?.time.updated ?? 0
|
|
const latestUpdated = Math.max(parentUpdated, latestChild)
|
|
|
|
const childIds = children.map((child) => child.id).join(",")
|
|
const signature = `${parentUpdated}:${latestChild}:${childIds}`
|
|
|
|
const cached = cache.byParentId.get(parent.id)
|
|
if (cached && cached.signature === signature) {
|
|
threads.push(cached.thread)
|
|
} else {
|
|
const thread: SessionThread = { parent, children, latestUpdated }
|
|
cache.byParentId.set(parent.id, { signature, thread })
|
|
threads.push(thread)
|
|
}
|
|
}
|
|
|
|
for (const parentId of Array.from(cache.byParentId.keys())) {
|
|
if (!seenParents.has(parentId)) {
|
|
cache.byParentId.delete(parentId)
|
|
}
|
|
}
|
|
|
|
threads.sort((a, b) => {
|
|
if (b.latestUpdated !== a.latestUpdated) return b.latestUpdated - a.latestUpdated
|
|
const bParentUpdated = b.parent.time.updated ?? 0
|
|
const aParentUpdated = a.parent.time.updated ?? 0
|
|
if (bParentUpdated !== aParentUpdated) return bParentUpdated - aParentUpdated
|
|
return b.parent.id.localeCompare(a.parent.id)
|
|
})
|
|
|
|
return threads
|
|
}
|
|
|
|
function isSessionParentExpanded(instanceId: string, parentSessionId: string): boolean {
|
|
return Boolean(expandedSessionParents().get(instanceId)?.has(parentSessionId))
|
|
}
|
|
|
|
function setSessionParentExpanded(instanceId: string, parentSessionId: string, expanded: boolean): void {
|
|
setExpandedSessionParents((prev) => {
|
|
const next = new Map(prev)
|
|
const currentSet = next.get(instanceId) ?? new Set<string>()
|
|
const updated = new Set(currentSet)
|
|
|
|
if (expanded) {
|
|
updated.add(parentSessionId)
|
|
} else {
|
|
updated.delete(parentSessionId)
|
|
}
|
|
|
|
if (updated.size === 0) {
|
|
next.delete(instanceId)
|
|
} else {
|
|
next.set(instanceId, updated)
|
|
}
|
|
|
|
return next
|
|
})
|
|
}
|
|
|
|
function toggleSessionParentExpanded(instanceId: string, parentSessionId: string): void {
|
|
setExpandedSessionParents((prev) => {
|
|
const next = new Map(prev)
|
|
const currentSet = next.get(instanceId) ?? new Set<string>()
|
|
const updated = new Set(currentSet)
|
|
|
|
if (updated.has(parentSessionId)) {
|
|
updated.delete(parentSessionId)
|
|
} else {
|
|
updated.add(parentSessionId)
|
|
}
|
|
|
|
next.set(instanceId, updated)
|
|
return next
|
|
})
|
|
}
|
|
|
|
function ensureSessionParentExpanded(instanceId: string, parentSessionId: string): void {
|
|
if (isSessionParentExpanded(instanceId, parentSessionId)) return
|
|
setSessionParentExpanded(instanceId, parentSessionId, true)
|
|
}
|
|
|
|
function getVisibleSessionIds(instanceId: string): string[] {
|
|
const threads = getSessionThreads(instanceId)
|
|
if (threads.length === 0) return []
|
|
|
|
const expanded = expandedSessionParents().get(instanceId)
|
|
const ids: string[] = []
|
|
|
|
for (const thread of threads) {
|
|
ids.push(thread.parent.id)
|
|
if (expanded?.has(thread.parent.id)) {
|
|
for (const child of thread.children) {
|
|
ids.push(child.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
return ids
|
|
}
|
|
|
|
function setActiveSessionFromList(instanceId: string, sessionId: string): void {
|
|
const session = sessions().get(instanceId)?.get(sessionId)
|
|
if (!session) return
|
|
|
|
if (session.parentId === null) {
|
|
setActiveParentSession(instanceId, sessionId)
|
|
return
|
|
}
|
|
|
|
const parentId = session.parentId
|
|
if (!parentId) return
|
|
|
|
batch(() => {
|
|
setActiveParentSession(instanceId, parentId)
|
|
setActiveSession(instanceId, sessionId)
|
|
})
|
|
}
|
|
|
|
function isSessionBusy(instanceId: string, sessionId: string): boolean {
|
|
const instanceSessions = sessions().get(instanceId)
|
|
if (!instanceSessions) return false
|
|
if (!instanceSessions.has(sessionId)) return false
|
|
return true
|
|
}
|
|
|
|
function isSessionMessagesLoading(instanceId: string, sessionId: string): boolean {
|
|
return Boolean(loading().loadingMessages.get(instanceId)?.has(sessionId))
|
|
}
|
|
|
|
function getSessionInfo(instanceId: string, sessionId: string): SessionInfo | undefined {
|
|
return sessionInfoByInstance().get(instanceId)?.get(sessionId)
|
|
}
|
|
|
|
async function isBlankSession(session: Session, instanceId: string, fetchIfNeeded = false): Promise<boolean> {
|
|
const created = session.time?.created || 0
|
|
const updated = session.time?.updated || 0
|
|
const hasChildren = getChildSessions(instanceId, session.id).length > 0
|
|
const isFreshSession = created === updated && !hasChildren
|
|
|
|
// Common short-circuit: fresh sessions without children
|
|
if (!fetchIfNeeded) {
|
|
return isFreshSession
|
|
}
|
|
|
|
// For a more thorough deep clean, we need to look at actual messages
|
|
|
|
const instance = instances().get(instanceId)
|
|
if (!instance?.client) {
|
|
return isFreshSession
|
|
}
|
|
let messages: any[] = []
|
|
try {
|
|
const worktreeSlug = getWorktreeSlugForSession(instanceId, session.id)
|
|
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
|
|
messages = await requestData<any[]>(
|
|
client.session.messages({ sessionID: session.id }),
|
|
"session.messages",
|
|
)
|
|
} catch (error) {
|
|
log.error(`Failed to fetch messages for session ${session.id}`, error)
|
|
return isFreshSession
|
|
}
|
|
|
|
// Specific logic by session type
|
|
if (session.parentId === null) {
|
|
// Parent: blank if no messages and no children (fresh !== blank sometimes!)
|
|
const hasChildren = getChildSessions(instanceId, session.id).length > 0
|
|
return messages.length === 0 && !hasChildren
|
|
} else if (session.title?.includes("subagent)")) {
|
|
// Subagent: "blank" (really: finished doing its job) if actually blank...
|
|
// ... OR no streaming, no pending perms, no tool parts
|
|
if (messages.length === 0) return true
|
|
|
|
const hasStreaming = messages.some((msg) => {
|
|
const info = msg.info.status || msg.status
|
|
return info === "streaming" || info === "sending"
|
|
})
|
|
|
|
const lastMessage = messages[messages.length - 1]
|
|
const lastParts = lastMessage?.parts || []
|
|
const hasToolPart = lastParts.some((part: any) =>
|
|
part.type === "tool" || part.data?.type === "tool"
|
|
)
|
|
|
|
return !hasStreaming && !session.pendingPermission && !hasToolPart
|
|
} else {
|
|
// Fork: blank if somehow has no messages or at revert point
|
|
if (messages.length === 0) return true
|
|
|
|
const lastMessage = messages[messages.length - 1]
|
|
const lastInfo = lastMessage?.info || lastMessage
|
|
return lastInfo?.id === session.revert?.messageID
|
|
}
|
|
}
|
|
|
|
|
|
async function cleanupBlankSessions(instanceId: string, excludeSessionId?: string, fetchIfNeeded = false): Promise<void> {
|
|
const instanceSessions = sessions().get(instanceId)
|
|
if (!instanceSessions) return
|
|
|
|
if (fetchIfNeeded) {
|
|
const confirmed = await showConfirmDialog(
|
|
tGlobal("sessionState.cleanup.deepConfirm.message"),
|
|
{
|
|
title: tGlobal("sessionState.cleanup.deepConfirm.title"),
|
|
detail: tGlobal("sessionState.cleanup.deepConfirm.detail"),
|
|
confirmLabel: tGlobal("sessionState.cleanup.deepConfirm.confirmLabel"),
|
|
cancelLabel: tGlobal("sessionState.cleanup.deepConfirm.cancelLabel"),
|
|
}
|
|
)
|
|
if (!confirmed) return
|
|
}
|
|
|
|
const cleanupPromises = Array.from(instanceSessions)
|
|
.filter(([sessionId]) => sessionId !== excludeSessionId)
|
|
.map(async ([sessionId, session]) => {
|
|
const isBlank = await isBlankSession(session, instanceId, fetchIfNeeded)
|
|
if (!isBlank) return false
|
|
|
|
await deleteSession(instanceId, sessionId).catch((error: Error) => {
|
|
log.error(`Failed to delete blank session ${sessionId}`, error)
|
|
})
|
|
return true
|
|
})
|
|
|
|
if (cleanupPromises.length > 0) {
|
|
log.info(`Cleaning up ${cleanupPromises.length} blank sessions`)
|
|
const deletionResults = await Promise.all(cleanupPromises)
|
|
const deletedCount = deletionResults.filter(Boolean).length
|
|
|
|
if (deletedCount > 0) {
|
|
showToastNotification({
|
|
message: deletedCount === 1
|
|
? tGlobal("sessionState.cleanup.toast.one", { count: deletedCount })
|
|
: tGlobal("sessionState.cleanup.toast.other", { count: deletedCount }),
|
|
variant: "info"
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
export {
|
|
sessions,
|
|
setSessions,
|
|
activeSessionId,
|
|
setActiveSessionId,
|
|
activeParentSessionId,
|
|
setActiveParentSessionId,
|
|
agents,
|
|
setAgents,
|
|
providers,
|
|
setProviders,
|
|
loading,
|
|
setLoading,
|
|
messagesLoaded,
|
|
setMessagesLoaded,
|
|
sessionInfoByInstance,
|
|
setSessionInfoByInstance,
|
|
getSessionDraftPrompt,
|
|
setSessionDraftPrompt,
|
|
clearSessionDraftPrompt,
|
|
clearInstanceDraftPrompts,
|
|
pruneDraftPrompts,
|
|
withSession,
|
|
setSessionPendingPermission,
|
|
setSessionPendingQuestion,
|
|
setSessionStatus,
|
|
setActiveSession,
|
|
|
|
setActiveParentSession,
|
|
|
|
clearActiveParentSession,
|
|
getActiveSession,
|
|
getActiveParentSession,
|
|
getSessions,
|
|
getParentSessions,
|
|
getChildSessions,
|
|
getSessionFamily,
|
|
getSessionThreads,
|
|
getVisibleSessionIds,
|
|
isSessionParentExpanded,
|
|
setSessionParentExpanded,
|
|
toggleSessionParentExpanded,
|
|
ensureSessionParentExpanded,
|
|
setActiveSessionFromList,
|
|
isSessionBusy,
|
|
isSessionMessagesLoading,
|
|
getSessionInfo,
|
|
isBlankSession,
|
|
cleanupBlankSessions,
|
|
}
|