fix(tui): stabilize compaction completion (#36435)
This commit is contained in:
@@ -57,7 +57,7 @@ import { usePromptMove } from "./move"
|
|||||||
import { readLocalAttachment } from "./local-attachment"
|
import { readLocalAttachment } from "./local-attachment"
|
||||||
import { useData } from "../../context/data"
|
import { useData } from "../../context/data"
|
||||||
import { useLocation } from "../../context/location"
|
import { useLocation } from "../../context/location"
|
||||||
import { lastAssistantWithUsage } from "../../util/session"
|
import { contextUsage } from "../../util/session"
|
||||||
|
|
||||||
registerOpencodeSpinner()
|
registerOpencodeSpinner()
|
||||||
|
|
||||||
@@ -280,21 +280,20 @@ export function Prompt(props: PromptProps) {
|
|||||||
if (!props.sessionID) return
|
if (!props.sessionID) return
|
||||||
const session = data.session.get(props.sessionID)
|
const session = data.session.get(props.sessionID)
|
||||||
if (!session) return
|
if (!session) return
|
||||||
const last = lastAssistantWithUsage(data.session.message.list(props.sessionID), session.revert?.messageID)
|
|
||||||
if (!last) return
|
|
||||||
|
|
||||||
const tokens =
|
|
||||||
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
|
||||||
if (tokens <= 0) return
|
|
||||||
|
|
||||||
const model = data.location.model
|
|
||||||
.list(session.location)
|
|
||||||
?.find((model) => model.providerID === last.model.providerID && model.id === last.model.id)
|
|
||||||
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
|
|
||||||
const cost = data.session.cost(props.sessionID)
|
const cost = data.session.cost(props.sessionID)
|
||||||
|
const formattedCost = cost > 0 ? money.format(cost) : undefined
|
||||||
|
const context = contextUsage(
|
||||||
|
data.session.message.list(props.sessionID),
|
||||||
|
data.location.model.list(session.location),
|
||||||
|
session.revert?.messageID,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
|
context: context
|
||||||
cost: cost > 0 ? money.format(cost) : undefined,
|
? context.percent === undefined
|
||||||
|
? Locale.number(context.tokens)
|
||||||
|
: `${Locale.number(context.tokens)} (${context.percent}%)`
|
||||||
|
: undefined,
|
||||||
|
cost: formattedCost,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -669,16 +669,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running")
|
const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running")
|
||||||
const current = draft[position]
|
const current = draft[position]
|
||||||
if (current?.type === "compaction") {
|
if (current?.type === "compaction") {
|
||||||
draft[position] = {
|
Object.assign(current, {
|
||||||
id: current.id,
|
|
||||||
type: "compaction",
|
|
||||||
status: "completed",
|
status: "completed",
|
||||||
reason: event.data.reason,
|
reason: event.data.reason,
|
||||||
summary: event.data.text,
|
summary: event.data.text,
|
||||||
recent: event.data.recent,
|
recent: event.data.recent,
|
||||||
metadata: current.metadata,
|
})
|
||||||
time: current.time,
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
message.append(draft, index, {
|
message.append(draft, index, {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui"
|
||||||
import type { BuiltinTuiPlugin } from "../builtins"
|
import type { BuiltinTuiPlugin } from "../builtins"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo, Show } from "solid-js"
|
||||||
import { useData } from "../../context/data"
|
import { useData } from "../../context/data"
|
||||||
import { lastAssistantWithUsage } from "../../util/session"
|
import { contextUsage } from "../../util/session"
|
||||||
|
|
||||||
const id = "internal:sidebar-context"
|
const id = "internal:sidebar-context"
|
||||||
|
|
||||||
@@ -18,33 +18,23 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
|
|||||||
const session = createMemo(() => data.session.get(props.session_id))
|
const session = createMemo(() => data.session.get(props.session_id))
|
||||||
const cost = createMemo(() => data.session.cost(props.session_id))
|
const cost = createMemo(() => data.session.cost(props.session_id))
|
||||||
|
|
||||||
const state = createMemo(() => {
|
const state = createMemo(() => contextUsage(msg(), data.location.model.list(session()?.location), session()?.revert?.messageID))
|
||||||
const last = lastAssistantWithUsage(msg(), session()?.revert?.messageID)
|
|
||||||
if (!last) {
|
|
||||||
return {
|
|
||||||
tokens: 0,
|
|
||||||
percent: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens =
|
|
||||||
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
|
||||||
const model = data.location
|
|
||||||
.model.list(session()?.location)
|
|
||||||
?.find((model) => model.providerID === last.model.providerID && model.id === last.model.id)
|
|
||||||
return {
|
|
||||||
tokens,
|
|
||||||
percent: model?.limit.context ? Math.round((tokens / model.limit.context) * 100) : null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box>
|
<box>
|
||||||
<text fg={theme().text}>
|
<text fg={theme().text}>
|
||||||
<b>Context</b>
|
<b>Context</b>
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme().textMuted}>{state().tokens.toLocaleString()} tokens</text>
|
<Show when={state()} fallback={<text fg={theme().textMuted}>Not measured</text>}>
|
||||||
<text fg={theme().textMuted}>{state().percent ?? 0}% used</text>
|
{(value) => (
|
||||||
|
<>
|
||||||
|
<text fg={theme().textMuted}>{value().tokens.toLocaleString()} tokens</text>
|
||||||
|
<Show when={value().percent !== undefined}>
|
||||||
|
<text fg={theme().textMuted}>{value().percent}% used</text>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
<text fg={theme().textMuted}>{money.format(cost())} spent</text>
|
<text fg={theme().textMuted}>{money.format(cost())} spent</text>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1325,23 +1325,18 @@ function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { typ
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CompactionMessage(props: {
|
function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type: "compaction" }> }) {
|
||||||
message?: Extract<SessionMessageInfo, { type: "compaction" }>
|
|
||||||
status?: "running"
|
|
||||||
text?: string
|
|
||||||
}) {
|
|
||||||
const ctx = use()
|
const ctx = use()
|
||||||
const kv = useKV()
|
const kv = useKV()
|
||||||
const { theme, syntax } = useTheme()
|
const { theme, syntax } = useTheme()
|
||||||
const status = () => props.message?.status ?? props.status
|
const status = () => props.message.status
|
||||||
const text = () =>
|
const text = () => (props.message.status === "failed" ? props.message.error.message : props.message.summary)
|
||||||
props.message?.status === "failed" ? props.message.error.message : (props.message?.summary ?? props.text ?? "")
|
const content = createMemo(() => text().trim())
|
||||||
const color = () => (status() === "failed" ? theme.error : status() === "completed" ? theme.success : theme.textMuted)
|
const color = () => (status() === "failed" ? theme.error : theme.textMuted)
|
||||||
const border = color
|
|
||||||
return (
|
return (
|
||||||
<box>
|
<box>
|
||||||
<box flexDirection="row" alignItems="center">
|
<box flexDirection="row" alignItems="center">
|
||||||
<box border={["top"]} borderColor={border()} flexGrow={1} />
|
<box border={["top"]} borderColor={color()} flexGrow={1} />
|
||||||
<box flexDirection="row" gap={1} paddingLeft={1} paddingRight={1}>
|
<box flexDirection="row" gap={1} paddingLeft={1} paddingRight={1}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={status() === "running"}>
|
<Match when={status() === "running"}>
|
||||||
@@ -1349,24 +1344,21 @@ function CompactionMessage(props: {
|
|||||||
<spinner frames={SPINNER_FRAMES} interval={80} color={color()} />
|
<spinner frames={SPINNER_FRAMES} interval={80} color={color()} />
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={status() === "completed"}>
|
|
||||||
<text fg={color()}>✓</text>
|
|
||||||
</Match>
|
|
||||||
<Match when={status() === "failed"}>
|
<Match when={status() === "failed"}>
|
||||||
<text fg={color()}>✗</text>
|
<text fg={color()}>✗</text>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
<text fg={color()}>Compaction</text>
|
<text fg={color()}>Compaction</text>
|
||||||
</box>
|
</box>
|
||||||
<box border={["top"]} borderColor={border()} flexGrow={1} />
|
<box border={["top"]} borderColor={color()} flexGrow={1} />
|
||||||
</box>
|
</box>
|
||||||
<Show when={text().trim()}>
|
<Show when={content()}>
|
||||||
<box paddingTop={1} paddingLeft={3}>
|
<box paddingTop={1} paddingLeft={3}>
|
||||||
<markdown
|
<markdown
|
||||||
syntaxStyle={syntax()}
|
syntaxStyle={syntax()}
|
||||||
streaming={status() === "running"}
|
streaming={true}
|
||||||
internalBlockMode="top-level"
|
internalBlockMode="top-level"
|
||||||
content={text().trim()}
|
content={content()}
|
||||||
tableOptions={{ style: "grid" }}
|
tableOptions={{ style: "grid" }}
|
||||||
conceal={ctx.conceal()}
|
conceal={ctx.conceal()}
|
||||||
fg={theme.markdownText}
|
fg={theme.markdownText}
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||||||
{
|
{
|
||||||
id: message.id,
|
id: message.id,
|
||||||
created: message.time.created,
|
created: message.time.created,
|
||||||
input: message.status === "running",
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
@@ -183,7 +182,9 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||||||
}
|
}
|
||||||
const subscriptions = [
|
const subscriptions = [
|
||||||
data.on("session.input.admitted", input),
|
data.on("session.input.admitted", input),
|
||||||
data.on("session.compaction.started", message),
|
data.on("session.compaction.started", (event) => {
|
||||||
|
if (event.data.sessionID === sessionID()) appendMessage(event.data.inputID ?? event.id.replace(/^evt_/, "msg_"))
|
||||||
|
}),
|
||||||
data.on("session.instructions.updated", message),
|
data.on("session.instructions.updated", message),
|
||||||
data.on("session.synthetic", (event) => {
|
data.on("session.synthetic", (event) => {
|
||||||
if (event.data.sessionID === sessionID() && event.data.description?.trim())
|
if (event.data.sessionID === sessionID() && event.data.description?.trim())
|
||||||
@@ -192,9 +193,6 @@ export function createSessionRows(sessionID: Accessor<string>) {
|
|||||||
data.on("session.shell.started", message),
|
data.on("session.shell.started", message),
|
||||||
data.on("session.agent.selected", message),
|
data.on("session.agent.selected", message),
|
||||||
data.on("session.model.selected", message),
|
data.on("session.model.selected", message),
|
||||||
data.on("session.compaction.ended", (event) => {
|
|
||||||
if (event.data.reason !== "manual") message(event)
|
|
||||||
}),
|
|
||||||
data.on("session.text.delta", (event) => {
|
data.on("session.text.delta", (event) => {
|
||||||
if (event.data.sessionID === sessionID())
|
if (event.data.sessionID === sessionID())
|
||||||
appendPart({ messageID: event.data.assistantMessageID, partID: `text:${event.data.ordinal}` })
|
appendPart({ messageID: event.data.assistantMessageID, partID: `text:${event.data.ordinal}` })
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import { SplitBorder } from "../../ui/border"
|
|||||||
import { Locale } from "../../util/locale"
|
import { Locale } from "../../util/locale"
|
||||||
import { useTerminalDimensions } from "@opentui/solid"
|
import { useTerminalDimensions } from "@opentui/solid"
|
||||||
import { useCommandShortcut, useOpencodeKeymap } from "../../keymap"
|
import { useCommandShortcut, useOpencodeKeymap } from "../../keymap"
|
||||||
import { lastAssistantWithUsage } from "../../util/session"
|
import { contextUsage } from "../../util/session"
|
||||||
|
|
||||||
|
const money = new Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD",
|
||||||
|
})
|
||||||
|
|
||||||
export function SubagentFooter() {
|
export function SubagentFooter() {
|
||||||
const route = useRouteData("session")
|
const route = useRouteData("session")
|
||||||
@@ -23,26 +28,21 @@ export function SubagentFooter() {
|
|||||||
const usage = createMemo(() => {
|
const usage = createMemo(() => {
|
||||||
const current = session()
|
const current = session()
|
||||||
if (!current) return
|
if (!current) return
|
||||||
const last = lastAssistantWithUsage(data.session.message.list(route.sessionID), current.revert?.messageID)
|
const cost = current.cost
|
||||||
if (!last) return
|
const formattedCost = cost > 0 ? money.format(cost) : undefined
|
||||||
const tokens =
|
const context = contextUsage(
|
||||||
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
data.session.message.list(route.sessionID),
|
||||||
if (tokens <= 0) return
|
data.location.model.list(current.location),
|
||||||
|
current.revert?.messageID,
|
||||||
const model = data.location
|
)
|
||||||
.model.list(current.location)
|
|
||||||
?.find((model) => model.providerID === last.model.providerID && model.id === last.model.id)
|
|
||||||
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
|
|
||||||
const cost = data.session.cost(route.sessionID)
|
|
||||||
|
|
||||||
const money = new Intl.NumberFormat("en-US", {
|
|
||||||
style: "currency",
|
|
||||||
currency: "USD",
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
|
context: context
|
||||||
cost: cost > 0 ? money.format(cost) : undefined,
|
? context.percent === undefined
|
||||||
|
? Locale.number(context.tokens)
|
||||||
|
: `${Locale.number(context.tokens)} (${context.percent}%)`
|
||||||
|
: undefined,
|
||||||
|
cost: formattedCost,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/sdk/v2"
|
import type { ModelInfo, SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
export function isDefaultTitle(title: string) {
|
export function isDefaultTitle(title: string) {
|
||||||
return /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(title)
|
return /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(title)
|
||||||
@@ -7,8 +7,29 @@ export function isDefaultTitle(title: string) {
|
|||||||
export function lastAssistantWithUsage(messages: ReadonlyArray<SessionMessageInfo>, boundary?: string) {
|
export function lastAssistantWithUsage(messages: ReadonlyArray<SessionMessageInfo>, boundary?: string) {
|
||||||
const boundaryIndex = boundary ? messages.findIndex((message) => message.id === boundary) : -1
|
const boundaryIndex = boundary ? messages.findIndex((message) => message.id === boundary) : -1
|
||||||
if (boundary && boundaryIndex === -1) return undefined
|
if (boundary && boundaryIndex === -1) return undefined
|
||||||
|
const end = boundaryIndex === -1 ? messages.length : boundaryIndex
|
||||||
|
const compactionIndex = messages.findLastIndex(
|
||||||
|
(message, index) => message.type === "compaction" && message.status === "completed" && index < end,
|
||||||
|
)
|
||||||
return messages.findLast(
|
return messages.findLast(
|
||||||
(message, index): message is SessionMessageAssistant & { tokens: NonNullable<SessionMessageAssistant["tokens"]> } =>
|
(message, index): message is SessionMessageAssistant & { tokens: NonNullable<SessionMessageAssistant["tokens"]> } =>
|
||||||
message.type === "assistant" && message.tokens !== undefined && (boundaryIndex === -1 || index < boundaryIndex),
|
message.type === "assistant" && message.tokens !== undefined && index > compactionIndex && index < end,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function contextUsage(
|
||||||
|
messages: ReadonlyArray<SessionMessageInfo>,
|
||||||
|
models: ReadonlyArray<ModelInfo> | undefined,
|
||||||
|
boundary?: string,
|
||||||
|
) {
|
||||||
|
const last = lastAssistantWithUsage(messages, boundary)
|
||||||
|
if (!last) return
|
||||||
|
const tokens =
|
||||||
|
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
||||||
|
if (tokens <= 0) return
|
||||||
|
const model = models?.find((model) => model.providerID === last.model.providerID && model.id === last.model.id)
|
||||||
|
return {
|
||||||
|
tokens,
|
||||||
|
percent: model?.limit.context ? Math.round((tokens / model.limit.context) * 100) : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1064,6 +1064,9 @@ test("tracks session status from active sessions and execution events", async ()
|
|||||||
const message = data.session.message.get("session-manual", "message-compaction")
|
const message = data.session.message.get("session-manual", "message-compaction")
|
||||||
return message?.type === "compaction" && message.status === "running" && message.summary === "Streamed summary"
|
return message?.type === "compaction" && message.status === "running" && message.summary === "Streamed summary"
|
||||||
})
|
})
|
||||||
|
const compactionRow = manualRows.find(
|
||||||
|
(row) => row.type === "message" && row.messageID === "message-compaction",
|
||||||
|
)
|
||||||
emitEvent(events, {
|
emitEvent(events, {
|
||||||
id: "evt_manual_compaction_ended",
|
id: "evt_manual_compaction_ended",
|
||||||
created: 3,
|
created: 3,
|
||||||
@@ -1078,6 +1081,9 @@ test("tracks session status from active sessions and execution events", async ()
|
|||||||
expect(manualRows.filter((row) => row.type === "message")).toEqual([
|
expect(manualRows.filter((row) => row.type === "message")).toEqual([
|
||||||
{ type: "message", messageID: "message-compaction" },
|
{ type: "message", messageID: "message-compaction" },
|
||||||
])
|
])
|
||||||
|
expect(manualRows.find((row) => row.type === "message" && row.messageID === "message-compaction")).toBe(
|
||||||
|
compactionRow,
|
||||||
|
)
|
||||||
|
|
||||||
emitEvent(events, {
|
emitEvent(events, {
|
||||||
id: "evt_compaction_started",
|
id: "evt_compaction_started",
|
||||||
@@ -1102,6 +1108,9 @@ test("tracks session status from active sessions and execution events", async ()
|
|||||||
const message = data.session.message.get("session-live", "msg_compaction_started")
|
const message = data.session.message.get("session-live", "msg_compaction_started")
|
||||||
return message?.type === "compaction" && message.status === "running" && message.summary === "Live summary"
|
return message?.type === "compaction" && message.status === "running" && message.summary === "Live summary"
|
||||||
})
|
})
|
||||||
|
const autoCompactionRow = rows.find(
|
||||||
|
(row) => row.type === "message" && row.messageID === "msg_compaction_started",
|
||||||
|
)
|
||||||
|
|
||||||
emitEvent(events, {
|
emitEvent(events, {
|
||||||
id: "evt_compaction_ended",
|
id: "evt_compaction_ended",
|
||||||
@@ -1119,6 +1128,10 @@ test("tracks session status from active sessions and execution events", async ()
|
|||||||
status: "completed",
|
status: "completed",
|
||||||
summary: "Live summary",
|
summary: "Live summary",
|
||||||
})
|
})
|
||||||
|
expect(rows.find((row) => row.type === "message" && row.messageID === "msg_compaction_started")).toBe(
|
||||||
|
autoCompactionRow,
|
||||||
|
)
|
||||||
|
expect(rows.some((row) => row.type === "message" && row.messageID === "msg_compaction_ended")).toBeFalse()
|
||||||
} finally {
|
} finally {
|
||||||
app.renderer.destroy()
|
app.renderer.destroy()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,6 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import type { SessionMessageInfo } from "@opencode-ai/sdk/v2"
|
import type { SessionMessageInfo } from "@opencode-ai/sdk/v2"
|
||||||
import { isDefaultTitle, lastAssistantWithUsage } from "../../src/util/session"
|
import { isDefaultTitle, lastAssistantWithUsage } from "../../src/util/session"
|
||||||
|
|
||||||
describe("util.session", () => {
|
|
||||||
test("recognizes generated parent and child titles", () => {
|
|
||||||
expect(isDefaultTitle("New session - 2026-06-06T12:34:56.789Z")).toBeTrue()
|
|
||||||
expect(isDefaultTitle("Child session - 2026-06-06T12:34:56.789Z")).toBeTrue()
|
|
||||||
expect(isDefaultTitle("New session - custom")).toBeFalse()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("tracks usage across undo and redo boundaries", () => {
|
|
||||||
const assistant = (id: string, input: number): SessionMessageInfo => ({
|
const assistant = (id: string, input: number): SessionMessageInfo => ({
|
||||||
id,
|
id,
|
||||||
type: "assistant",
|
type: "assistant",
|
||||||
@@ -19,6 +11,15 @@ describe("util.session", () => {
|
|||||||
tokens: { input, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
tokens: { input, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||||
time: { created: 0 },
|
time: { created: 0 },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("util.session", () => {
|
||||||
|
test("recognizes generated parent and child titles", () => {
|
||||||
|
expect(isDefaultTitle("New session - 2026-06-06T12:34:56.789Z")).toBeTrue()
|
||||||
|
expect(isDefaultTitle("Child session - 2026-06-06T12:34:56.789Z")).toBeTrue()
|
||||||
|
expect(isDefaultTitle("New session - custom")).toBeFalse()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("tracks usage across undo and redo boundaries", () => {
|
||||||
const messages = [assistant("msg_z", 10), assistant("msg_a", 30)]
|
const messages = [assistant("msg_z", 10), assistant("msg_a", 30)]
|
||||||
|
|
||||||
expect(lastAssistantWithUsage(messages)?.tokens.input).toBe(30)
|
expect(lastAssistantWithUsage(messages)?.tokens.input).toBe(30)
|
||||||
@@ -26,4 +27,22 @@ describe("util.session", () => {
|
|||||||
expect(lastAssistantWithUsage(messages, "msg_missing")).toBeUndefined()
|
expect(lastAssistantWithUsage(messages, "msg_missing")).toBeUndefined()
|
||||||
expect(lastAssistantWithUsage(messages)?.tokens.input).toBe(30)
|
expect(lastAssistantWithUsage(messages)?.tokens.input).toBe(30)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("resets usage at completed compaction until the next assistant reports it", () => {
|
||||||
|
const compaction: SessionMessageInfo = {
|
||||||
|
id: "msg_compaction",
|
||||||
|
type: "compaction",
|
||||||
|
status: "completed",
|
||||||
|
reason: "manual",
|
||||||
|
summary: "Current state",
|
||||||
|
recent: "",
|
||||||
|
time: { created: 0 },
|
||||||
|
}
|
||||||
|
const messages = [assistant("msg_before", 30), compaction]
|
||||||
|
|
||||||
|
expect(lastAssistantWithUsage(messages)).toBeUndefined()
|
||||||
|
|
||||||
|
messages.push(assistant("msg_after", 5))
|
||||||
|
expect(lastAssistantWithUsage(messages)?.tokens.input).toBe(5)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user