Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/commands/compact/compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { ToolUseContext } from '../../Tool.js'
import type { LocalCommandCall } from '../../types/command.js'
import type { Message } from '../../types/message.js'
import { hasExactErrorMessage } from '../../utils/errors.js'
import { formatTokens } from '../../utils/format.js'
import { executePreCompactHooks } from '../../utils/hooks.js'
import { logError } from '../../utils/log.js'
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
Expand Down Expand Up @@ -78,7 +79,10 @@ export const call: LocalCommandCall = async (args, context) => {
return {
type: 'compact',
compactionResult: sessionMemoryResult,
displayText: buildDisplayText(context),
displayText: buildDisplayText(context, undefined, {
pre: sessionMemoryResult.preCompactTokenCount,
post: sessionMemoryResult.truePostCompactTokenCount,
}),
}
}
}
Expand Down Expand Up @@ -121,7 +125,10 @@ export const call: LocalCommandCall = async (args, context) => {
return {
type: 'compact',
compactionResult: result,
displayText: buildDisplayText(context, result.userDisplayMessage),
displayText: buildDisplayText(context, result.userDisplayMessage, {
pre: result.preCompactTokenCount,
post: result.truePostCompactTokenCount,
}),
}
} catch (error) {
if (abortController.signal.aborted) {
Expand Down Expand Up @@ -218,7 +225,10 @@ async function compactViaReactive(
...outcome.result!,
userDisplayMessage: combinedMessage,
},
displayText: buildDisplayText(context, combinedMessage),
displayText: buildDisplayText(context, combinedMessage, {
pre: outcome.result!.preCompactTokenCount,
post: outcome.result!.truePostCompactTokenCount,
}),
}
} finally {
context.setStreamMode?.('requesting')
Expand All @@ -231,17 +241,23 @@ async function compactViaReactive(
function buildDisplayText(
context: ToolUseContext,
userDisplayMessage?: string,
tokenCounts?: { pre?: number; post?: number },
): string {
const upgradeMessage = getUpgradeMessage('tip')
const expandShortcut = getShortcutDisplay(
'app:toggleTranscript',
'Global',
'ctrl+o',
)
const tokenLine =
tokenCounts?.pre !== undefined && tokenCounts.post !== undefined
? `Context: ~${formatTokens(tokenCounts.pre)} \u2192 ~${formatTokens(tokenCounts.post)} tokens`
: undefined
const dimmed = [
...(context.options.verbose
? []
: [`(${expandShortcut} to see full summary)`]),
...(tokenLine ? [tokenLine] : []),
...(userDisplayMessage ? [userDisplayMessage] : []),
...(upgradeMessage ? [upgradeMessage] : []),
]
Expand Down
15 changes: 7 additions & 8 deletions src/components/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import { collapseReadSearchGroups } from '../utils/collapseReadSearch.js';
import { collapseTeammateShutdowns } from '../utils/collapseTeammateShutdowns.js';
import { getGlobalConfig } from '../utils/config.js';
import { isEnvTruthy } from '../utils/envUtils.js';
import { isFullscreenEnvEnabled } from '../utils/fullscreen.js';
import { applyGrouping } from '../utils/groupToolUses.js';
import {
buildMessageLookups,
Expand All @@ -41,7 +40,6 @@ import {
updateMessageLookupsIncremental,
createAssistantMessage,
deriveUUID,
getMessagesAfterCompactBoundary,
getToolUseID,
getToolUseIDs,
hasUnresolvedHooksFromLookup,
Expand Down Expand Up @@ -543,12 +541,13 @@ const MessagesImpl = ({
// (this PR's core goal — full history in UI, filter only for the model).
// Also avoids a UUID mismatch: normalizeMessages derives new UUIDs, so
// projectSnippedView's check against original removedUuids would fail.
const compactAwareMessages =
verbose || isFullscreenEnvEnabled()
? normalizedMessages
: getMessagesAfterCompactBoundary(normalizedMessages, {
includeSnipped: true,
});
// Keep the full pre-compact history visible in every view (not just
// verbose/transcript). The model context is filtered separately in
// query.ts via getMessagesAfterCompactBoundary, so retaining the messages
// here only affects display: after /compact the user still sees the prior
// conversation (with the compact_boundary marker inline as the signal),
// while tokens are still freed from the API request.
const compactAwareMessages = normalizedMessages;

const messagesToShowNotTruncated = reorderMessagesInUI(
compactAwareMessages.filter(
Expand Down
4 changes: 4 additions & 0 deletions src/components/Spinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ type Props = {
overrideMessage?: string | null;
spinnerSuffix?: string | null;
verbose: boolean;
/** True while a compaction summary is streaming — shows a token progress bar. */
compactProgressActiveRef?: React.RefObject<boolean>;
hasActiveTools?: boolean;
/** Leader's turn has completed (no active query). Used to suppress stall-red spinner when only teammates are running. */
leaderIsIdle?: boolean;
Expand Down Expand Up @@ -110,6 +112,7 @@ function SpinnerWithVerbInner({
overrideMessage,
spinnerSuffix,
verbose,
compactProgressActiveRef,
hasActiveTools = false,
leaderIsIdle = false,
}: Props): React.ReactNode {
Expand Down Expand Up @@ -352,6 +355,7 @@ function SpinnerWithVerbInner({
spinnerSuffix={spinnerSuffix}
verbose={verbose}
columns={columns}
compactProgressActiveRef={compactProgressActiveRef}
hasRunningTeammates={hasRunningTeammates}
teammateTokens={teammateTokens}
foregroundedTeammate={foregroundedTeammate}
Expand Down
26 changes: 24 additions & 2 deletions src/components/Spinner/SpinnerAnimationRow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import figures from 'figures';
import * as React from 'react';
import { useMemo, useRef } from 'react';
import { Box, Text, useAnimationFrame, stringWidth, Byline } from '@anthropic/ink';
import { Box, Text, useAnimationFrame, stringWidth, Byline, ProgressBar } from '@anthropic/ink';
import { toInkColor } from '../../utils/ink.js';
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js';
import { formatDuration, formatNumber } from '../../utils/format.js';
Expand Down Expand Up @@ -48,6 +48,8 @@ export type SpinnerAnimationRowProps = {
spinnerSuffix?: string | null;
verbose: boolean;
columns: number;
/** True while a compaction summary is streaming — renders a token progress bar. */
compactProgressActiveRef?: React.RefObject<boolean>;

// Teammate-derived (computed by parent from tasks)
hasRunningTeammates: boolean;
Expand Down Expand Up @@ -86,6 +88,7 @@ export function SpinnerAnimationRow({
spinnerSuffix,
verbose,
columns,
compactProgressActiveRef,
hasRunningTeammates,
teammateTokens,
foregroundedTeammate,
Expand Down Expand Up @@ -283,7 +286,14 @@ export function SpinnerAnimationRow({
)
) : null;

return (
// Compaction progress bar. Summary length is unknown up front, so map
// streamed tokens through an asymptotic curve (approaches but never reaches
// 100%) so it always reads as forward progress. Bar disappears on compact_end.
const isCompacting = compactProgressActiveRef?.current === true;
const compactRatio = isCompacting ? 1 - Math.exp(-leaderTokens / 1200) : 0;
const compactBarWidth = Math.max(10, Math.min(30, columns - 20));

const spinnerRow = (
<Box ref={viewportRef} flexDirection="row" flexWrap="wrap" marginTop={1} width="100%">
<SpinnerGlyph
frame={frame}
Expand All @@ -304,6 +314,18 @@ export function SpinnerAnimationRow({
{status}
</Box>
);

if (!isCompacting) return spinnerRow;

return (
<Box flexDirection="column" width="100%">
{spinnerRow}
<Box flexDirection="row">
<ProgressBar ratio={compactRatio} width={compactBarWidth} fillColor={messageColor} />
<Text dimColor> {Math.round(compactRatio * 100)}%</Text>
</Box>
</Box>
);
}

function SpinnerModeGlyph({ mode }: { mode: SpinnerMode }): React.ReactNode {
Expand Down
4 changes: 4 additions & 0 deletions src/screens/REPL.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,7 @@ export function REPL({
// Ref instead of state to avoid triggering React re-renders on every
// streaming text_delta. The spinner reads this via its animation timer.
const responseLengthRef = useRef(0);
const compactProgressActiveRef = useRef(false);
// API performance metrics ref for ant-only spinner display (TTFT/OTPS).
// Accumulates metrics from all API requests in a turn for P50 aggregation.
const apiMetricsRef = useRef<
Expand Down Expand Up @@ -3011,11 +3012,13 @@ export function REPL({
break;
case 'compact_start':
setSpinnerMessage('Compacting conversation');
compactProgressActiveRef.current = true;
break;
case 'compact_end':
setSpinnerMessage(null);
setSpinnerColor(null);
setSpinnerShimmerColor(null);
compactProgressActiveRef.current = false;
break;
}
},
Expand Down Expand Up @@ -5953,6 +5956,7 @@ export function REPL({
spinnerTip={spinnerTip}
responseLengthRef={responseLengthRef}
apiMetricsRef={apiMetricsRef}
compactProgressActiveRef={compactProgressActiveRef}
overrideMessage={spinnerMessage}
spinnerSuffix={stopHookSpinnerSuffix}
verbose={verbose}
Expand Down
8 changes: 7 additions & 1 deletion src/services/compact/compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1230,7 +1230,13 @@ async function streamCompactSummary({
// Pass the compact context's abortController so user Esc aborts the
// fork — same signal the streaming fallback uses at
// `signal: context.abortController.signal` below.
overrides: { abortController: context.abortController },
// Share setResponseLength so streamed summary tokens drive the
// compaction progress bar (the fork path is the default; without
// this the bar sits at 0% until compact_end).
overrides: {
abortController: context.abortController,
shareSetResponseLength: true,
},
})
const assistantMsg = getLastAssistantMessage(result.messages)
const assistantText = assistantMsg
Expand Down
12 changes: 12 additions & 0 deletions src/utils/forkedAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,18 @@ export async function runForkedAgent({
if (isMessageDeltaStreamEvent(message)) {
const turnUsage = updateUsage({ ...EMPTY_USAGE }, message.event.usage)
totalUsage = accumulateUsage(totalUsage, turnUsage)
} else {
// Feed streamed text length into the (optionally shared) response
// length callback so callers with shareSetResponseLength can drive
// token-based UI (e.g. the compaction progress bar). No-op by default.
const event = (message as StreamEventMessage).event
if (
event.type === 'content_block_delta' &&
event.delta.type === 'text_delta'
) {
const textLength = event.delta.text.length
isolatedToolUseContext.setResponseLength(len => len + textLength)
}
}
continue
}
Expand Down