import { existsSync, readFileSync } from "node:fs"
import { isAbsolute, relative, resolve } from "node:path"
import { pathToFileURL } from "node:url"
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent"
import {
buildSessionContext,
convertToLlm,
formatSkillsForPrompt,
parseSkillBlock,
type SessionEntry
} from "@earendil-works/pi-coding-agent"
import { Box, Text, wrapTextWithAnsi } from "@earendil-works/pi-tui"
import { CONTEXT_READ_MAP_ENTRY_TYPE } from "./entries"
import { isRecord, numberValue } from "./schema"
type EvidenceKind = "startup-context" | "advertised-skill" | "loaded-skill-body" | "tool-read"
type EvidenceRange = {
startLine: number
endLine: number
}
type EvidenceSource = {
kind: EvidenceKind
range: EvidenceRange
ordinal: number
media?: boolean
}
type FileEvidence = {
path: string
displayPath: string
totalLines: number
missing: boolean
sources: EvidenceSource[]
order: number
}
const TOKEN_ROWS = [
{ id: "system-base", section: "prefix", label: "System prompt · base", unit: "section" },
{ id: "startup-context", section: "prefix", label: "Startup context files", unit: "file" },
{ id: "advertised-skills", section: "prefix", label: "Advertised skills", unit: "skill" },
{ id: "tool-definitions", section: "prefix", label: "Tool definitions", unit: "tool" },
{ id: "user-messages", section: "messages", label: "User messages", unit: "message" },
{ id: "loaded-skills", section: "messages", label: "Loaded skill bodies", unit: "skill" },
{ id: "assistant-text", section: "messages", label: "Assistant text", unit: "block" },
{ id: "assistant-thinking", section: "messages", label: "Assistant thinking", unit: "block" },
{ id: "tool-calls", section: "messages", label: "Tool calls", unit: "call" },
{ id: "tool-results", section: "messages", label: "Tool results", unit: "result" },
{ id: "compactions", section: "messages", label: "Compactions", unit: "summary" },
{ id: "branch-summaries", section: "messages", label: "Branch summaries", unit: "summary" },
{ id: "bash-executions", section: "messages", label: "User shell runs", unit: "run" },
{ id: "custom-messages", section: "messages", label: "Custom messages", unit: "message" },
{ id: "media", section: "messages", label: "Images / media", unit: "image" }
] as const
type TokenRowId = (typeof TOKEN_ROWS)[number]["id"]
type TokenSection = (typeof TOKEN_ROWS)[number]["section"]
type TokenBreakdownRow = {
id: TokenRowId
section: TokenSection
label: string
tokens: number
count: number
unit: string
tools?: ToolTokenBreakdown[]
}
type ToolTokenBreakdown = {
toolName: string
tokens: number
count: number
}
type ContextTokenBreakdown = {
estimatedTokens: number
contextWindow: number | null
piTokens: number | null
rows: TokenBreakdownRow[]
}
type ContextReadMapData = {
linesPerCell: number
tokens?: ContextTokenBreakdown
files: FileEvidence[]
}
type ReadCall = {
path: string
offset?: number
limit?: number
}
const LINES_PER_CELL = 10
const CHARS_PER_TOKEN = 4
const ESTIMATED_IMAGE_CHARS = 4800
const EMPTY_GLYPH = " "
const BRAILLE_BASE = 0x2800
const LEFT_DOTS_BY_COUNT = [0, 64, 68, 70, 71] as const
const RIGHT_DOTS_BY_COUNT = [0, 128, 160, 176, 184] as const
function normalizePath(cwd: string, path: string) {
return isAbsolute(path) ? resolve(path) : resolve(cwd, path)
}
function displayPath(cwd: string, path: string) {
const rel = relative(cwd, path)
if (rel === "") return "."
if (!rel.startsWith("..") && !isAbsolute(rel)) return rel
return path
}
function countLines(text: string) {
const trimmed = text.replace(/\n+$/, "")
if (trimmed.length === 0) return 0
return trimmed.split("\n").length
}
function safeJsonStringify(value: unknown) {
try {
return JSON.stringify(value) ?? "undefined"
} catch {
return "[unserializable]"
}
}
function contextFilesPromptSection(contextFiles: Array<{ path: string; content: string }>) {
if (contextFiles.length === 0) return ""
let section = "\n\n\n\nProject-specific instructions and guidelines:\n\n"
for (const contextFile of contextFiles) {
section += `\n${contextFile.content}\n\n\n`
}
return `${section}\n`
}
function contentSize(content: unknown) {
if (typeof content === "string") return { textChars: content.length, images: 0 }
if (!Array.isArray(content)) return { textChars: 0, images: 0 }
let textChars = 0
let images = 0
for (const block of content) {
if (!isRecord(block)) continue
const contentBlock = block as { type?: unknown; text?: unknown }
if (contentBlock.type === "text" && typeof contentBlock.text === "string") textChars += contentBlock.text.length
if (contentBlock.type === "image") images++
}
return { textChars, images }
}
function convertedMessageTextChars(message: unknown) {
const converted = convertToLlm([message] as Parameters[0])
return converted.reduce((sum, item) => sum + contentSize(item.content).textChars, 0)
}
function addToolTotal(totals: Map, toolName: string, chars: number) {
const total = totals.get(toolName) ?? { chars: 0, count: 0 }
total.chars += Math.max(0, chars)
total.count++
totals.set(toolName, total)
}
function splitToolTotals(totals: Map, categoryTokens: number): ToolTokenBreakdown[] {
const entries = [...totals.entries()].sort((a, b) => b[1].chars - a[1].chars || a[0].localeCompare(b[0]))
const totalChars = entries.reduce((sum, [, total]) => sum + total.chars, 0)
let cumulativeChars = 0
let allocatedTokens = 0
return entries.map(([toolName, total], index) => {
cumulativeChars += total.chars
const boundary =
totalChars === 0 ? 0 : index === entries.length - 1 ? categoryTokens : Math.round((cumulativeChars / totalChars) * categoryTokens)
const tokens = Math.max(0, boundary - allocatedTokens)
allocatedTokens = boundary
return { toolName, tokens, count: total.count }
})
}
function collectTokenBreakdown(
pi: ExtensionAPI,
ctx: ExtensionCommandContext,
messages: ReturnType["messages"]
): ContextTokenBreakdown {
const totals = new Map()
const toolCallTotals = new Map()
const toolResultTotals = new Map()
for (const definition of TOKEN_ROWS) totals.set(definition.id, { chars: 0, count: 0 })
const add = (id: TokenRowId, chars: number, count = 0) => {
const total = totals.get(id)
if (!total) return
total.chars += Math.max(0, chars)
total.count += count
}
const options = ctx.getSystemPromptOptions()
const systemPrompt = ctx.getSystemPrompt()
const contextSection = contextFilesPromptSection(options.contextFiles ?? [])
const skillsSection = formatSkillsForPrompt(options.skills ?? [])
let baseSystemChars = systemPrompt.length
if (contextSection && systemPrompt.includes(contextSection)) {
baseSystemChars -= contextSection.length
add("startup-context", contextSection.length, options.contextFiles?.length ?? 0)
}
if (skillsSection && systemPrompt.includes(skillsSection)) {
baseSystemChars -= skillsSection.length
add("advertised-skills", skillsSection.length, (options.skills ?? []).filter(skill => !skill.disableModelInvocation).length)
}
add("system-base", baseSystemChars, baseSystemChars > 0 ? 1 : 0)
const activeTools = new Set(pi.getActiveTools())
const tools = pi
.getAllTools()
.filter(tool => activeTools.has(tool.name))
.map(tool => ({ name: tool.name, description: tool.description, parameters: tool.parameters }))
add("tool-definitions", tools.length > 0 ? safeJsonStringify(tools).length : 0, tools.length)
for (const message of messages) {
if (!isRecord(message)) continue
if (message.role === "user") {
const { textChars, images } = contentSize(message.content)
const skillBlock = parseSkillBlock(messageText(message))
if (skillBlock) {
const userChars = Math.min(textChars, skillBlock.userMessage?.length ?? 0)
add("loaded-skills", textChars - userChars, 1)
if (userChars > 0) add("user-messages", userChars, 1)
} else {
add("user-messages", textChars, 1)
}
add("media", images * ESTIMATED_IMAGE_CHARS, images)
continue
}
if (message.role === "assistant" && Array.isArray(message.content)) {
let thinkingChars = 0
let thinkingBlocks = 0
for (const block of message.content) {
if (!isRecord(block)) continue
if (block.type === "text" && typeof block.text === "string") add("assistant-text", block.text.length, 1)
if (block.type === "thinking" && typeof block.thinking === "string") {
thinkingChars += block.thinking.length
thinkingBlocks++
}
if (block.type === "toolCall") {
const toolName = typeof block.name === "string" ? block.name : "unknown"
const chars = toolName.length + safeJsonStringify(block.arguments).length
add("tool-calls", chars, 1)
addToolTotal(toolCallTotals, toolName, chars)
}
}
// Providers with hidden or summarized thinking (GPT reasoning models, Claude with
// summarized display) replay the full reasoning, not the visible block text.
// usage.reasoning carries the true token count when reported; max() because the
// replayed reasoning replaces the visible summary rather than adding to it.
const reasoningChars = numberValue(isRecord(message.usage) ? message.usage.reasoning : undefined) * CHARS_PER_TOKEN
const totalThinkingChars = Math.max(thinkingChars, reasoningChars)
if (totalThinkingChars > 0) add("assistant-thinking", totalThinkingChars, Math.max(thinkingBlocks, 1))
continue
}
if (message.role === "toolResult") {
const { textChars, images } = contentSize(message.content)
add("tool-results", textChars, 1)
addToolTotal(toolResultTotals, typeof message.toolName === "string" ? message.toolName : "unknown", textChars)
add("media", images * ESTIMATED_IMAGE_CHARS, images)
continue
}
if (message.role === "compactionSummary") {
add("compactions", convertedMessageTextChars(message), 1)
continue
}
if (message.role === "branchSummary") {
add("branch-summaries", convertedMessageTextChars(message), 1)
continue
}
if (message.role === "bashExecution" && !message.excludeFromContext) {
add("bash-executions", convertedMessageTextChars(message), 1)
continue
}
if (message.role === "custom") {
const { textChars, images } = contentSize(message.content)
add("custom-messages", textChars, 1)
add("media", images * ESTIMATED_IMAGE_CHARS, images)
}
}
const rows = TOKEN_ROWS.map(definition => {
const total = totals.get(definition.id) ?? { chars: 0, count: 0 }
const tokens = Math.ceil(total.chars / CHARS_PER_TOKEN)
const tools =
definition.id === "tool-calls"
? splitToolTotals(toolCallTotals, tokens)
: definition.id === "tool-results"
? splitToolTotals(toolResultTotals, tokens)
: undefined
return { ...definition, tokens, count: total.count, ...(tools && tools.length > 0 ? { tools } : {}) }
})
const usage = ctx.getContextUsage()
return {
estimatedTokens: rows.reduce((sum, row) => sum + row.tokens, 0),
contextWindow: usage?.contextWindow ?? ctx.model?.contextWindow ?? null,
piTokens: usage?.tokens ?? null,
rows
}
}
function currentLineCount(path: string, fallback: number) {
if (!existsSync(path)) return { totalLines: Math.max(1, fallback), missing: true }
return { totalLines: Math.max(1, countLines(readFileSync(path, "utf8"))), missing: false }
}
function getFile(
files: Map,
cwd: string,
path: string,
order: number,
fallbackLines: number,
totalLinesOverride?: number
) {
const normalized = normalizePath(cwd, path)
const existing = files.get(normalized)
if (existing) return existing
const { totalLines, missing } =
totalLinesOverride === undefined
? currentLineCount(normalized, fallbackLines)
: { totalLines: Math.max(1, totalLinesOverride), missing: !existsSync(normalized) }
const file: FileEvidence = {
path: normalized,
displayPath: displayPath(cwd, normalized),
totalLines,
missing,
sources: [],
order
}
files.set(normalized, file)
return file
}
function addEvidence(
files: Map,
cwd: string,
path: string,
source: EvidenceSource,
order: number,
totalLinesOverride?: number
) {
const file = getFile(files, cwd, path, order, source.range.endLine, totalLinesOverride)
file.sources.push(source)
}
function readSkillRanges(path: string) {
const fallback = { startLine: 1, endLine: 1 }
if (!existsSync(path)) return { frontmatter: fallback, body: fallback }
const lines = readFileSync(path, "utf8").split("\n")
if (lines[0]?.trim() !== "---") {
return { frontmatter: fallback, body: { startLine: 1, endLine: Math.max(1, lines.length) } }
}
const endIndex = lines.findIndex((line, index) => index > 0 && line.trim() === "---")
const bodyStart = endIndex === -1 ? 1 : Math.min(lines.length, endIndex + 2)
return {
frontmatter: { startLine: 1, endLine: endIndex === -1 ? 1 : endIndex + 1 },
body: { startLine: bodyStart, endLine: Math.max(bodyStart, lines.length) }
}
}
function messageText(message: unknown) {
if (!isRecord(message) || !("content" in message)) return ""
const content = (message as { content?: unknown }).content
if (typeof content === "string") return content
if (!Array.isArray(content)) return ""
return content
.map(block => {
if (!isRecord(block)) return ""
const textBlock = block as { type?: unknown; text?: unknown }
return textBlock.type === "text" && typeof textBlock.text === "string" ? textBlock.text : ""
})
.join("\n")
}
function hasMediaContent(message: unknown) {
if (!isRecord(message) || !("content" in message)) return false
const content = (message as { content?: unknown }).content
return Array.isArray(content) && content.some(block => isRecord(block) && (block as { type?: unknown }).type === "image")
}
function readCallFromBlock(block: unknown): ReadCall | undefined {
if (!isRecord(block)) return undefined
const toolCall = block as { type?: unknown; name?: unknown; id?: unknown; arguments?: unknown }
if (toolCall.type !== "toolCall" || toolCall.name !== "read" || typeof toolCall.id !== "string") return undefined
const args = isRecord(toolCall.arguments) ? (toolCall.arguments as { path?: unknown; offset?: unknown; limit?: unknown }) : undefined
if (typeof args?.path !== "string") return undefined
const call: ReadCall = { path: args.path }
if (typeof args.offset === "number") call.offset = args.offset
if (typeof args.limit === "number") call.limit = args.limit
return call
}
function collectContextReadMap(pi: ExtensionAPI, ctx: ExtensionCommandContext): ContextReadMapData {
const cwd = ctx.cwd
const files = new Map()
// ordinal = evidence recency; order = first-seen file order for stable groups.
let ordinal = 0
let order = 0
const systemPromptOptions = ctx.getSystemPromptOptions()
const skillRanges = new Map>()
const getSkillRanges = (path: string) => {
const cached = skillRanges.get(path)
if (cached) return cached
const ranges = readSkillRanges(path)
skillRanges.set(path, ranges)
return ranges
}
for (const contextFile of systemPromptOptions.contextFiles ?? []) {
const lineCount = Math.max(1, countLines(contextFile.content))
addEvidence(
files,
cwd,
contextFile.path,
{ kind: "startup-context", range: { startLine: 1, endLine: lineCount }, ordinal: ordinal++ },
order++
)
}
for (const skill of systemPromptOptions.skills ?? []) {
if (skill.disableModelInvocation) continue
addEvidence(
files,
cwd,
skill.filePath,
{ kind: "advertised-skill", range: getSkillRanges(skill.filePath).frontmatter, ordinal: ordinal++ },
order++
)
}
const readCalls = new Map()
const messages = buildSessionContext(ctx.sessionManager.getEntries() as SessionEntry[], ctx.sessionManager.getLeafId()).messages
for (const message of messages) {
if (!isRecord(message)) continue
const contextMessage = message as { role?: unknown; content?: unknown; toolName?: unknown; toolCallId?: unknown; isError?: unknown }
if (contextMessage.role === "assistant" && Array.isArray(contextMessage.content)) {
for (const block of contextMessage.content) {
const call = readCallFromBlock(block)
const id = isRecord(block) ? (block as { id?: unknown }).id : undefined
if (call && typeof id === "string") readCalls.set(id, call)
}
}
if (
contextMessage.role === "toolResult" &&
contextMessage.toolName === "read" &&
typeof contextMessage.toolCallId === "string" &&
!contextMessage.isError
) {
const call = readCalls.get(contextMessage.toolCallId)
if (!call) continue
if (hasMediaContent(message)) {
addEvidence(
files,
cwd,
call.path,
{ kind: "tool-read", range: { startLine: 1, endLine: 10 }, ordinal: ordinal++, media: true },
order++,
1
)
continue
}
const startLine = Math.max(1, call.offset ?? 1)
let lineCount = countLines(messageText(message))
if (typeof call.limit === "number") lineCount = Math.min(lineCount, call.limit)
if (lineCount > 0) {
addEvidence(
files,
cwd,
call.path,
{ kind: "tool-read", range: { startLine, endLine: startLine + lineCount - 1 }, ordinal: ordinal++ },
order++
)
}
}
if (contextMessage.role === "user") {
const skillBlock = parseSkillBlock(messageText(message))
if (skillBlock && !skillBlock.location.includes("${")) {
addEvidence(
files,
cwd,
skillBlock.location,
{ kind: "loaded-skill-body", range: getSkillRanges(skillBlock.location).body, ordinal: ordinal++ },
order++
)
}
}
}
const sorted = [...files.values()].sort((a, b) => {
const group = (file: FileEvidence) => {
if (file.sources.some(source => source.kind === "startup-context")) return 0
if (file.sources.some(source => source.kind === "advertised-skill")) return 1
return 2
}
const groupDiff = group(a) - group(b)
if (groupDiff !== 0) return groupDiff
if (group(a) < 2) return a.order - b.order
const recentDiff = Math.max(...b.sources.map(source => source.ordinal)) - Math.max(...a.sources.map(source => source.ordinal))
return recentDiff || a.displayPath.localeCompare(b.displayPath)
})
return { linesPerCell: LINES_PER_CELL, tokens: collectTokenBreakdown(pi, ctx, messages), files: sorted }
}
function sourcePriority(kind: EvidenceKind) {
if (kind === "tool-read") return 3
if (kind === "loaded-skill-body") return 2
return 1
}
function colorCell(text: string, kind: EvidenceKind, ordinal: number, maxOrdinal: number, theme: Theme) {
if (kind === "startup-context" || kind === "advertised-skill") return theme.fg("borderAccent", text)
if (kind === "loaded-skill-body") return theme.fg("accent", text)
if (maxOrdinal <= 0) return theme.fg("toolOutput", text)
const recency = ordinal / maxOrdinal
if (recency > 0.66) return theme.fg("warning", text)
if (recency > 0.33) return theme.fg("toolTitle", text)
return theme.fg("dim", text)
}
function barCell(text: string, theme: Theme) {
return theme.bg("selectedBg", text)
}
function readCountGlyph(leftCount: number, rightCount: number) {
if (leftCount === 0 && rightCount === 0) return EMPTY_GLYPH
const leftDots = LEFT_DOTS_BY_COUNT[Math.min(leftCount, LEFT_DOTS_BY_COUNT.length - 1)] ?? 0
const rightDots = RIGHT_DOTS_BY_COUNT[Math.min(rightCount, RIGHT_DOTS_BY_COUNT.length - 1)] ?? 0
return String.fromCodePoint(BRAILLE_BASE + leftDots + rightDots)
}
function renderBarCells(file: FileEvidence, maxOrdinal: number, theme: Theme) {
const cellCount = Math.max(1, Math.ceil(file.totalLines / LINES_PER_CELL))
const cells: string[] = []
for (let index = 0; index < cellCount; index++) {
const start = index * LINES_PER_CELL + 1
const middle = Math.min(file.totalLines, start + LINES_PER_CELL / 2 - 1)
const end = Math.min(file.totalLines, start + LINES_PER_CELL - 1)
const overlapping = file.sources.filter(source => source.range.startLine <= end && source.range.endLine >= start)
if (overlapping.length === 0) {
cells.push(barCell(theme.fg("dim", EMPTY_GLYPH), theme))
continue
}
const mediaCount = overlapping.filter(source => source.media).length
const leftCount =
mediaCount + overlapping.filter(source => !source.media && source.range.startLine <= middle && source.range.endLine >= start).length
const rightCount =
mediaCount + overlapping.filter(source => !source.media && source.range.startLine <= end && source.range.endLine > middle).length
const glyph = readCountGlyph(leftCount, rightCount)
const strongest = overlapping.reduce((best, source) => {
const priorityDiff = sourcePriority(source.kind) - sourcePriority(best.kind)
if (priorityDiff !== 0) return priorityDiff > 0 ? source : best
return source.ordinal > best.ordinal ? source : best
})
cells.push(osc8(barCell(colorCell(glyph, strongest.kind, strongest.ordinal, maxOrdinal, theme), theme), fileUrl(file.path, start)))
}
return cells
}
function wrapBar(cells: string[], width: number) {
const cellWidth = Math.max(1, width - 2)
const lines: string[] = []
for (let index = 0; index < cells.length; index += cellWidth) {
const isFirst = index === 0
const isLast = index + cellWidth >= cells.length
const prefix = isFirst ? "[" : "↳"
const suffix = isLast ? "]" : "↴"
lines.push(`${prefix}${cells.slice(index, index + cellWidth).join("")}${suffix}`)
}
return lines.length === 0 ? ["[]"] : lines
}
function truncatePath(path: string, width: number) {
if (path.length <= width) return path
if (width <= 5) return path.slice(0, width)
const marker = "…/…"
const available = width - marker.length
const head = Math.ceil(available / 2)
const tail = available - head
return `${path.slice(0, head)}${marker}${path.slice(path.length - tail)}`
}
function truncateLabel(label: string, width: number) {
if (label.length <= width) return label
if (width <= 1) return label.slice(0, width)
return `${label.slice(0, width - 1)}…`
}
function osc8(text: string, url: string) {
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`
}
function fileUrl(path: string, startLine?: number, endLine?: number) {
const { TERM_PROGRAM } = process.env
if (TERM_PROGRAM === "vscode" && startLine !== undefined) return `vscode://file${path}:${startLine}:1`
const lineFragment =
startLine === undefined ? "" : `#L${startLine}${endLine === undefined || endLine === startLine ? "" : `-L${endLine}`}`
return `${pathToFileURL(path).href}${lineFragment}`
}
function shortTokenCount(value: number) {
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(value >= 10_000_000 ? 0 : 1)}m`
if (value >= 1000) return `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`
return `${value}`
}
function tokenRowColor(id: TokenRowId, text: string, theme: Theme) {
if (id === "system-base") return theme.fg("borderAccent", text)
if (id === "startup-context") return theme.fg("accent", text)
if (id === "advertised-skills") return theme.fg("warning", text)
if (id === "tool-definitions") return theme.fg("toolTitle", text)
if (id === "user-messages") return theme.fg("success", text)
if (id === "loaded-skills") return theme.fg("mdLink", text)
if (id === "assistant-text") return theme.fg("toolOutput", text)
if (id === "assistant-thinking") return theme.fg("muted", text)
if (id === "tool-calls") return theme.fg("syntaxFunction", text)
if (id === "tool-results") return theme.fg("syntaxString", text)
if (id === "compactions") return theme.fg("error", text)
if (id === "branch-summaries") return theme.fg("syntaxType", text)
if (id === "bash-executions") return theme.fg("bashMode", text)
if (id === "custom-messages") return theme.fg("customMessageLabel", text)
return theme.fg("mdLinkUrl", text)
}
function cumulativeTokenBar(rows: TokenBreakdownRow[], capacity: number, width: number, theme: Theme) {
if (width <= 0) return ""
if (capacity === 0) return theme.fg("dim", "░".repeat(width))
let cumulativeTokens = 0
let usedCells = 0
const segments = rows
.map(row => {
cumulativeTokens += row.tokens
const boundary = Math.min(width, Math.round((cumulativeTokens / capacity) * width))
const cells = Math.max(0, boundary - usedCells)
usedCells = boundary
return tokenRowColor(row.id, "█".repeat(cells), theme)
})
.join("")
return `${segments}${theme.fg("dim", "░".repeat(Math.max(0, width - usedCells)))}`
}
function countLabel(row: TokenBreakdownRow) {
if (row.count === 0) return "-"
return `${row.count} ${row.unit}${row.count === 1 ? "" : "s"}`
}
function toolCountLabel(tool: ToolTokenBreakdown, unit: string) {
return `${tool.count} ${unit}${tool.count === 1 ? "" : "s"}`
}
function renderTokenBreakdown(tokens: ContextTokenBreakdown, width: number, theme: Theme) {
const estimatedPercent = tokens.contextWindow ? (tokens.estimatedTokens / tokens.contextWindow) * 100 : undefined
const piPercent = tokens.contextWindow && tokens.piTokens !== null ? (tokens.piTokens / tokens.contextWindow) * 100 : undefined
const capacity = tokens.contextWindow ? ` / ${shortTokenCount(tokens.contextWindow)} (${estimatedPercent?.toFixed(1)}%)` : ""
const meter =
tokens.piTokens === null
? "Pi meter unavailable"
: `Pi meter ${shortTokenCount(tokens.piTokens)}${piPercent === undefined ? "" : ` (${piPercent.toFixed(1)}%)`}`
const bar = cumulativeTokenBar(tokens.rows, tokens.contextWindow ?? tokens.estimatedTokens, Math.max(1, width - 2), theme)
const lines = [
...wrapTextWithAnsi(
`${theme.fg("accent", theme.bold("Context token breakdown"))} ≈${shortTokenCount(tokens.estimatedTokens)}${capacity} · ${meter}`,
width
),
...wrapTextWithAnsi(theme.fg("dim", "Estimated as chars ÷ 4 · each image ≈1.2k · provider framing/tokenizer overhead excluded"), width),
width > 2 ? `[${bar}]` : bar,
""
]
for (const section of ["prefix", "messages"] as const) {
const rows = tokens.rows.filter(row => row.section === section)
const sectionTokens = rows.reduce((sum, row) => sum + row.tokens, 0)
lines.push(
`${theme.bold(section === "prefix" ? "Prompt prefix" : "Effective messages")} ${theme.fg("dim", `≈${shortTokenCount(sectionTokens)}`)}`
)
if (width >= 76) {
const labelWidth = 23
const tokenWidth = 7
const percentWidth = 6
const countWidth = 13
for (const row of rows) {
const percent = tokens.estimatedTokens === 0 ? 0 : (row.tokens / tokens.estimatedTokens) * 100
const label = row.label.padEnd(labelWidth)
const amount = `≈${shortTokenCount(row.tokens)}`.padStart(tokenWidth)
const share = `${percent.toFixed(1)}%`.padStart(percentWidth)
const count = countLabel(row).padStart(countWidth)
lines.push(` ${tokenRowColor(row.id, "■", theme)} ${label} ${amount} ${share} ${theme.fg("dim", count)}`)
const tools = row.tools ?? []
for (const [toolIndex, tool] of tools.entries()) {
const toolLabel = truncateLabel(tool.toolName, labelWidth - 4).padEnd(labelWidth - 4)
const toolAmount = `≈${shortTokenCount(tool.tokens)}`.padStart(tokenWidth)
const toolPercent = tokens.estimatedTokens === 0 ? 0 : (tool.tokens / tokens.estimatedTokens) * 100
const toolShare = `${toolPercent.toFixed(1)}%`.padStart(percentWidth)
const toolCount = toolCountLabel(tool, row.unit).padStart(countWidth)
const branch = toolIndex === tools.length - 1 ? "└──" : "├──"
lines.push(` ${branch} ${toolLabel} ${theme.fg("dim", `${toolAmount} ${toolShare} ${toolCount}`)}`)
}
}
} else {
for (const row of rows) {
const percent = tokens.estimatedTokens === 0 ? 0 : (row.tokens / tokens.estimatedTokens) * 100
const stats = `≈${shortTokenCount(row.tokens)} · ${percent.toFixed(1)}% · ${countLabel(row)}`
lines.push(...wrapTextWithAnsi(` ${tokenRowColor(row.id, "■", theme)} ${row.label} ${theme.fg("dim", stats)}`, width))
const tools = row.tools ?? []
for (const [toolIndex, tool] of tools.entries()) {
const toolPercent = tokens.estimatedTokens === 0 ? 0 : (tool.tokens / tokens.estimatedTokens) * 100
const toolStats = `≈${shortTokenCount(tool.tokens)} · ${toolPercent.toFixed(1)}% · ${toolCountLabel(tool, row.unit)}`
const branch = toolIndex === tools.length - 1 ? "└──" : "├──"
lines.push(...wrapTextWithAnsi(` ${branch} ${tool.toolName} ${theme.fg("dim", toolStats)}`, width))
}
}
}
lines.push("")
}
return lines
}
function renderSnapshot(details: ContextReadMapData, width: number, theme: Theme) {
const tokenLines = details.tokens ? renderTokenBreakdown(details.tokens, width, theme) : []
if (details.files.length === 0) return [...tokenLines, "No file-backed context evidence."].join("\n")
const maxOrdinal = Math.max(0, ...details.files.flatMap(file => file.sources.map(source => source.ordinal)))
const wide = width >= 100
const pathWidth = 50
const lines = [
...tokenLines,
`${theme.fg("accent", theme.bold("Context read map"))} ${`One cell = ${details.linesPerCell} lines · ${details.files.length} files total`}`,
`${theme.fg("dim", "Read count:")} ${theme.fg("dim", "⣀")} 1 ${theme.fg("dim", "⣤")} 2 ${theme.fg("dim", "⣶")} 3 ${theme.fg("dim", "⣿")} 4+`,
`${theme.fg("dim", "Type:")} ${theme.fg("borderAccent", "system prompt")} ${theme.fg("accent", "skill loaded")} Read tool: [ ${theme.fg("warning", "recent")} / ${theme.fg("toolTitle", "mid")} / ${theme.fg("dim", "old")} ]`,
""
]
for (const file of details.files) {
const cells = renderBarCells(file, maxOrdinal, theme)
if (wide) {
const displayName = truncatePath(file.displayPath, pathWidth)
const padding = " ".repeat(Math.max(0, pathWidth - displayName.length))
const linkedName = `${padding}${osc8(displayName, fileUrl(file.path))}`
const name = file.missing ? theme.fg("warning", linkedName) : linkedName
const wrapped = wrapBar(cells, Math.max(10, width - pathWidth - 3))
lines.push(`${name} ${wrapped[0]}`)
for (const chunk of wrapped.slice(1)) lines.push(`${"".padEnd(pathWidth)} ${chunk}`)
} else {
const displayName = truncatePath(file.displayPath, Math.max(10, width - 2))
const linkedName = osc8(displayName, fileUrl(file.path))
const name = file.missing ? theme.fg("warning", linkedName) : linkedName
lines.push(name)
for (const chunk of wrapBar(cells, Math.max(10, width - 2))) lines.push(chunk)
}
}
return lines.join("\n")
}
function isContextReadMapData(value: unknown): value is ContextReadMapData {
if (!isRecord(value)) return false
const details = value as { files?: unknown; linesPerCell?: unknown }
return Array.isArray(details.files) && typeof details.linesPerCell === "number"
}
export function registerShowContextCommand(pi: ExtensionAPI) {
pi.registerEntryRenderer(CONTEXT_READ_MAP_ENTRY_TYPE, (entry, _options, theme) => {
const details = isContextReadMapData(entry.data) ? entry.data : undefined
const box = new Box(1, 1)
if (details) {
box.addChild({
render: width => renderSnapshot(details, width, theme).split("\n"),
invalidate() {}
})
} else {
box.addChild(new Text("Invalid context read map.", 0, 0))
}
return box
})
pi.registerCommand("show-context", {
description: "Show token and file coverage breakdowns for the current model context.",
async handler(_args, ctx) {
await ctx.waitForIdle()
const details = collectContextReadMap(pi, ctx)
pi.appendEntry(CONTEXT_READ_MAP_ENTRY_TYPE, details)
}
})
}