Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ interface FileTreeViewProps {
// unmount (the side panel swaps the Files tab out for a file/diff viewer).
// Callers that stay mounted can omit it and keep component-local state.
viewStateKey?: string;
// Re-arms the file provider behind this tree. Rendered as "Try again" on the
// provider-unavailable panel, which otherwise offers no way out: the
// provider's acquisition is effect-driven and its inputs do not move when the
// machine comes back, so the failure outlives the outage that caused it.
onProviderRetry?: () => void;
}

type ControlledFileTreeViewProps = Omit<FileTreeViewProps, 'session' | 'autoCodeCollab'>;
Expand Down Expand Up @@ -445,6 +450,7 @@ function ControlledFileTreeView({
fileProviderMessage,
changedFilePaths,
viewStateKey,
onProviderRetry,
}: ControlledFileTreeViewProps) {
const { t } = useTranslation();
const scrollViewportRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -516,6 +522,16 @@ function ControlledFileTreeView({
description={
message ?? t('sessions.codeSession.files.unavailable', 'Files are unavailable.')
}
{...(onProviderRetry === undefined
? {}
: {
action: (
<Button variant="outline" size="sm" className="gap-1.5" onClick={onProviderRetry}>
<RefreshCw className="h-3.5 w-3.5" />
{t('sessions.codeSession.files.retry', 'Try again')}
</Button>
),
})}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5338,6 +5338,9 @@ const SessionDetail = ({
// Opening a file selects its viewer tab, which unmounts this tree. Key
// its expanded folders per session so returning to Files restores them.
viewStateKey={`session-files:${activeSession.id}`}
// "Files unavailable" is otherwise terminal: the provider re-arms on an
// offline -> online edge, and this is the way out of every other cause.
onProviderRetry={activeSessionCodeCollabFiles.reload}
/>
) : activeSidebarTab === 'pr' && latestPr && repoFullName && latestPrNumber ? (
<PrTabContainer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type CodeCollabFileIndexResource,
} from '@/lib/code-collab-file-index-cache';
import { describeCodeCollabError, warnCodeCollab } from '@/lib/code-collab-debug';
import { useMachineOnlineStatus } from '@/hooks/use-machine-online-status';
import {
CodeCollabSessionFileProvider,
codeCollabFileTreeToSessionFileEntries,
Expand Down Expand Up @@ -63,6 +64,13 @@ export type UseCodeCollabSessionFileProviderResult = {
readonly role?: CodeCollabRole;
readonly message?: string;
readonly error?: unknown;
/**
* Re-runs the file-index acquisition with every other input unchanged. A
* surface that renders `status: 'error'` or `'unavailable'` should offer it:
* the acquisition is effect-driven and its inputs do not move when a machine
* comes back, so without an explicit re-arm the failure is permanent.
*/
readonly reload?: () => void;
};

export const CODE_COLLAB_NO_OWNING_MACHINE_MESSAGE =
Expand Down Expand Up @@ -187,18 +195,33 @@ function useCodeCollabFileIndexLoadState(args: {
readonly ownerSessionId: SessionId;
readonly prepareTarget?: () => Promise<FileIndexTargetPlane>;
readonly loadLocalSnapshot?: () => Promise<LocalCodeCollabFileIndexSnapshot>;
/**
* Bumped to re-run the acquisition below with every other input unchanged.
* Without it a failed acquire is terminal: nothing else in `requestKey` moves
* when the machine comes back, so the effect never fires again and the file
* surfaces stay on "Files unavailable" until the component unmounts.
*/
readonly reloadNonce?: number;
}): HookFileIndexLoadState {
const [acquired, setAcquired] = useState<AcquiredFileIndexResource | null>(null);
const [fallbackState, setFallbackState] = useState<KeyedFileIndexLoadState | null>(null);
const [acquireError, setAcquireError] = useState<KeyedFileIndexLoadState | null>(null);
const { enabled, cache, workspaceId, ownerSessionId, prepareTarget, loadLocalSnapshot } = args;
const {
enabled,
cache,
workspaceId,
ownerSessionId,
prepareTarget,
loadLocalSnapshot,
reloadNonce = 0,
} = args;
const flockDocId =
enabled && workspaceId
? getCodeCollabFileIndexFlockDocId(workspaceId as WorkspaceId, ownerSessionId)
: null;
const requestKey = useMemo<object>(
() => ({ cache, flockDocId, loadLocalSnapshot, prepareTarget }),
[cache, flockDocId, loadLocalSnapshot, prepareTarget]
() => ({ cache, flockDocId, loadLocalSnapshot, prepareTarget, reloadNonce }),
[cache, flockDocId, loadLocalSnapshot, prepareTarget, reloadNonce]
);

useEffect(() => {
Expand Down Expand Up @@ -372,13 +395,32 @@ export function useCodeCollabSessionFileProvider(
: undefined,
[machineId, ownerSessionId, runtime, sessionId]
);
// Re-arm on a TRANSITION, never on a status. "Retry while the status is
// error" loops forever against a machine that is online and answering
// errors; an offline -> online edge fires at most once per outage, and every
// other cause is covered by the explicit `reload` below.
const [reloadNonce, setReloadNonce] = useState(0);
const reload = useCallback(() => setReloadNonce((nonce) => nonce + 1), []);
const machineOnlineStatus = useMachineOnlineStatus(machineId);
const sawMachineOfflineRef = useRef(false);
useEffect(() => {
if (machineOnlineStatus === 'offline') {
sawMachineOfflineRef.current = true;
return;
}
if (machineOnlineStatus === 'online' && sawMachineOfflineRef.current) {
sawMachineOfflineRef.current = false;
setReloadNonce((nonce) => nonce + 1);
}
}, [machineOnlineStatus]);
const fileIndexLoadState = useCodeCollabFileIndexLoadState({
enabled: options.enabled !== false && !!machineId,
cache: runtime?.codeCollabFileIndexCache ?? null,
workspaceId,
ownerSessionId,
prepareTarget: prepareFileIndexTarget,
loadLocalSnapshot: loadLocalFileIndexSnapshot,
reloadNonce,
});
const fileIndexSnapshot =
fileIndexLoadState.status === 'ready' ? fileIndexLoadState.snapshot : null;
Expand Down Expand Up @@ -494,7 +536,7 @@ export function useCodeCollabSessionFileProvider(
});
}, [materializedSharedState, providerTextState, role, rpcRuntime, sourceState]);

return useMemo<UseCodeCollabSessionFileProviderResult>(() => {
const result = useMemo<UseCodeCollabSessionFileProviderResult>(() => {
if (options.enabled === false) {
return disabledResult;
}
Expand Down Expand Up @@ -549,4 +591,11 @@ export function useCodeCollabSessionFileProvider(
role,
runtime,
]);

// `reload` rides on every branch, including `disabledResult` (a module
// constant, so it cannot carry a per-hook callback of its own).
return useMemo<UseCodeCollabSessionFileProviderResult>(
() => ({ ...result, reload }),
[reload, result]
);
}