diff --git a/packages/cli/src/__tests__/pi-transcript.test.ts b/packages/cli/src/__tests__/pi-transcript.test.ts index e2c15c0779..dcf8ff403c 100644 --- a/packages/cli/src/__tests__/pi-transcript.test.ts +++ b/packages/cli/src/__tests__/pi-transcript.test.ts @@ -287,6 +287,165 @@ describe('Maka Pi TUI transcript', () => { ); }); + test('status line drops whole low-value segments on overflow, lowest rank first (#3421)', () => { + const richMeta = { + ...meta(), + modelContextWindow: 500_000, + usage: { + costUsd: 0.42, + cacheHitInput: 60, + cacheMissInput: 40, + contextRemaining: 480_000, + }, + }; + // Wide: everything renders. + const wide = stripAnsi(renderMakaPiStatusLine(richMeta, 120)); + assert.match(wide, /ctx 20k\/500k 4%/); + assert.match(wide, /\$0\.42/); + assert.match(wide, /cache 60%/); + assert.match(wide, /deepseek · \/tmp\/project/); + + // Below full width, cache drops before cost, and no segment is cut + // mid-token while any lower rank still survives. + const fullWidth = visibleWidth(wide); + const noCache = stripAnsi(renderMakaPiStatusLine(richMeta, fullWidth - 1)); + assert.doesNotMatch(noCache, /cache/); + assert.match(noCache, /\$0\.42/); + const noCost = stripAnsi( + renderMakaPiStatusLine(richMeta, fullWidth - 'cache 60% · '.length - 1), + ); + assert.doesNotMatch(noCost, /cache|\$0\.42/); + assert.match(noCost, /deepseek · \/tmp\/project/); + }); + + test('status line shortens cwd to its basename before dropping it (#3421)', () => { + const line = stripAnsi( + renderMakaPiStatusLine( + { + ...meta(), + cwd: '/very/long/nested/project-directory', + modelContextWindow: 500_000, + usage: { + costUsd: 0, + cacheHitInput: 1, + cacheMissInput: 1, + contextRemaining: 480_000, + }, + }, + // Room for title, mode, model, ctx and a short tail only. + 'Maka · Auto · deepseek-v4-flash · ctx 20k/500k 4% · project-directory'.length, + ), + ); + assert.doesNotMatch(line, /very\/long/); + assert.match(line, /project-directory/); + }); + + test('status line drops a drive-root cwd instead of rendering an empty basename (#3421)', () => { + const line = stripAnsi( + renderMakaPiStatusLine( + { + ...meta(), + cwd: 'C:\\', + modelContextWindow: 500_000, + usage: { + costUsd: 0.5, + cacheHitInput: 1, + cacheMissInput: 1, + contextRemaining: 480_000, + }, + }, + 40, + ), + ); + // C:\ has no useful basename; the segment drops cleanly rather than + // leaving an empty segment dangling after the separator. + assert.doesNotMatch(line, /C:\\/); + assert.doesNotMatch(line, /·\s*$/); + }); + + test('status line never drops mode, model, goal, or ctx at narrow widths (#3421)', () => { + const line = stripAnsi( + renderMakaPiStatusLine( + { + ...meta(), + permissionMode: 'bypass', + modelContextWindow: 500_000, + usage: { + costUsd: 9.99, + cacheHitInput: 1, + cacheMissInput: 1, + contextRemaining: 480_000, + }, + goal: { + goalId: 'goal-1', + revision: 1, + sessionId: 'session-1', + condition: 'Ship it', + setAt: Date.now() - 60_000, + iterations: 1, + maxIterations: 50, + consecutiveNoProgress: 0, + blockCap: 8, + tokenBudget: null, + tokensSpent: 0, + lastReason: null, + achievedAt: null, + pausedAt: null, + status: 'active' as const, + }, + }, + 75, + ), + ); + assert.match(line, /Full access/); + assert.match(line, /deepseek-v4-flash/); + assert.match(line, /goal 1\/50/); + assert.match(line, /ctx 20k\/500k 4%/); + assert.doesNotMatch(line, /\$9\.99|cache|deepseek ·|tmp\/project/); + }); + + test('status line compacts every critical segment before the narrow-width fallback (#3421)', () => { + const metadata = { + ...meta(), + permissionMode: 'bypass', + model: 'anthropic/claude-opus-4-1-very-long', + modelContextWindow: 500_000, + usage: { + costUsd: 9.99, + cacheHitInput: 1, + cacheMissInput: 1, + contextRemaining: 20_000, + }, + goal: { + goalId: 'goal-1', + revision: 1, + sessionId: 'session-1', + condition: 'Ship it', + setAt: Date.now() - 60_000, + iterations: 1, + maxIterations: 50, + consecutiveNoProgress: 0, + blockCap: 8, + tokenBudget: null, + tokensSpent: 0, + lastReason: null, + achievedAt: null, + pausedAt: null, + status: 'active' as const, + }, + }; + + for (const width of [40, 70, 80]) { + const line = stripAnsi(renderMakaPiStatusLine(metadata, width)); + assert.ok(visibleWidth(line) <= width); + assert.match(line, /Maka/); + assert.match(line, /Full/); + assert.match(line, /anthropic/); + assert.match(line, /(?:goal |g)1\/50/); + assert.match(line, /(?:ctx .*96%|c96%)/); + } + }); + test('keeps assistant text after a tool call visible after the tool block', () => { const state = createMakaPiTranscriptState(); appendUserPrompt(state, 'inspect the package'); diff --git a/packages/cli/src/__tests__/pi-tui-runner.test.ts b/packages/cli/src/__tests__/pi-tui-runner.test.ts index dfcbed939f..f21a769c36 100644 --- a/packages/cli/src/__tests__/pi-tui-runner.test.ts +++ b/packages/cli/src/__tests__/pi-tui-runner.test.ts @@ -3506,7 +3506,9 @@ describe('Maka Pi TUI runner', () => { }); test('restores switched session state from stored messages', async () => { - const terminal = new FakeTerminal(); + // 120 cols: the status line fits every segment, so the usage segments this + // test asserts (ctx, cache) are not priority-dropped (#3421). + const terminal = new FakeTerminal(120); const driver = new SlashCommandDriver( [fakeSessionSummary('session-2', '/repo')], new Map([ diff --git a/packages/cli/src/pi-transcript.ts b/packages/cli/src/pi-transcript.ts index b514cb5f78..89f8bce0be 100644 --- a/packages/cli/src/pi-transcript.ts +++ b/packages/cli/src/pi-transcript.ts @@ -45,6 +45,7 @@ import { } from '@maka/core/tool-result-status'; import { type ShellRunUpdate } from '@maka/core/events'; import { homedir } from 'node:os'; +import { basename } from 'node:path'; import type { MakaSessionDriver, MakaSideConversationParentStatus } from './session-driver.js'; import { BoundedChunkBuffer } from './bounded-chunk-buffer.js'; import { ansi } from './tui-ansi.js'; @@ -1336,38 +1337,69 @@ export function renderMakaPiStatusLine(metadata: MakaPiTranscriptMetadata, width return fitLine(ansi.dim(sideConversationStatusLineText(metadata.sideConversation)), safeWidth); } const sep = ansi.dim(' · '); - const parts: string[] = [ - ansi.bold(metadata.title), - ansi.dim(permissionModeLabel(metadata.permissionMode)), - ansi.dim(metadata.model), + // #3421: segments carry a dropRank so overflow drops whole low-value + // segments instead of cutting the chain mid-token from the right. + // Lower ranks drop first; segments without a rank never drop: + // title, permission mode and goal are safety-relevant, ctx is the + // context budget, model is the session's identity. + const parts: MakaPiStatusLineSegment[] = [ + { + text: ansi.bold(metadata.title), + compactRank: 1, + shortenedText: ansi.bold(fitLine(metadata.title, 7)), + }, + { + text: ansi.dim(permissionModeLabel(metadata.permissionMode)), + compactRank: 4, + shortenedText: ansi.dim(compactPermissionModeLabel(metadata.permissionMode)), + }, + { + text: ansi.dim(metadata.model), + compactRank: 0, + shortenedText: ansi.dim(fitLine(metadata.model, 11)), + }, ]; if (metadata.sideConversation?.view === 'parent') { - parts.push(ansi.dim(sideConversationStatusLineText(metadata.sideConversation))); + parts.push({ + text: ansi.dim(sideConversationStatusLineText(metadata.sideConversation)), + dropRank: 4, + }); } // #1064: omit thinking:default — it is noise before the user explicitly // changes the level. Only a non-default, explicitly set level shows. if (metadata.thinkingLevel) { - parts.push(ansi.dim(`thinking:${metadata.thinkingLevel}`)); + parts.push({ text: ansi.dim(`thinking:${metadata.thinkingLevel}`), dropRank: 3 }); } if (metadata.orchestrationMode === 'swarm') { - parts.push(ansi.accent('swarm')); + parts.push({ text: ansi.accent('swarm'), dropRank: 4 }); } else if (metadata.orchestrationMode === 'graph') { - parts.push(ansi.accent('graph')); + parts.push({ text: ansi.accent('graph'), dropRank: 4 }); } // An autonomous goal burns tokens between prompts; it must never be // invisible. Terminal goals show nothing (the desktop chip hides them too). if (metadata.goal && isLiveGoalStatus(metadata.goal.status)) { const text = goalStatusLineText(metadata.goal, Date.now()); + const compactStatus = + metadata.goal.status === 'active' ? '' : metadata.goal.status === 'paused' ? 'p' : 'w'; + const compactText = `g${compactStatus}${metadata.goal.iterations}/${metadata.goal.maxIterations}`; // paused gets warning salience: the loop stopped burning but stays armed // and resumable, which the user must not miss. waiting is a normal // transient between turns, so it stays dim like the other chrome. - parts.push( - metadata.goal.status === 'active' - ? ansi.accent(text) - : metadata.goal.status === 'paused' - ? ansi.yellow(text) - : ansi.dim(text), - ); + parts.push({ + text: + metadata.goal.status === 'active' + ? ansi.accent(text) + : metadata.goal.status === 'paused' + ? ansi.yellow(text) + : ansi.dim(text), + compactRank: 3, + shortenedText: + metadata.goal.status === 'active' + ? ansi.accent(fitLine(compactText, 8)) + : metadata.goal.status === 'paused' + ? ansi.yellow(fitLine(compactText, 8)) + : ansi.dim(fitLine(compactText, 8)), + }); } const usage = metadata.usage; // ctx segment: only show "used" when contextRemaining is available, since @@ -1380,32 +1412,118 @@ export function renderMakaPiStatusLine(metadata: MakaPiTranscriptMetadata, width const pct = Math.round((used / metadata.modelContextWindow) * 100); // #1064: color warning — yellow >80%, red >95%, dim otherwise. const ctxColor = pct > 95 ? ansi.red : pct > 80 ? ansi.yellow : ansi.dim; - parts.push( - ctxColor( + parts.push({ + text: ctxColor( `ctx ${formatTokenCount(used)}/${formatTokenCount(metadata.modelContextWindow)} ${pct}%`, ), - ); + compactRank: 2, + shortenedText: ctxColor(`c${pct}%`), + }); } else if (metadata.modelContextWindow !== undefined) { // #3371: the window is known but no usage has arrived yet (fresh session, // or the provider doesn't report per-step input tokens). Degrade // explicitly, pi-style, instead of hiding the segment silently — the user // can then tell "not measured yet" apart from "window unknown". - parts.push(ansi.dim(`ctx ?/${formatTokenCount(metadata.modelContextWindow)}`)); + parts.push({ + text: ansi.dim(`ctx ?/${formatTokenCount(metadata.modelContextWindow)}`), + compactRank: 2, + shortenedText: ansi.dim('c?'), + }); } if (usage) { if (usage.costUsd > 0) { - parts.push(ansi.dim(`$${formatCost(usage.costUsd)}`)); + parts.push({ text: ansi.dim(`$${formatCost(usage.costUsd)}`), dropRank: 1 }); } const totalCache = usage.cacheHitInput + usage.cacheMissInput; if (totalCache > 0) { const hitRate = Math.round((usage.cacheHitInput / totalCache) * 100); - parts.push(ansi.dim(`cache ${hitRate}%`)); + parts.push({ text: ansi.dim(`cache ${hitRate}%`), dropRank: 0 }); } } - parts.push(ansi.dim(metadata.connectionSlug)); + parts.push({ text: ansi.dim(metadata.connectionSlug), dropRank: 2 }); // #1064: shorten cwd to ~-relative path instead of the full path. - parts.push(ansi.dim(shortenCwd(metadata.cwd))); - return fitLine(parts.join(sep), safeWidth); + const cwd = shortenCwd(metadata.cwd); + // cwd degrades progressively (full → basename → dropped), after every + // ranked segment above but before the final truncation fallback. A drive + // root (C:\) or filesystem root has no useful basename — empty, or the + // path itself — so it drops directly instead of rendering an empty + // segment after the separator. + const cwdBase = basename(cwd); + parts.push({ + text: ansi.dim(cwd), + dropRank: 5, + shortenedText: cwdBase === '' || cwdBase === cwd ? undefined : ansi.dim(cwdBase), + }); + return fitStatusLine(parts, sep, safeWidth); +} + +interface MakaPiStatusLineSegment { + text: string; + /** Overflow drops whole segments lowest-rank-first; undefined never drops. */ + dropRank?: number; + /** Critical segments shorten in this order after every droppable segment is gone. */ + compactRank?: number; + /** Progressive fallback tried before this segment is dropped entirely. */ + shortenedText?: string; +} + +function fitStatusLine(segments: MakaPiStatusLineSegment[], sep: string, width: number): string { + const compactSep = ansi.dim(' '); + let activeSep = sep; + const lineWidth = (segs: MakaPiStatusLineSegment[]): number => + visibleWidth(segs.map((segment) => segment.text).join(activeSep)); + let kept = segments; + // Drop whole low-value segments, lowest rank first, re-checking after each + // rank so the fewest possible segments are sacrificed. + while (lineWidth(kept) > width) { + const dropRanks = kept.flatMap((segment) => + segment.dropRank !== undefined ? [segment.dropRank] : [], + ); + if (dropRanks.length > 0) { + const lowest = Math.min(...dropRanks); + // A droppable segment with a shortened form degrades before disappearing. + const shorten = kept.find( + (segment) => segment.dropRank === lowest && segment.shortenedText !== undefined, + ); + if (shorten) { + kept = kept.map((segment) => + segment === shorten + ? { ...segment, text: segment.shortenedText ?? segment.text, shortenedText: undefined } + : segment, + ); + } else { + kept = kept.filter((segment) => segment.dropRank !== lowest); + } + continue; + } + + const compactRanks = kept.flatMap((segment) => + segment.compactRank !== undefined && segment.shortenedText !== undefined + ? [segment.compactRank] + : [], + ); + if (compactRanks.length === 0) break; + const nextRank = Math.min(...compactRanks); + const shorten = kept.find( + (segment) => segment.compactRank === nextRank && segment.shortenedText !== undefined, + ); + if (!shorten) break; + kept = kept.map((segment) => + segment === shorten + ? { ...segment, text: segment.shortenedText ?? segment.text, shortenedText: undefined } + : segment, + ); + // Once critical values compact, reclaim separator chrome too. At widths + // below the compact critical seam, fitLine remains the honest fallback. + activeSep = compactSep; + } + return fitLine(kept.map((segment) => segment.text).join(activeSep), width); +} + +function compactPermissionModeLabel(mode: string): string { + if (mode === 'bypass') return 'Full'; + if (mode === 'explore') return 'Read'; + return 'Auto'; } function sideConversationStatusLineText(