Skip to content

Commit 6f6c6c1

Browse files
refactor(web): drop the unreachable paint-only thread timeline path (#25787)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2ad853a commit 6f6c6c1

6 files changed

Lines changed: 62 additions & 190 deletions

File tree

‎apps/web/src/components/ChatView.logic.test.ts‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ import {
4545
rememberReadyThreadTimeline,
4646
resetHeldThreadTimeline,
4747
resolveThreadSwitchTimeline,
48-
threadKeysShareEnvironment,
4948
timelineHasEphemeralPreviewUrls,
5049
createLocalDispatchSnapshot,
5150
deriveCommittedServerUserMessageIds,
@@ -371,19 +370,18 @@ describe("resolveThreadSwitchTimeline", () => {
371370
activeThreadKey: "env-1:thread-a",
372371
nextEntries: [],
373372
}),
374-
).toEqual({ entries: ["a1", "a2"], displayThreadKey: "env-1:thread-a" });
373+
).toEqual(["a1", "a2"]);
375374
});
376375

377-
it("does not hold another environment's timeline across a jump", () => {
378-
expect(threadKeysShareEnvironment("env-1:thread-a", "env-2:thread-b")).toBe(false);
376+
it("does not paint the last-viewed thread for a destination with nothing remembered", () => {
377+
rememberReadyThreadTimeline(held);
379378
expect(
380379
resolveThreadSwitchTimeline({
381380
loading: true,
382381
activeThreadKey: "env-2:thread-b",
383382
nextEntries: [],
384-
lastReady: held,
385383
}),
386-
).toEqual({ entries: [], displayThreadKey: "env-2:thread-b" });
384+
).toEqual([]);
387385
});
388386

389387
it("does not remember a timeline that still has handoff blob previews", () => {

‎apps/web/src/components/ChatView.logic.ts‎

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import {
4646
} from "@t3tools/contracts";
4747
import { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup";
4848
import * as DateTime from "effect/DateTime";
49-
import { parseScopedThreadKey } from "@t3tools/client-runtime/environment";
5049
import { resolveAssetUrl } from "@t3tools/client-runtime/state/assets";
5150
import {
5251
squashAtomCommandFailure,
@@ -346,15 +345,12 @@ export function resolveDraftHeroState(input: {
346345
export type HeldThreadTimeline<T extends readonly unknown[]> = {
347346
threadKey: string | null;
348347
entries: T;
349-
markdownCwd?: string | null;
350-
workspaceRoot?: string | null;
351348
};
352349

353350
const MAX_REMEMBERED_THREAD_TIMELINES = 16;
354351

355352
let rememberedThreadTimelines = new Map<string, HeldThreadTimeline<readonly unknown[]>>();
356353
let rememberedThreadTimelineOrder: string[] = [];
357-
let lastReadyThreadKey: string | null = null;
358354

359355
function rememberThreadTimelineEntries(held: HeldThreadTimeline<readonly unknown[]>): void {
360356
if (held.threadKey === null) {
@@ -371,7 +367,6 @@ function rememberThreadTimelineEntries(held: HeldThreadTimeline<readonly unknown
371367
rememberedThreadTimelines.delete(evicted);
372368
}
373369
}
374-
lastReadyThreadKey = held.threadKey;
375370
}
376371

377372
export function rememberReadyThreadTimeline<T extends readonly unknown[]>(
@@ -392,61 +387,28 @@ export function peekRememberedThreadTimeline<T extends readonly unknown[]>(
392387
return (rememberedThreadTimelines.get(threadKey)?.entries as T | undefined) ?? null;
393388
}
394389

395-
export function peekHeldThreadTimeline<
396-
T extends readonly unknown[],
397-
>(): HeldThreadTimeline<T> | null {
398-
if (lastReadyThreadKey === null) {
399-
return null;
400-
}
401-
const held = rememberedThreadTimelines.get(lastReadyThreadKey);
402-
if (held === undefined || held.entries.length === 0) {
403-
return null;
404-
}
405-
return held as HeldThreadTimeline<T>;
406-
}
407-
408390
export function resetHeldThreadTimeline(): void {
409391
rememberedThreadTimelines = new Map();
410392
rememberedThreadTimelineOrder = [];
411-
lastReadyThreadKey = null;
412-
}
413-
414-
export function threadKeysShareEnvironment(left: string | null, right: string | null): boolean {
415-
if (left === null || right === null) {
416-
return false;
417-
}
418-
const leftRef = parseScopedThreadKey(left);
419-
const rightRef = parseScopedThreadKey(right);
420-
return leftRef !== null && rightRef !== null && leftRef.environmentId === rightRef.environmentId;
421-
}
422-
423-
export function isPaintOnlyThreadTimeline(
424-
displayThreadKey: string | null,
425-
activeThreadKey: string | null,
426-
): boolean {
427-
return (
428-
displayThreadKey !== null && activeThreadKey !== null && displayThreadKey !== activeThreadKey
429-
);
430393
}
431394

432395
export function resolveThreadSwitchTimeline<T extends readonly unknown[]>(input: {
433396
loading: boolean;
434397
activeThreadKey: string | null;
435398
nextEntries: T;
436399
rememberedForActive?: T | null;
437-
lastReady?: HeldThreadTimeline<T> | null;
438-
}): { entries: T; displayThreadKey: string | null } {
400+
}): T {
439401
if (input.nextEntries.length > 0) {
440-
return { entries: input.nextEntries, displayThreadKey: input.activeThreadKey };
402+
return input.nextEntries;
441403
}
442404

443405
const rememberedForActive =
444406
input.rememberedForActive ?? peekRememberedThreadTimeline<T>(input.activeThreadKey);
445407
if (input.loading && rememberedForActive !== null && rememberedForActive.length > 0) {
446-
return { entries: rememberedForActive, displayThreadKey: input.activeThreadKey };
408+
return rememberedForActive;
447409
}
448410

449-
return { entries: input.nextEntries, displayThreadKey: input.activeThreadKey };
411+
return input.nextEntries;
450412
}
451413

452414
export function startNewThreadForProject(

‎apps/web/src/components/ChatView.tsx‎

Lines changed: 47 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,7 @@ import { THREAD_FIND_BAR_RESERVED_HEIGHT } from "./chat/ThreadFindBar";
3232
import { usageLimitRecoveryBannerItem } from "./chat/UsageLimitRecoveryBanner";
3333
import { resolveDraftHeroState } from "./ChatView.logic";
3434
import { useQueuedRunEdit } from "./chat/useQueuedRunEdit";
35-
import {
36-
isPaintOnlyThreadTimeline,
37-
peekHeldThreadTimeline,
38-
peekRememberedThreadTimeline,
39-
resolveThreadSwitchTimeline,
40-
recallCheckoutIsRepo,
41-
rememberCheckoutIsRepo,
42-
} from "./ChatView.logic";
35+
import { recallCheckoutIsRepo, rememberCheckoutIsRepo } from "./ChatView.logic";
4336
import { useLogicalProjectEnvironments } from "../hooks/useLogicalProjectEnvironments";
4437
import { useStableCallback } from "../hooks/useStableCallback";
4538
import { useProjectScriptEditing } from "./chat/useProjectScriptEditing";
@@ -74,7 +67,6 @@ import {
7467
type ServerProvider,
7568
type ScopedThreadRef,
7669
type ThreadId,
77-
type RunId,
7870
ProviderInteractionMode,
7971
resolveIdleCompaction,
8072
RuntimeMode,
@@ -405,11 +397,6 @@ type ChatViewProps =
405397
draftId: DraftId;
406398
};
407399

408-
const EMPTY_HELD_TURN_DIFF_SUMMARIES: readonly never[] = [];
409-
const noopHeldTurnDiff = (_turnId: RunId, _filePath?: string) => {};
410-
const noopHeldRevert = (_targetTurnCount: number) => {};
411-
const noopHeldAttachment = (_attachment: ChatFileAttachment) => {};
412-
413400
export default function ChatView(props: ChatViewProps) {
414401
const {
415402
environmentId,
@@ -1469,17 +1456,7 @@ export default function ChatView(props: ChatViewProps) {
14691456
entry.kind === "message" ? [entry.message] : [],
14701457
);
14711458
const timelineEmpty = timelineEntries.length === 0 && !hasStreamedTimelineRows;
1472-
const displayedTimeline = resolveThreadSwitchTimeline({
1473-
loading: timelineEmpty && threadSyncPhase !== null,
1474-
activeThreadKey,
1475-
nextEntries: timelineEntries,
1476-
rememberedForActive: peekRememberedThreadTimeline<typeof timelineEntries>(activeThreadKey),
1477-
});
1478-
const displayedTimelineKey = displayedTimeline.displayThreadKey ?? routeThreadKey;
1479-
const paintOnlyDisplayedTimeline = isPaintOnlyThreadTimeline(
1480-
displayedTimeline.displayThreadKey,
1481-
activeThreadKey,
1482-
);
1459+
const displayedTimelineKey = activeThreadKey ?? routeThreadKey;
14831460
const displayedThreadRef = parseScopedThreadKey(displayedTimelineKey);
14841461
const {
14851462
worktreeSetup,
@@ -1624,9 +1601,6 @@ export default function ChatView(props: ChatViewProps) {
16241601
const activeProjectCwd = activeProject?.workspaceRoot ?? null;
16251602
const activeThreadWorktreePath = activeThread?.worktreePath ?? null;
16261603
const activeWorkspaceRoot = activeThreadWorktreePath ?? activeProjectCwd ?? undefined;
1627-
const heldPaintContext = paintOnlyDisplayedTimeline
1628-
? peekHeldThreadTimeline<typeof timelineEntries>()
1629-
: null;
16301604
const activeTerminalLaunchContext =
16311605
terminalUiLaunchContext?.threadId === activeThreadId ? terminalUiLaunchContext : null;
16321606
const liveIsGitRepo = gitStatusQuery.data?.isRepo;
@@ -2590,8 +2564,7 @@ export default function ChatView(props: ChatViewProps) {
25902564
const timelineSkills = activeProviderStatus
25912565
? resolveProviderSkillsForCwd(activeProviderStatus, gitCwd)
25922566
: EMPTY_PROVIDER_SKILLS;
2593-
const threadFindEnabled =
2594-
isServerThread && serverConfig?.threadFind === true && !paintOnlyDisplayedTimeline;
2567+
const threadFindEnabled = isServerThread && serverConfig?.threadFind === true;
25952568
const isThreadFindActive = useThreadFindOpen(activeThreadRef, threadFindEnabled);
25962569

25972570
useChatShortcuts({
@@ -2669,7 +2642,6 @@ export default function ChatView(props: ChatViewProps) {
26692642
serverProjection,
26702643
activeThread,
26712644
timelineMessages,
2672-
paintOnlyDisplayedTimeline,
26732645
phase,
26742646
isConnecting,
26752647
isSendBusy,
@@ -3249,126 +3221,79 @@ export default function ChatView(props: ChatViewProps) {
32493221
activeThreadKey={activeThreadKey}
32503222
syncing={threadSyncPhase !== null}
32513223
detailLoading={threadDetailLoading}
3252-
readyMarkdownCwd={gitCwd}
3253-
readyWorkspaceRoot={activeWorkspaceRoot ?? null}
3254-
citationRequest={paintOnlyDisplayedTimeline ? null : citationRequest}
3224+
citationRequest={citationRequest}
32553225
citationHistory={citationHistory}
3256-
{...(!paintOnlyDisplayedTimeline
3257-
? {
3258-
onCiteAssistantText: citeAssistantTextStable,
3259-
...(activeProject ? { onRunShellCommand: runShellCommandStable } : {}),
3260-
}
3261-
: {})}
3262-
isWorking={!paintOnlyDisplayedTimeline && isWorking}
3226+
onCiteAssistantText={citeAssistantTextStable}
3227+
{...(activeProject ? { onRunShellCommand: runShellCommandStable } : {})}
3228+
isWorking={isWorking}
32633229
runlessWorkActive={runlessWorkStartedAt !== null}
3264-
activeTurnInProgress={
3265-
!paintOnlyDisplayedTimeline && (isWorking || !latestRunSettled)
3266-
}
3267-
isCompacting={!paintOnlyDisplayedTimeline && isCompacting}
3230+
activeTurnInProgress={isWorking || !latestRunSettled}
3231+
isCompacting={isCompacting}
32683232
awaitingUser={
3269-
paintOnlyDisplayedTimeline
3270-
? null
3271-
: activePendingApproval !== null
3272-
? "approval"
3273-
: activePendingUserInput !== null ||
3274-
// A secret request has no runtime request; the shell carries it.
3275-
activeThreadShell?.hasPendingUserInput === true
3276-
? "input"
3277-
: null
3233+
activePendingApproval !== null
3234+
? "approval"
3235+
: activePendingUserInput !== null ||
3236+
// A secret request has no runtime request; the shell carries it.
3237+
activeThreadShell?.hasPendingUserInput === true
3238+
? "input"
3239+
: null
32783240
}
32793241
awaitingRequestId={
3280-
paintOnlyDisplayedTimeline
3281-
? null
3282-
: (activePendingApproval?.requestId ??
3283-
activePendingUserInput?.requestId ??
3284-
null)
3242+
activePendingApproval?.requestId ?? activePendingUserInput?.requestId ?? null
32853243
}
3286-
activeTurnStartedAt={paintOnlyDisplayedTimeline ? null : activeWorkStartedAt}
3287-
worktreeSetup={paintOnlyDisplayedTimeline ? null : worktreeSetup}
3244+
activeTurnStartedAt={activeWorkStartedAt}
3245+
worktreeSetup={worktreeSetup}
32883246
onCancelWorktreeSetup={onCancelWorktreeSetupStable}
3289-
{...(paintOnlyDisplayedTimeline
3290-
? {}
3291-
: {
3292-
retryableWorkspacePreparationRunIds,
3293-
onRetryWorkspacePreparation: onRetryWorkspacePreparationStable,
3294-
})}
3247+
retryableWorkspacePreparationRunIds={retryableWorkspacePreparationRunIds}
3248+
onRetryWorkspacePreparation={onRetryWorkspacePreparationStable}
32953249
{...(draftId
32963250
? { onWorktreeSetupWorkLocally: onWorktreeSetupWorkLocallyStable }
32973251
: {})}
32983252
{...(onOpenWorktreeSetupTerminal ? { onOpenWorktreeSetupTerminal } : {})}
3299-
isPreparingWorktree={!paintOnlyDisplayedTimeline && isPreparingWorktree}
3300-
footer={paintOnlyDisplayedTimeline ? null : timelineFooter}
3253+
isPreparingWorktree={isPreparingWorktree}
3254+
footer={timelineFooter}
33013255
listRef={legendListRef}
3302-
runs={paintOnlyDisplayedTimeline ? [] : (serverProjection?.runs ?? [])}
3303-
latestRun={paintOnlyDisplayedTimeline ? null : activeActivityRun}
3304-
announcedRun={
3305-
paintOnlyDisplayedTimeline
3306-
? null
3307-
: announcedRun(activeActivityRun, serverProjection?.runs ?? [])
3308-
}
3309-
detailBehindShell={
3310-
!paintOnlyDisplayedTimeline &&
3311-
isDetailBehindShell(activeLatestRun, activeThreadShell?.latestRun ?? null)
3312-
}
3313-
runningRunId={paintOnlyDisplayedTimeline ? null : activeRunningTurnId}
3314-
turnDiffSummaries={
3315-
paintOnlyDisplayedTimeline ? EMPTY_HELD_TURN_DIFF_SUMMARIES : turnDiffSummaries
3316-
}
3256+
runs={serverProjection?.runs ?? []}
3257+
latestRun={activeActivityRun}
3258+
announcedRun={announcedRun(activeActivityRun, serverProjection?.runs ?? [])}
3259+
detailBehindShell={isDetailBehindShell(
3260+
activeLatestRun,
3261+
activeThreadShell?.latestRun ?? null,
3262+
)}
3263+
runningRunId={activeRunningTurnId}
3264+
turnDiffSummaries={turnDiffSummaries}
33173265
activeThreadEnvironmentId={
33183266
displayedThreadRef?.environmentId ?? activeThread.environmentId
33193267
}
33203268
routeThreadKey={displayedTimelineKey}
33213269
displayThreadKey={displayedTimelineKey}
3322-
paintOnly={paintOnlyDisplayedTimeline}
33233270
announceTurnStatus={!isProviderSubagent}
3324-
onOpenTurnDiff={
3325-
paintOnlyDisplayedTimeline ? noopHeldTurnDiff : onOpenTurnDiffStable
3326-
}
3271+
onOpenTurnDiff={onOpenTurnDiffStable}
33273272
onOpenThread={onOpenRelatedThreadStable}
3328-
parentThreadLink={paintOnlyDisplayedTimeline ? null : parentThreadLink}
3329-
workflowOpening={paintOnlyDisplayedTimeline ? null : workflowOpening}
3330-
onForkFromRun={paintOnlyDisplayedTimeline ? async () => {} : onForkFromRunStable}
3273+
parentThreadLink={parentThreadLink}
3274+
workflowOpening={workflowOpening}
3275+
onForkFromRun={onForkFromRunStable}
33313276
onRollbackCheckpoint={onTimelineRollbackCheckpointStable}
3332-
supportsConversationRollback={
3333-
!paintOnlyDisplayedTimeline && supportsConversationRollback
3334-
}
3277+
supportsConversationRollback={supportsConversationRollback}
33353278
threadStartCheckpointReady={threadStartCheckpointReady}
33363279
availableRevertTurnCounts={availableRevertTurnCounts}
3337-
onRevertToTurnCount={
3338-
paintOnlyDisplayedTimeline ? noopHeldRevert : onRevertTimelineTurnStable
3339-
}
3340-
{...(!paintOnlyDisplayedTimeline
3341-
? {
3342-
onUseArtifactTemplate: insertArtifactTemplateStable,
3343-
onSendAppMessage: sendAppMessageStable,
3344-
}
3345-
: {})}
3280+
onRevertToTurnCount={onRevertTimelineTurnStable}
3281+
onUseArtifactTemplate={insertArtifactTemplateStable}
3282+
onSendAppMessage={sendAppMessageStable}
33463283
isRevertingCheckpoint={isRevertingCheckpoint}
33473284
onImageExpand={onExpandTimelineImageStable}
3348-
onFileOpen={
3349-
paintOnlyDisplayedTimeline ? noopHeldAttachment : openFileAttachmentStable
3350-
}
3351-
onFileDownload={
3352-
paintOnlyDisplayedTimeline ? noopHeldAttachment : downloadFileAttachmentStable
3353-
}
3354-
markdownCwd={
3355-
paintOnlyDisplayedTimeline
3356-
? (heldPaintContext?.markdownCwd ?? undefined)
3357-
: (gitCwd ?? undefined)
3358-
}
3285+
onFileOpen={openFileAttachmentStable}
3286+
onFileDownload={downloadFileAttachmentStable}
3287+
markdownCwd={gitCwd ?? undefined}
33593288
resolvedTheme={resolvedTheme}
33603289
timestampFormat={timestampFormat}
3361-
workspaceRoot={
3362-
paintOnlyDisplayedTimeline
3363-
? (heldPaintContext?.workspaceRoot ?? undefined)
3364-
: activeWorkspaceRoot
3365-
}
3290+
workspaceRoot={activeWorkspaceRoot}
33663291
skills={timelineSkills}
3367-
anchorMessageId={paintOnlyDisplayedTimeline ? null : timelineAnchorMessageId}
3292+
anchorMessageId={timelineAnchorMessageId}
33683293
onAnchorReady={onTimelineAnchorReadyStable}
33693294
onAnchorSizeChanged={onTimelineAnchorSizeChangedStable}
33703295
contentInsetEndAdjustment={composerTimelineInset}
3371-
liveFollowEnabled={!paintOnlyDisplayedTimeline && timelineLiveFollowEnabled}
3296+
liveFollowEnabled={timelineLiveFollowEnabled}
33723297
onIsAtEndChange={onIsAtEndChangeStable}
33733298
onContentOverflowChange={setTimelineOverflows}
33743299
onToolOutputCollapsedAtEnd={onToolOutputCollapsedAtEndStable}
@@ -3377,7 +3302,7 @@ export default function ChatView(props: ChatViewProps) {
33773302
hideEmptyPlaceholder={isDraftHeroState || threadDetailLoading}
33783303
emptyPlaceholderInset={composerReservedHeight}
33793304
topFadeEnabled={!hasTimelineTopBanner}
3380-
{...(paintOnlyDisplayedTimeline || threadHistoryControls === undefined
3305+
{...(threadHistoryControls === undefined
33813306
? {}
33823307
: { historyControls: threadHistoryControls })}
33833308
/>

0 commit comments

Comments
 (0)