Skip to content

Commit 7608931

Browse files
bargraph: v1
1 parent 13d622a commit 7608931

4 files changed

Lines changed: 111 additions & 22 deletions

File tree

lib/strategies/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,17 @@ export function countMessageTextTokens(msg: WithParts): number {
133133
if (texts.length === 0) return 0
134134
return estimateTokensBatch(texts)
135135
}
136+
137+
export function countAllMessageTokens(msg: WithParts): number {
138+
const parts = Array.isArray(msg.parts) ? msg.parts : []
139+
const texts: string[] = []
140+
for (const part of parts) {
141+
if (part.type === "text") {
142+
texts.push(part.text)
143+
} else {
144+
texts.push(...extractToolContent(part))
145+
}
146+
}
147+
if (texts.length === 0) return 0
148+
return estimateTokensBatch(texts)
149+
}

lib/tools/compress.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type { ToolContext } from "./types"
44
import { ensureSessionInitialized } from "../state"
55
import { saveSessionState } from "../state/persistence"
66
import { loadPrompt } from "../prompts"
7-
import { getCurrentParams, getTotalToolTokens, countMessageTextTokens } from "../strategies/utils"
7+
import { getCurrentParams, countAllMessageTokens, countTokens } from "../strategies/utils"
8+
import type { AssistantMessage } from "@opencode-ai/sdk/v2"
89
import { findStringInMessages, collectToolIdsInRange, collectMessageIdsInRange } from "./utils"
910
import { sendCompressNotification } from "../ui/notification"
1011
import { prune as applyPruneTransforms } from "../messages/prune"
@@ -330,26 +331,42 @@ export function createCompressTool(ctx: ToolContext): ReturnType<typeof tool> {
330331
},
331332
})
332333

333-
let textTokens = 0
334+
let estimatedCompressedTokens = 0
334335
for (const msgId of compressedMessageIds) {
335336
const msg = messages.find((m) => m.info.id === msgId)
336337
if (msg) {
337-
const tokens = countMessageTextTokens(msg)
338-
textTokens += tokens
338+
const tokens = countAllMessageTokens(msg)
339+
estimatedCompressedTokens += tokens
339340
state.prune.messages.set(msgId, tokens)
340341
}
341342
}
342-
const toolTokens = getTotalToolTokens(state, compressedToolIds)
343343
for (const id of compressedToolIds) {
344344
const entry = state.toolParameters.get(id)
345345
state.prune.tools.set(id, entry?.tokenCount ?? 0)
346346
}
347-
const estimatedCompressedTokens = textTokens + toolTokens
347+
348+
// Use API-reported tokens from last assistant message (matches OpenCode UI)
349+
let totalSessionTokens = 0
350+
for (let i = messages.length - 1; i >= 0; i--) {
351+
if (messages[i].info.role === "assistant") {
352+
const info = messages[i].info as AssistantMessage
353+
if (info.tokens?.output > 0) {
354+
totalSessionTokens =
355+
(info.tokens?.input || 0) +
356+
(info.tokens?.output || 0) +
357+
(info.tokens?.reasoning || 0) +
358+
(info.tokens?.cache?.read || 0) +
359+
(info.tokens?.cache?.write || 0)
360+
break
361+
}
362+
}
363+
}
348364

