Skip to content
Open
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
3 changes: 3 additions & 0 deletions crates/agent-gateway/web/src/app/GatewayApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4899,6 +4899,9 @@ export default function GatewayApp() {
liveStartIndex={transcriptLiveStartIndex}
activeTurnKey={displayedTranscript.activeTurnKey}
contentWidth={settings.customSettings.chatTranscript.width}
processDetailsExpanded={
settings.customSettings.chatTranscript.processDetailsExpanded
}
isViewportFollowing={transcriptFollow.isFollowing}
navRef={transcriptNavRef}
onAnchorUserRowChange={setActiveFloorKey}
Expand Down
96 changes: 59 additions & 37 deletions crates/agent-gateway/web/src/components/GatewayTranscript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import type { GitClient } from "@/lib/git/types";
import { DEFAULT_CHAT_TRANSCRIPT_WIDTH } from "@/lib/settings";
import { cn } from "@/lib/shared/utils";
import { estimateAssistantResponseRowHeight } from "@/lib/transcript-virtual/assistantResponseEstimate";
import { extractLiveRange } from "@/lib/transcript-virtual/liveRangeExtractor";
import { createLiveRowScrollAdjustPolicy } from "@/lib/transcript-virtual/liveScrollAdjustPolicy";
import {
Expand All @@ -46,9 +47,7 @@ import {
} from "@/lib/transcript-virtual/measurementsLru";
import {
CHECKPOINT_ROW_ESTIMATE_PX,
estimateAssistantRowHeight,
estimateUserRowHeight,
measureEstimateText,
} from "@/lib/transcript-virtual/rowEstimates";
import {
AssistantAvatar,
Expand Down Expand Up @@ -89,6 +88,7 @@ type GatewayTranscriptProps = {
// Key of the actively streaming turn (caret / live structural state).
activeTurnKey?: string | null;
contentWidth?: number;
processDetailsExpanded?: boolean;
// Whether the scroll-follow engine is attached to the bottom; gates the
// virtualizer's resize-compensation carve-out for live-row growth.
isViewportFollowing?: () => boolean;
Expand Down Expand Up @@ -1120,60 +1120,56 @@ const GatewayAssistantMessageActions = memo(function GatewayAssistantMessageActi
);
});

const rowEstimateCache = new WeakMap<TranscriptRow, number>();
type CachedRowEstimates = {
collapsed?: number;
expanded?: number;
};

const rowEstimateCache = new WeakMap<TranscriptRow, CachedRowEstimates>();

// Content-shaped height estimates: only ever used for rows the virtualizer
// has never measured (the measurement cache is keyed by row key and survives
// folding), but a shaped guess keeps scroll corrections small while reading
// unmeasured history.
function estimateRowHeight(row: TranscriptRow): number {
const cached = rowEstimateCache.get(row);
function estimateRowHeight(row: TranscriptRow, processDetailsExpanded: boolean): number {
const variant = processDetailsExpanded ? "expanded" : "collapsed";
const cached = rowEstimateCache.get(row)?.[variant];
if (cached !== undefined) {
return cached;
}
let estimate: number;
if (row.kind === "user") {
estimate = estimateUserRowHeight(row.text.length, row.attachments.length);
} else if (row.kind === "assistant") {
let proseChars = 0;
let codeLines = 0;
let codeFences = 0;
let toolCount = 0;
let thinkingCount = 0;
for (const round of row.rounds) {
for (const block of round.blocks) {
if (block.kind === "text") {
const measured = measureEstimateText(block.text);
proseChars += measured.proseChars;
codeLines += measured.codeLines;
codeFences += measured.codeFences;
} else if (block.kind === "thinking") {
thinkingCount += 1;
} else {
toolCount += 1;
}
}
}
estimate = estimateAssistantRowHeight({
proseChars,
codeLines,
codeFences,
toolCount,
thinkingCount,
});
estimate = estimateAssistantResponseRowHeight(row.rounds, processDetailsExpanded);
} else if (row.kind === "checkpoint") {
estimate = CHECKPOINT_ROW_ESTIMATE_PX;
} else {
estimate = 120;
}
rowEstimateCache.set(row, estimate);
const nextCache = rowEstimateCache.get(row) ?? {};
nextCache[variant] = estimate;
rowEstimateCache.set(row, nextCache);
return estimate;
}

function estimateVirtualItemHeight(item: GatewayTranscriptVirtualItem): number {
function estimateVirtualItemHeight(
item: GatewayTranscriptVirtualItem,
processDetailsExpanded: boolean,
): number {
if (item.kind === "loadRemoteHistory") return 44;
if (item.kind === "pendingBubble") return 56;
return estimateRowHeight(item.row);
return estimateRowHeight(item.row, processDetailsExpanded);
}

function buildGatewayTranscriptLayoutKey(
viewportWidth: number,
contentWidth: number,
processDetailsExpanded: boolean,
) {
const baseKey = buildTranscriptLayoutKey(viewportWidth, contentWidth);
if (!baseKey) return "";
return `${baseKey}:process-details-${processDetailsExpanded ? "expanded" : "collapsed"}`;
}

const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(props: {
Expand All @@ -1182,6 +1178,7 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
liveStartIndex: number;
activeTurnKey?: string | null;
contentWidth: number;
processDetailsExpanded: boolean;
scrollViewport: HTMLDivElement | null;
isViewportFollowing?: () => boolean;
navRef?: MutableRefObject<GatewayTranscriptNavHandle | null>;
Expand Down Expand Up @@ -1216,6 +1213,7 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
liveStartIndex,
activeTurnKey,
contentWidth,
processDetailsExpanded,
scrollViewport,
isViewportFollowing,
navRef,
Expand Down Expand Up @@ -1345,7 +1343,11 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
(conversationId && scrollViewport
? transcriptMeasurementsLru.restore(
conversationId,
buildTranscriptLayoutKey(scrollViewport.clientWidth, contentWidth),
buildGatewayTranscriptLayoutKey(
scrollViewport.clientWidth,
contentWidth,
processDetailsExpanded,
),
)
: null) ?? [],
);
Expand All @@ -1355,7 +1357,9 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
getScrollElement: () => scrollViewport,
estimateSize: (index) => {
const item = virtualItems[index];
return item ? estimateVirtualItemHeight(item) : TRANSCRIPT_ROW_ESTIMATED_HEIGHT;
return item
? estimateVirtualItemHeight(item, processDetailsExpanded)
: TRANSCRIPT_ROW_ESTIMATED_HEIGHT;
},
getItemKey: getTranscriptItemKey,
gap: TRANSCRIPT_ROW_GAP,
Expand All @@ -1376,6 +1380,16 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
rangeExtractor: (range) => extractLiveRange(range, forceMountStartRef.current),
});

const previousProcessDetailsExpandedRef = useRef(processDetailsExpanded);
useLayoutEffect(() => {
if (previousProcessDetailsExpandedRef.current === processDetailsExpanded) return;
previousProcessDetailsExpandedRef.current = processDetailsExpanded;
// The preference can change every unmounted history row's default height
// at once. Clear virtualizer measurements without changing React row keys,
// preserving any per-response manual disclosure choices on mounted rows.
transcriptVirtualizer.measure();
}, [processDetailsExpanded, transcriptVirtualizer]);

// TanStack exposes the resize-compensation predicate as an instance field,
// not an option; reassigning per render keeps the closure's inputs current.
// It only governs the detached reader — while virtually at the end, the
Expand Down Expand Up @@ -1577,7 +1591,11 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
if (!conversationId || !scrollViewport) return;
transcriptMeasurementsLru.save(
conversationId,
buildTranscriptLayoutKey(scrollViewport.clientWidth, contentWidth),
buildGatewayTranscriptLayoutKey(
scrollViewport.clientWidth,
contentWidth,
processDetailsExpanded,
),
transcriptVirtualizer.takeSnapshot(),
);
};
Expand Down Expand Up @@ -1717,6 +1735,7 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
>
<div className="group/assistant min-w-0 w-full max-w-full space-y-1">
<AssistantBubble
disclosureKey={`${conversationId ?? "shared"}:${row.key}`}
rounds={row.rounds}
showUsage={showUsage}
usageContextWindow={usageContextWindow}
Expand All @@ -1725,6 +1744,7 @@ const GatewayTranscriptListRegion = memo(function GatewayTranscriptListRegion(pr
renderMode={rowRenderMode(row)}
readOnly={readOnly}
redactToolContent={redactToolContent}
processDetailsExpanded={processDetailsExpanded}
workdir={workspaceRoot}
onOpenFileLink={onOpenFileLink}
/>
Expand Down Expand Up @@ -1795,6 +1815,7 @@ export function GatewayTranscript({
liveStartIndex = -1,
activeTurnKey = null,
contentWidth = DEFAULT_CHAT_TRANSCRIPT_WIDTH,
processDetailsExpanded = false,
isViewportFollowing,
navRef,
onAnchorUserRowChange,
Expand Down Expand Up @@ -1882,6 +1903,7 @@ export function GatewayTranscript({
liveStartIndex={liveStartIndex}
activeTurnKey={activeTurnKey}
contentWidth={contentWidth}
processDetailsExpanded={processDetailsExpanded}
scrollViewport={transcriptScrollViewport}
isViewportFollowing={isViewportFollowing}
navRef={navRef}
Expand Down
8 changes: 8 additions & 0 deletions crates/agent-gateway/web/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export const translations: Record<Locale, Record<string, string>> = {
"chat.stopGeneration": "停止生成",
"chat.thinking": "思考中",
"chat.thinkingProcess": "思考过程",
"chat.processDetails": "过程详情",
"chat.retryDetailsToggle": "重试详情 ({count})",
"chat.retryAttemptLabel": "第 {attempt}/{maxAttempts} 次重试",
"chat.runtime.thinkingOn": "Thinking 已开启",
Expand Down Expand Up @@ -1171,6 +1172,9 @@ export const translations: Record<Locale, Record<string, string>> = {
/* ── Settings System ── */
"settings.appearance": "外观主题",
"settings.appearanceDesc": "选择应用的颜色主题,偏好会自动保存。",
"settings.processDetailsExpanded": "默认展开过程详情",
"settings.processDetailsExpandedDesc":
"开启后,思考和工具调用默认展开;关闭后,生成正式回答时自动收起,仍可手动查看。",
"settings.systemProxy": "应用代理",
"settings.systemProxyEnable": "启用应用代理",
"settings.systemProxyDesc":
Expand Down Expand Up @@ -2323,6 +2327,7 @@ export const translations: Record<Locale, Record<string, string>> = {
"chat.stopGeneration": "Stop Generation",
"chat.thinking": "Thinking",
"chat.thinkingProcess": "Thinking Process",
"chat.processDetails": "Process details",
"chat.retryDetailsToggle": "Retry details ({count})",
"chat.retryAttemptLabel": "Retry {attempt}/{maxAttempts}",
"chat.runtime.thinkingOn": "Thinking enabled",
Expand Down Expand Up @@ -3391,6 +3396,9 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.appearance": "Appearance",
"settings.appearanceDesc":
"Choose the color theme for the application. Your preference will be saved automatically.",
"settings.processDetailsExpanded": "Expand process details by default",
"settings.processDetailsExpandedDesc":
"Keep reasoning and tool activity expanded by default. When disabled, they collapse as the final answer appears and can still be opened manually.",
"settings.systemProxy": "App Proxy",
"settings.systemProxyEnable": "Enable app proxy",
"settings.systemProxyDesc":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const MAX_MANUAL_PROCESS_DETAILS_STATES = 1_000;

const manualOpenByDisclosureKey = new Map<string, boolean>();

export function readManualProcessDetailsOpen(disclosureKey: string): boolean | undefined {
const value = manualOpenByDisclosureKey.get(disclosureKey);
if (value === undefined) return undefined;
manualOpenByDisclosureKey.delete(disclosureKey);
manualOpenByDisclosureKey.set(disclosureKey, value);
return value;
}

export function writeManualProcessDetailsOpen(disclosureKey: string, open: boolean): void {
manualOpenByDisclosureKey.delete(disclosureKey);
manualOpenByDisclosureKey.set(disclosureKey, open);
while (manualOpenByDisclosureKey.size > MAX_MANUAL_PROCESS_DETAILS_STATES) {
const oldestKey = manualOpenByDisclosureKey.keys().next().value;
if (oldestKey === undefined) break;
manualOpenByDisclosureKey.delete(oldestKey);
}
}
Loading
Loading