);
}
function connectionLabel(status: Status): string {
switch (status) {
case "connecting":
return "Connecting";
case "connected":
return "Connected";
case "reconnecting":
return "Reconnecting";
case "denied":
return "Access changed";
case "deleted":
return "Document deleted";
case "closed":
return "Disconnected";
}
}
export function ResearchWorkspace(
{
agent = true,
archivedAt,
canEdit,
canManage,
description,
descriptionRevision,
label,
repository,
room,
slug,
updatedAt,
userId,
workspaceId,
}: ResearchWorkspaceProps,
) {
let {
onDocumentAction,
onDocumentChanged,
onDocumentDeleted,
onRepositoryAccessChanged,
onResearchWorkspaceChanged,
onResearchWorkspaceLoaded,
onResearchWorkspacesRefresh,
} = useNavigationDocument();
let [detail, setDetail] = useState();
let detailRef = useRef(undefined);
let [error, setError] = useState();
let [refreshing, setRefreshing] = useState(false);
let [status, setStatus] = useState("connecting");
let [effectiveCanEdit, setEffectiveCanEdit] = useState(canEdit && !archivedAt);
let [effectiveCanManage, setEffectiveCanManage] = useState(canManage);
let [deleted, setDeleted] = useState(false);
let [capabilities, setCapabilities] = useState({ backgroundJobs: false, webResearch: false });
let [metadata, setMetadata] = useState({
archivedAt,
description,
descriptionRevision,
title: label,
slug,
updatedAt,
});
let metadataRef = useRef(metadata);
let requestGeneration = useRef(0);
let requestController = useRef(undefined);
let acceptedRevision = useRef(-1);
let researchRevision = useRef(-1);
let jobRevision = useRef(-1);
let [cancelling, setCancelling] = useState();
let [cancellationError, setCancellationError] = useState();
let accept = useCallback((next: Api.ResearchWorkspaceDetail) => {
if (next.workspace.revision < acceptedRevision.current) return;
acceptedRevision.current = next.workspace.revision;
researchRevision.current = Math.max(researchRevision.current, next.workspace.revision);
let merged = mergeResearchWorkspaceDetail(detailRef.current, next);
detailRef.current = merged;
setDetail(merged);
setError(undefined);
onResearchWorkspaceLoaded(
parentChannel(room, repository, metadataRef.current),
next.workspace,
);
}, [onResearchWorkspaceLoaded, repository, room]);
let refresh = useCallback(async () => {
let generation = ++requestGeneration.current;
requestController.current?.abort();
let controller = new AbortController();
requestController.current = controller;
setRefreshing(true);
try {
let next = await Api.researchWorkspace(room, workspaceId, controller.signal);
if (controller.signal.aborted || generation !== requestGeneration.current) return;
accept(next);
} catch (failure) {
if (controller.signal.aborted || generation !== requestGeneration.current) return;
setError(failure);
} finally {
if (generation === requestGeneration.current) setRefreshing(false);
}
}, [accept, room, workspaceId]);
let acceptMutation = useCallback((next: Api.ResearchWorkspaceDetail) => {
requestGeneration.current++;
requestController.current?.abort();
setRefreshing(false);
accept(next);
if (
researchRevision.current > next.workspace.revision
|| jobRevision.current > latestResearchJobRevision(next)
) void refresh();
}, [accept, refresh]);
let updateMetadata = useCallback((next: WorkspaceMetadata) => {
let metadata = newestDocumentMetadata(metadataRef.current, next);
metadataRef.current = metadata;
setMetadata(metadata);
rememberChannel(userId, { id: room, title: metadata.title, slug: metadata.slug }, repository);
onDocumentChanged(room, metadata);
let path = researchWorkspacePath(
repository.owner,
repository.name,
metadata.slug,
workspaceId,
);
if (location.pathname !== path) {
history.replaceState(null, "", `${path}${location.search}${location.hash}`);
}
}, [onDocumentChanged, repository, room, userId, workspaceId]);
useEffect(() => {
let next: WorkspaceMetadata = {
archivedAt,
description,
descriptionRevision,
title: label,
slug,
updatedAt,
};
next = newestDocumentMetadata(metadataRef.current, next);
metadataRef.current = next;
setMetadata(next);
setEffectiveCanEdit(canEdit && !archivedAt);
setEffectiveCanManage(canManage);
}, [
archivedAt,
canEdit,
canManage,
description,
descriptionRevision,
label,
room,
slug,
updatedAt,
]);
useEffect(() => {
void refresh();
return () => {
requestGeneration.current++;
requestController.current?.abort();
};
}, [refresh]);
useEffect(() => {
let socket = new Wire({
channelId: room,
onAuthenticationRequired: () => location.reload(),
onDeleted: () => {
setDeleted(true);
onDocumentDeleted(room);
},
onStatus: next => {
setStatus(next);
if (next === "connected") void refresh();
},
});
let off = [
socket.on("session:hello", frame => {
setEffectiveCanEdit(frame.canEdit && !frame.archivedAt);
setEffectiveCanManage(frame.canManage);
setCapabilities({
backgroundJobs: frame.backgroundJobs,
webResearch: frame.webResearch,
});
updateMetadata(frame);
onRepositoryAccessChanged();
onResearchWorkspacesRefresh(parentChannel(room, repository, metadataRef.current));
void refresh();
}),
socket.on("session:access", frame => {
setEffectiveCanEdit(frame.canEdit);
setEffectiveCanManage(frame.canManage);
onRepositoryAccessChanged();
void refresh();
}),
socket.on("session:channel", frame => {
if (frame.channelId !== room) return;
setEffectiveCanEdit(!frame.archivedAt && frame.canManage);
setEffectiveCanManage(frame.canManage);
updateMetadata(frame);
void refresh();
}),
socket.on("research:changed", frame => {
onResearchWorkspaceChanged(
parentChannel(room, repository, metadataRef.current),
frame.workspaceId,
frame.revision,
);
if (frame.workspaceId !== workspaceId || frame.revision <= researchRevision.current) return;
researchRevision.current = frame.revision;
void refresh();
}),
socket.on("job:changed", frame => {
if (frame.revision <= jobRevision.current) return;
jobRevision.current = frame.revision;
void refresh();
}),
];
return () => {
for (let unsubscribe of off) unsubscribe();
socket.dispose();
};
}, [
onDocumentDeleted,
onResearchWorkspaceChanged,
onResearchWorkspacesRefresh,
onRepositoryAccessChanged,
refresh,
repository,
room,
updateMetadata,
workspaceId,
]);
let workspaceArchivedAt = archivedAt ?? metadata.archivedAt;
let workspaceCanEdit = effectiveCanEdit && !workspaceArchivedAt;
let cancel = async (turnId: string) => {
if (cancelling || status !== "connected" || !workspaceCanEdit) return;
setCancelling(turnId);
setCancellationError(undefined);
try {
acceptMutation(await Api.cancelResearchWorkspaceTurn(room, workspaceId, turnId));
} catch (failure) {
setCancellationError(failure);
} finally {
setCancelling(undefined);
}
};
if (deleted) {
return (
This document was deleted.
);
}
let parentHref = documentPath(repository.owner, repository.name, metadata.slug);
let connected = status === "connected";
let connection = connectionLabel(status);
if (!detail && !error) {
return (
Opening research workspace...
);
}
if (!detail) {
return (
Cannot open research workspace
{error instanceof Error ? error.message : "The research workspace could not be loaded."}