349365
clog.info(C.COMPRESS, `Token Accounting`, {
350-
text: textTokens,
351-
tools: toolTokens,
352-
total: estimatedCompressedTokens,
366+
totalSessionTokens,
367+
estimatedCompressedTokens,
368+
compressedMessages: compressedMessageIds.length,
369+
compressedTools: compressedToolIds.length,
353370
pruneState: {
354371
tools: state.prune.tools.size,
355372
messages: state.prune.messages.size,
@@ -369,6 +386,21 @@ export function createCompressTool(ctx: ToolContext): ReturnType<typeof tool> {
369386
}
370387

371388
const currentParams = getCurrentParams(state, messages, logger)
389+
const summaryTokens = countTokens(args.content.summary)
390+
391+
clog.info(C.COMPRESS, `Notification Values`, {
392+
totalSessionTokens,
393+
estimatedCompressedTokens,
394+
summaryTokens,
395+
reductionPercent:
396+
totalSessionTokens > 0
397+
? `-${Math.round((estimatedCompressedTokens / totalSessionTokens) * 100)}%`
398+
: "N/A",
399+
messageCount: messages.length,
400+
compressedMessageIds: compressedMessageIds.length,
401+
compressedToolIds: compressedToolIds.length,
402+
})
403+
372404
await sendCompressNotification(
373405
client,
374406
logger,
@@ -379,6 +411,9 @@ export function createCompressTool(ctx: ToolContext): ReturnType<typeof tool> {
379411
compressedMessageIds,
380412
topic,
381413
summary,
414+
summaryTokens,
415+
totalSessionTokens,
416+
estimatedCompressedTokens,
382417
rawStartResult,
383418
rawEndResult,
384419
messages.length,
@@ -398,7 +433,7 @@ export function createCompressTool(ctx: ToolContext): ReturnType<typeof tool> {
398433
clog.error(C.STATE, `✗ State Persistence Failed`, { error: err.message })
399434
})
400435

401-
const result = `Compressed ${compressedMessageIds.length} messages (${compressedToolIds.length} tool calls) into summary. The content will be replaced with your summary.`
436+
const result = `Compressed ${compressedMessageIds.length} messages (${compressedToolIds.length} tool calls) into summary (${summaryTokens} tokens). The content will be replaced with your summary.`
402437
clog.info(
403438
C.COMPRESS,
404439
`${separator}\n✓ COMPRESS INVOCATION SUCCESS\nID: ${invocationId}\n\n${result}\n${separator}`,

lib/ui/notification.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import {
44
formatPrunedItemsList,
55
formatStatsHeader,
66
formatTokenCount,
7-
formatProgressBar,
7+
formatCompressBar,
88
} from "./utils"
99
import { ToolParameterEntry } from "../state"
1010
import { PluginConfig } from "../config"
11+
import { clog, C } from "../compress-logger"
1112

1213
export type PruneReason = "completion" | "noise" | "extraction"
1314
export const PRUNE_REASON_LABELS: Record<PruneReason, string> = {
@@ -137,6 +138,9 @@ export async function sendCompressNotification(
137138
messageIds: string[],
138139
topic: string,
139140
summary: string,
141+
summaryTokens: number,
142+
totalSessionTokens: number,
143+
compressedTokens: number,
140144
startResult: any,
141145
endResult: any,
142146
totalMessages: number,
@@ -147,20 +151,27 @@ export async function sendCompressNotification(
147151
}
148152

149153
let message: string
154+
const summaryTokensStr = formatTokenCount(summaryTokens)
150155

151156
if (config.pruneNotification === "minimal") {
152157
message = formatStatsHeader(state.stats.totalPruneTokens, state.stats.pruneTokenCounter)
153158
} else {
154159
message = formatStatsHeader(state.stats.totalPruneTokens, state.stats.pruneTokenCounter)
155160

156-
const pruneTokenCounterStr = `~${formatTokenCount(state.stats.pruneTokenCounter)}`
157-
const progressBar = formatProgressBar(
158-
totalMessages,
159-
startResult.messageIndex,
160-
endResult.messageIndex,
161-
25,
162-
)
163-
message += `\n\n▣ Compressing (${pruneTokenCounterStr}) ${progressBar}`
161+
const pruneTokenCounterStr = `~${formatTokenCount(compressedTokens)}`
162+
163+
clog.info(C.COMPRESS, `sendCompressNotification inputs`, {
164+
totalSessionTokens,
165+
compressedTokens,
166+
summaryTokens,
167+
ratio:
168+
totalSessionTokens > 0 ? (compressedTokens / totalSessionTokens).toFixed(4) : "N/A",
169+
})
170+
171+
const progressBar = formatCompressBar(totalSessionTokens, compressedTokens, 25)
172+
const reduction =
173+
totalSessionTokens > 0 ? Math.round((compressedTokens / totalSessionTokens) * 100) : 0
174+
message += `\n\n▣ Compressing (${pruneTokenCounterStr} removed, ${reduction}% reduction) ${progressBar}`
164175
message += `\n→ Topic: ${topic}`
165176
message += `\n→ Items: ${messageIds.length} messages`
166177
if (toolIds.length > 0) {
@@ -169,7 +180,7 @@ export async function sendCompressNotification(
169180
message += ` condensed`
170181
}
171182
if (config.tools.compress.showCompression) {
172-
message += `\n→ Compression: ${summary}`
183+
message += `\n→ Compression (~${summaryTokensStr}): ${summary}`
173184
}
174185
}
175186

@@ -179,8 +190,8 @@ export async function sendCompressNotification(
179190
const truncatedSummary = truncateToastSummary(summary)
180191
if (truncatedSummary !== summary) {
181192
toastMessage = toastMessage.replace(
182-
`\n→ Compression: ${summary}`,
183-
`\n→ Compression: ${truncatedSummary}`,
193+
`\n→ Compression (~${summaryTokensStr}): ${summary}`,
194+
`\n→ Compression (~${summaryTokensStr}): ${truncatedSummary}`,
184195
)
185196
}
186197
}

lib/ui/utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ToolParameterEntry } from "../state"
22
import { extractParameterKey } from "../messages/utils"
33
import { countTokens } from "../strategies/utils"
4+
import { clog, C } from "../compress-logger"
45

56
export function countDistillationTokens(distillation?: string[]): number {
67
if (!distillation || distillation.length === 0) return 0
@@ -58,6 +59,34 @@ export function formatProgressBar(
5859
return `│${bar}│`
5960
}
6061

62+
export function formatCompressBar(
63+
sessionContext: number,
64+
compressedAmount: number,
65+
width: number = 20,
66+
): string {
67+
if (sessionContext <= 0) return `│${"░".repeat(width)}│ 0%`
68+
69+
// sessionContext already includes compressedAmount (it's the full session total)
70+
const remainingRatio = (sessionContext - compressedAmount) / sessionContext
71+
const remainingWidth = Math.round(remainingRatio * width)
72+
const compressedWidth = width - remainingWidth
73+
const reductionPercent = -Math.round((compressedAmount / sessionContext) * 100)
74+
75+
clog.info(C.COMPRESS, `formatCompressBar`, {
76+
sessionContext,
77+
compressedAmount,
78+
remainingRatio: remainingRatio.toFixed(4),
79+
remainingWidth,
80+
compressedWidth,
81+
reductionPercent,
82+
})
83+
84+
const remainingBar = "█".repeat(Math.max(0, remainingWidth))
85+
const compressedBar = "░".repeat(Math.max(0, compressedWidth))
86+
87+
return `│${remainingBar}${compressedBar}│`
88+
}
89+
6190
export function shortenPath(input: string, workingDirectory?: string): string {
6291
const inPathMatch = input.match(/^(.+) in (.+)$/)
6392
if (inPathMatch) {

0 commit comments

Comments
 (0)