mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
fix(ui): hydrate full child sessions for usage totals
Use the dedicated session children endpoint when refreshing the session list so parent rows and aggregate usage are based on the complete child set rather than the recent /session window. Move thread usage totals into session state and refresh them whenever session info changes, including SSE-driven message updates. Parent message loading now also hydrates child messages from the store while preserving existing loaded-message guards. Validation: npm run typecheck --workspace @codenomad/ui
This commit is contained in:
parent
c9dff33662
commit
a0a8da97e6
5 changed files with 153 additions and 32 deletions
|
|
@ -1,13 +1,9 @@
|
|||
import { createEffect, createMemo, Show, untrack, type Component } from "solid-js"
|
||||
import { getChildSessions, getSessionFamily, getSessionInfo, loadMessages, sessionInfoByInstance } from "../../stores/sessions"
|
||||
import { createMemo, Show, type Component } from "solid-js"
|
||||
import { getChildSessions, getSessionInfo, getThreadTotals } from "../../stores/sessions"
|
||||
import { formatTokenTotal } from "../../lib/formatters"
|
||||
import { computeThreadTotals } from "../../lib/thread-totals"
|
||||
import { useI18n } from "../../lib/i18n"
|
||||
import { getLogger } from "../../lib/logger"
|
||||
import { useConfig } from "../../stores/preferences"
|
||||
|
||||
const log = getLogger("session")
|
||||
|
||||
interface ContextUsagePanelProps {
|
||||
instanceId: string
|
||||
sessionId: string
|
||||
|
|
@ -39,32 +35,9 @@ const ContextUsagePanel: Component<ContextUsagePanelProps> = (props) => {
|
|||
const children = createMemo(() => getChildSessions(props.instanceId, props.sessionId))
|
||||
const hasChildren = createMemo(() => children().length > 0)
|
||||
|
||||
const instanceInfoMap = createMemo(() =>
|
||||
sessionInfoByInstance().get(props.instanceId),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (!showUsage()) return
|
||||
const instanceId = props.instanceId
|
||||
const childSessions = children()
|
||||
untrack(() => {
|
||||
for (const child of childSessions) {
|
||||
loadMessages(instanceId, child.id, { skipDiff: true }).catch((error) =>
|
||||
log.error("Failed to load child session messages", {
|
||||
instanceId,
|
||||
sessionId: child.id,
|
||||
error,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const threadTotals = createMemo(() => {
|
||||
if (!hasChildren()) return { cost: 0, inputTokens: 0, outputTokens: 0, reasoningTokens: 0 }
|
||||
const map = instanceInfoMap()
|
||||
const family = getSessionFamily(props.instanceId, props.sessionId)
|
||||
return computeThreadTotals(family, map)
|
||||
return getThreadTotals(props.instanceId, props.sessionId) ?? { cost: 0, inputTokens: 0, outputTokens: 0, reasoningTokens: 0 }
|
||||
})
|
||||
|
||||
const totalCostDisplay = createMemo(() => `$${threadTotals().cost.toFixed(2)}`)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Provider } from "../../types/session"
|
||||
import { DEFAULT_MODEL_OUTPUT_LIMIT } from "../session-models"
|
||||
import { providers, sessions, sessionInfoByInstance, setSessionInfoByInstance } from "../session-state"
|
||||
import { providers, sessions, sessionInfoByInstance, setSessionInfoByInstance, updateThreadTotalsForSession } from "../session-state"
|
||||
import { messageStoreBus } from "./bus"
|
||||
import type { SessionUsageState } from "./types"
|
||||
|
||||
|
|
@ -140,4 +140,6 @@ export function updateSessionInfo(instanceId: string, sessionId: string): void {
|
|||
next.set(instanceId, instanceInfo)
|
||||
return next
|
||||
})
|
||||
|
||||
updateThreadTotalsForSession(instanceId, sessionId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
setLoading,
|
||||
cleanupBlankSessions,
|
||||
syncInstanceSessionIndicator,
|
||||
updateThreadTotalsForParent,
|
||||
} from "./session-state"
|
||||
import { DEFAULT_MODEL_OUTPUT_LIMIT, getDefaultModel, isModelValid } from "./session-models"
|
||||
import { normalizeMessagePart } from "./message-v2/normalizers"
|
||||
|
|
@ -45,6 +46,7 @@ import {
|
|||
const log = getLogger("api")
|
||||
|
||||
const pendingSessionDiffFetches = new Map<string, Promise<void>>()
|
||||
const pendingSessionChildrenFetches = new Map<string, Promise<Session[]>>()
|
||||
|
||||
async function loadSessionDiff(instanceId: string, sessionId: string, force = false): Promise<void> {
|
||||
if (!instanceId || !sessionId) return
|
||||
|
|
@ -218,6 +220,12 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||
|
||||
|
||||
pruneDraftPrompts(instanceId, new Set(sessionMap.keys()))
|
||||
|
||||
const parentIds = Array.from(sessionMap.values())
|
||||
.filter((session) => session.parentId === null)
|
||||
.map((session) => session.id)
|
||||
|
||||
await Promise.all(parentIds.map((parentId) => fetchSessionChildren(instanceId, parentId)))
|
||||
} catch (error) {
|
||||
log.error("Failed to fetch sessions:", error)
|
||||
throw error
|
||||
|
|
@ -230,6 +238,96 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
function toClientSession(instanceId: string, apiSession: any, existingSession?: Session): Session {
|
||||
return {
|
||||
id: apiSession.id,
|
||||
instanceId,
|
||||
title: apiSession.title || existingSession?.title || "Untitled",
|
||||
parentId: apiSession.parentID || null,
|
||||
agent: existingSession?.agent ?? "",
|
||||
model: existingSession?.model ?? { providerId: "", modelId: "" },
|
||||
status: existingSession?.status ?? "idle",
|
||||
retry: existingSession?.retry ?? null,
|
||||
idleSince: existingSession?.idleSince ?? null,
|
||||
version: apiSession.version,
|
||||
time: {
|
||||
...apiSession.time,
|
||||
},
|
||||
revert: apiSession.revert
|
||||
? {
|
||||
messageID: apiSession.revert.messageID,
|
||||
partID: apiSession.revert.partID,
|
||||
snapshot: apiSession.revert.snapshot,
|
||||
diff: apiSession.revert.diff,
|
||||
}
|
||||
: existingSession?.revert,
|
||||
diff: existingSession?.diff,
|
||||
pendingPermission: existingSession?.pendingPermission,
|
||||
pendingQuestion: existingSession?.pendingQuestion,
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSessionChildren(instanceId: string, parentSessionId: string): Promise<Session[]> {
|
||||
if (!instanceId || !parentSessionId) return []
|
||||
|
||||
const key = `${instanceId}:${parentSessionId}`
|
||||
const pending = pendingSessionChildrenFetches.get(key)
|
||||
if (pending) return pending
|
||||
|
||||
const promise = (async () => {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance || !instance.client) {
|
||||
throw new Error("Instance not ready")
|
||||
}
|
||||
|
||||
const worktreeSlug = getWorktreeSlugForSession(instanceId, parentSessionId)
|
||||
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
|
||||
|
||||
log.info(`[HTTP] GET /session/{sessionID}/children for instance ${instanceId}`, { sessionId: parentSessionId })
|
||||
const apiChildren = await requestData<any[]>(
|
||||
client.session.children({ sessionID: parentSessionId }),
|
||||
"session.children",
|
||||
)
|
||||
|
||||
if (!Array.isArray(apiChildren)) return []
|
||||
|
||||
const currentSessions = sessions().get(instanceId)
|
||||
const children = apiChildren.map((apiSession) => toClientSession(instanceId, apiSession, currentSessions?.get(apiSession.id)))
|
||||
|
||||
setSessions((prev) => {
|
||||
const next = new Map(prev)
|
||||
const instanceSessions = new Map(next.get(instanceId))
|
||||
for (const child of children) {
|
||||
instanceSessions.set(child.id, child)
|
||||
}
|
||||
next.set(instanceId, instanceSessions)
|
||||
return next
|
||||
})
|
||||
|
||||
syncInstanceSessionIndicator(instanceId)
|
||||
updateThreadTotalsForParent(instanceId, parentSessionId)
|
||||
|
||||
if (messagesLoaded().get(instanceId)?.has(parentSessionId)) {
|
||||
for (const child of children) {
|
||||
void loadMessages(instanceId, child.id, { skipDiff: true, skipChildren: true }).catch((error) =>
|
||||
log.error("Failed to load child session messages", {
|
||||
instanceId,
|
||||
sessionId: child.id,
|
||||
parentSessionId,
|
||||
error,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return children
|
||||
})()
|
||||
|
||||
pendingSessionChildrenFetches.set(key, promise)
|
||||
void promise.finally(() => pendingSessionChildrenFetches.delete(key))
|
||||
return promise
|
||||
}
|
||||
|
||||
async function createSession(instanceId: string, agent?: string): Promise<Session> {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance || !instance.client) {
|
||||
|
|
@ -598,10 +696,11 @@ async function fetchProviders(instanceId: string): Promise<void> {
|
|||
async function loadMessages(
|
||||
instanceId: string,
|
||||
sessionId: string,
|
||||
options?: { force?: boolean; skipDiff?: boolean },
|
||||
options?: { force?: boolean; skipDiff?: boolean; skipChildren?: boolean },
|
||||
): Promise<void> {
|
||||
const force = options?.force ?? false
|
||||
const skipDiff = options?.skipDiff ?? false
|
||||
const skipChildren = options?.skipChildren ?? false
|
||||
|
||||
if (force) {
|
||||
setMessagesLoaded((prev) => {
|
||||
|
|
@ -783,6 +882,19 @@ async function loadMessages(
|
|||
}
|
||||
|
||||
updateSessionInfo(instanceId, sessionId)
|
||||
|
||||
if (!skipChildren && session.parentId === null) {
|
||||
for (const child of getChildSessions(instanceId, sessionId)) {
|
||||
void loadMessages(instanceId, child.id, { skipDiff: true, skipChildren: true }).catch((error) =>
|
||||
log.error("Failed to load child session messages", {
|
||||
instanceId,
|
||||
sessionId: child.id,
|
||||
parentSessionId: sessionId,
|
||||
error,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -792,6 +904,7 @@ export {
|
|||
fetchProviders,
|
||||
|
||||
fetchSessions,
|
||||
fetchSessionChildren,
|
||||
forkSession,
|
||||
loadMessages,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { getLogger } from "../lib/logger"
|
|||
import { requestData } from "../lib/opencode-api"
|
||||
import { getOrCreateWorktreeClient, getWorktreeSlugForSession } from "./worktrees"
|
||||
import { tGlobal } from "../lib/i18n"
|
||||
import { computeThreadTotals, type ThreadTotals } from "../lib/thread-totals"
|
||||
|
||||
const log = getLogger("session")
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ const [loading, setLoading] = createSignal({
|
|||
|
||||
const [messagesLoaded, setMessagesLoaded] = createSignal<Map<string, Set<string>>>(new Map())
|
||||
const [sessionInfoByInstance, setSessionInfoByInstance] = createSignal<Map<string, Map<string, SessionInfo>>>(new Map())
|
||||
const [threadTotalsByInstance, setThreadTotalsByInstance] = createSignal<Map<string, Map<string, ThreadTotals>>>(new Map())
|
||||
|
||||
const [expandedSessionParents, setExpandedSessionParents] = createSignal<Map<string, Set<string>>>(new Map())
|
||||
|
||||
|
|
@ -648,6 +650,29 @@ function getSessionInfo(instanceId: string, sessionId: string): SessionInfo | un
|
|||
return sessionInfoByInstance().get(instanceId)?.get(sessionId)
|
||||
}
|
||||
|
||||
function getThreadTotals(instanceId: string, parentSessionId: string): ThreadTotals | undefined {
|
||||
return threadTotalsByInstance().get(instanceId)?.get(parentSessionId)
|
||||
}
|
||||
|
||||
function updateThreadTotalsForParent(instanceId: string, parentSessionId: string): void {
|
||||
const family = getSessionFamily(instanceId, parentSessionId)
|
||||
const totals = computeThreadTotals(family, sessionInfoByInstance().get(instanceId))
|
||||
|
||||
setThreadTotalsByInstance((prev) => {
|
||||
const next = new Map(prev)
|
||||
const instanceTotals = new Map(next.get(instanceId))
|
||||
instanceTotals.set(parentSessionId, totals)
|
||||
next.set(instanceId, instanceTotals)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function updateThreadTotalsForSession(instanceId: string, sessionId: string): void {
|
||||
const session = sessions().get(instanceId)?.get(sessionId)
|
||||
if (!session) return
|
||||
updateThreadTotalsForParent(instanceId, session.parentId ?? session.id)
|
||||
}
|
||||
|
||||
async function isBlankSession(session: Session, instanceId: string, fetchIfNeeded = false): Promise<boolean> {
|
||||
const created = session.time?.created || 0
|
||||
const updated = session.time?.updated || 0
|
||||
|
|
@ -774,6 +799,10 @@ export {
|
|||
setMessagesLoaded,
|
||||
sessionInfoByInstance,
|
||||
setSessionInfoByInstance,
|
||||
threadTotalsByInstance,
|
||||
getThreadTotals,
|
||||
updateThreadTotalsForParent,
|
||||
updateThreadTotalsForSession,
|
||||
getSessionDraftPrompt,
|
||||
setSessionDraftPrompt,
|
||||
clearSessionDraftPrompt,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
getSessionFamily,
|
||||
getSessionInfo,
|
||||
getSessionThreads,
|
||||
getThreadTotals,
|
||||
getSessions,
|
||||
getVisibleSessionIds,
|
||||
isSessionBusy,
|
||||
|
|
@ -45,6 +46,7 @@ import {
|
|||
fetchAgents,
|
||||
fetchProviders,
|
||||
fetchSessions,
|
||||
fetchSessionChildren,
|
||||
forkSession,
|
||||
loadMessages,
|
||||
} from "./session-api"
|
||||
|
|
@ -109,6 +111,7 @@ export {
|
|||
fetchAgents,
|
||||
fetchProviders,
|
||||
fetchSessions,
|
||||
fetchSessionChildren,
|
||||
forkSession,
|
||||
getActiveParentSession,
|
||||
getActiveSession,
|
||||
|
|
@ -119,6 +122,7 @@ export {
|
|||
getSessionFamily,
|
||||
getSessionInfo,
|
||||
getSessionThreads,
|
||||
getThreadTotals,
|
||||
getSessions,
|
||||
getVisibleSessionIds,
|
||||
isSessionBusy,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue