Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -899,7 +899,7 @@
"react": 1
},
"importSpecifiers": 124,
"nonTriviaTokens": 15588
"nonTriviaTokens": 15582
},
"src/renderer/use-app-shell-composer-quotes.ts": {
"importDeclarations": 2,
Expand Down
37 changes: 15 additions & 22 deletions apps/desktop/src/renderer/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
isTaskSubmissionHardBlocked,
resolveTaskReadinessModelTarget,
transcriptReadingPosition,
type TranscriptHistoryGate,
} from './features/conversation';
import { deriveWorkspaceReadinessRecovery } from './workspace-readiness-recovery';
import { LiveTurnReconciler } from './live-turn-reconciler';
Expand Down Expand Up @@ -454,7 +455,7 @@ function AppShellContent({
// reads. A scroller can ask twice in one task — two scroll events before
// React has re-rendered anything — and a state read is still the old value
// for both of them.
const historyLoadPendingRef = useRef(false);
const historyLoadGateRef = useRef<TranscriptHistoryGate>({ pending: false });
const [transcriptTurnIndex, setTranscriptTurnIndex] = useState<{
sessionId: string;
throughSequence: number | null;
Expand Down Expand Up @@ -2537,36 +2538,28 @@ function AppShellContent({
setAnchor: sessionUiController.setTranscriptReadingAnchor,
});
}
async function loadTranscriptHistory(target: 'earlier' | 'latest', anchorTurnId?: string) {
function loadTranscriptHistory(target: 'earlier' | 'latest', anchorTurnId?: string) {
const controller = transcriptRangeRef.current;
const sessionId = activeId;
if (!controller || !sessionId || historyLoadPendingRef.current) return;
historyLoadPendingRef.current = true;
setHistoryLoadPendingSessionId(sessionId);
try {
if (target === 'earlier') {
await controller.loadBefore(DESKTOP_TRANSCRIPT_RANGE_MAX_BYTES, anchorTurnId);
} else await controller.loadLatest();
} catch (error) {
if (
activeIdRef.current !== sessionId ||
transcriptRangeRef.current !== controller
) {
return;
}
showSessionError(
if (!controller || !sessionId) return;
return transcriptReadingPosition.loadHistory({
gate: historyLoadGateRef.current,
request: { target, anchorTurnId },
controller,
maxBytes: DESKTOP_TRANSCRIPT_RANGE_MAX_BYTES,
isCurrent: () => activeIdRef.current === sessionId && transcriptRangeRef.current === controller,
setPending: (pending) => setHistoryLoadPendingSessionId((current) =>
pending ? sessionId : current === sessionId ? undefined : current),
onError: (error) => showSessionError(
sessionId,
desktopConversationCopy.actions.messageReadFailedTitle,
localizedShellErrorMessage(
error,
desktopConversationCopy.actions.operationFailedFallback,
uiLocale,
),
);
} finally {
historyLoadPendingRef.current = false;
setHistoryLoadPendingSessionId((current) => current === sessionId ? undefined : current);
}
),
});
}
const homeSurfaceActive =
navSelection.section === 'sessions' &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,52 @@ export function refreshTranscriptTurnLandmarks<T>(options: {
};
}

export interface TranscriptHistoryRequest {
readonly target: 'earlier' | 'latest';
readonly anchorTurnId?: string;
}

export interface TranscriptHistoryGate {
pending: boolean;
queued?: TranscriptHistoryRequest;
}

export async function loadTranscriptHistory(options: {
readonly gate: TranscriptHistoryGate;
readonly request: TranscriptHistoryRequest;
readonly controller: {
loadBefore(maxBytes: number, anchorTurnId?: string): Promise<void>;
loadLatest(): Promise<void>;
};
readonly maxBytes: number;
readonly isCurrent: () => boolean;
readonly setPending: (pending: boolean) => void;
readonly onError: (error: unknown) => void;
}): Promise<void> {
const { gate, request } = options;
if (gate.pending) {
// The scroller asks on every reader movement; dropping the request behind
// an in-flight load strands the reader until they move again.
if (request.target === 'latest' || gate.queued?.target !== 'latest') gate.queued = request;
return;
}
gate.pending = true;
options.setPending(true);
try {
if (request.target === 'earlier') {
await options.controller.loadBefore(options.maxBytes, request.anchorTurnId);
} else await options.controller.loadLatest();
} catch (error) {
if (options.isCurrent()) options.onError(error);
} finally {
gate.pending = false;
options.setPending(false);
const queued = gate.queued;
gate.queued = undefined;
if (queued && options.isCurrent()) void loadTranscriptHistory({ ...options, request: queued });
Comment thread
orangeCatDeveloper marked this conversation as resolved.
}
}

export function restoreSessionTranscriptRange<Message>(options: {
readonly sessionId?: string;
readonly searchTarget?: SearchTarget | null;
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/renderer/features/conversation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import {
captureTranscriptReadingAnchor,
currentTranscriptRange,
loadTranscriptHistory,
newestDurablePromptSequence,
refreshTranscriptTurnLandmarks,
restoreSessionTranscriptRange,
Expand All @@ -28,11 +29,13 @@ import {
export const transcriptReadingPosition = {
captureAnchor: captureTranscriptReadingAnchor,
currentRange: currentTranscriptRange,
loadHistory: loadTranscriptHistory,
newestDurablePromptSequence,
refreshLandmarks: refreshTranscriptTurnLandmarks,
restoreRange: restoreSessionTranscriptRange,
};

export type { TranscriptHistoryGate } from './controller/transcript-reading-position.js';
export {
deriveTaskReadinessNotice,
isTaskSubmissionHardBlocked,
Expand Down
9 changes: 4 additions & 5 deletions packages/ui/src/chat-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -744,12 +744,11 @@ export function ChatView(props: {
actionLabel={props.returnToLatest.label}
isPending={props.returnToLatest.isPending}
onReturnToLatest={async () => {
// Loading the latest range is the shell's job; putting the viewport
// on it is this view's, and setting the pin is the whole of it —
// the range that arrives afterwards is growth, and growth is
// already followed.
await props.returnToLatest?.onClick();
// Pin first: an unpinned scroller reports the Turn it is leaving
// as the reading anchor, and restoring that anchor would pull the
// arriving range straight back.
scrollAuthority.pinToTail();
await props.returnToLatest?.onClick();
}}
/>
) : null}
Expand Down
Loading