fix(web): make right sidebar header toggle work across mobile, projects, and groups

This commit is contained in:
cynarAI 2026-10-22 12:04:39 +02:00
parent 35b7291660
commit 996af168ef

View File

@ -662,7 +662,7 @@ export function ShellPage() {
); );
const inProject = Boolean(activeSpace && !activeSpace.isDefault); const inProject = Boolean(activeSpace && !activeSpace.isDefault);
const coordinatorBot: Bot | undefined = useMemo(() => { const coordinatorBot: Bot | undefined = useMemo(() => {
if (!inProject || !activeSpace) return undefined; if ((!inProject && !inGroup) || !activeSpace) return undefined;
const leadId = const leadId =
snapshot?.leadBotId ?? snapshot?.leadBotId ??
activeGroup?.leadBotId ?? activeGroup?.leadBotId ??
@ -673,10 +673,12 @@ export function ShellPage() {
bots.find((b) => b.kind === "main" && b.spaceId === activeSpace.id) ?? bots.find((b) => b.kind === "main" && b.spaceId === activeSpace.id) ??
bots.find((b) => b.spaceId === activeSpace.id); bots.find((b) => b.spaceId === activeSpace.id);
if (!found) return undefined; if (!found) return undefined;
const isLegacyName = found.name === "Chief" || found.name === "Projektkoordinator"; const isLegacyName =
inProject && (found.name === "Chief" || found.name === "Projektkoordinator");
return isLegacyName ? { ...found, name: "Projektmanager" } : found; return isLegacyName ? { ...found, name: "Projektmanager" } : found;
}, [ }, [
inProject, inProject,
inGroup,
activeSpace, activeSpace,
snapshot?.leadBotId, snapshot?.leadBotId,
activeGroup?.leadBotId, activeGroup?.leadBotId,
@ -786,6 +788,8 @@ export function ShellPage() {
routeGroupId.current = groupId; routeGroupId.current = groupId;
const activeBotId = useRef<string | undefined>(inGroup ? undefined : active?.id); const activeBotId = useRef<string | undefined>(inGroup ? undefined : active?.id);
activeBotId.current = inGroup ? undefined : active?.id; activeBotId.current = inGroup ? undefined : active?.id;
const coordinatorBotIdRef = useRef<string | undefined>(coordinatorBot?.id);
coordinatorBotIdRef.current = coordinatorBot?.id;
const activeGroupId = useRef<string | undefined>(groupId); const activeGroupId = useRef<string | undefined>(groupId);
activeGroupId.current = groupId; activeGroupId.current = groupId;
const screenRequest = useRef(0); const screenRequest = useRef(0);
@ -1053,7 +1057,9 @@ export function ShellPage() {
return loadComputerScreen({ return loadComputerScreen({
load: () => rpc.computer.screenUrl({ botId: id }), load: () => rpc.computer.screenUrl({ botId: id }),
isCurrent: () => isCurrent: () =>
request === screenRequest.current && activeBotId.current === id && computerVisible.current, request === screenRequest.current &&
(activeBotId.current === id || coordinatorBotIdRef.current === id) &&
computerVisible.current,
commit: (screen) => { commit: (screen) => {
setScreenUrl(screen.url); setScreenUrl(screen.url);
setComputerError(screen.error); setComputerError(screen.error);
@ -1480,17 +1486,21 @@ export function ShellPage() {
void refreshThread(active.id).catch(() => undefined); void refreshThread(active.id).catch(() => undefined);
} else if (isComputerStatusEvent(event)) { } else if (isComputerStatusEvent(event)) {
void refreshComputerScreen(active.id).catch(() => undefined); void refreshComputerScreen(active.id).catch(() => undefined);
} else if (event.type === "thread.computer") { }
if (event.type === "thread.computer") {
agentPointerFeed.publish(event.botId, event.payload, event.createdAt); agentPointerFeed.publish(event.botId, event.payload, event.createdAt);
const screenRun = liveScreenRunId(event); }
if (screenRun) { const screenRun = liveScreenRunId(event);
setScreenRunId(screenRun); if (screenRun) {
setScreenActive(true); setScreenRunId(screenRun);
window.clearTimeout(screenActiveTimer.current); setScreenActive(true);
screenActiveTimer.current = window.setTimeout( window.clearTimeout(screenActiveTimer.current);
() => setScreenActive(false), screenActiveTimer.current = window.setTimeout(
SCREEN_ACTIVE_MS, () => setScreenActive(false),
); SCREEN_ACTIVE_MS,
);
if (computerRef.current?.state !== "running") {
void refreshComputerScreen(active.id).catch(() => undefined);
} }
} }
}, },
@ -1598,6 +1608,9 @@ export function ShellPage() {
// waiting_input: reconcile ask cards if a stale post-send refresh raced SSE. // waiting_input: reconcile ask cards if a stale post-send refresh raced SSE.
void refreshGroupThread(groupId).catch(() => undefined); void refreshGroupThread(groupId).catch(() => undefined);
} }
if (event.type === "thread.computer") {
agentPointerFeed.publish(event.botId, event.payload, event.createdAt);
}
}, },
}); });
return () => { return () => {
@ -1910,10 +1923,11 @@ export function ShellPage() {
const handleRunErrorPresented = useCallback((runId: string) => { const handleRunErrorPresented = useCallback((runId: string) => {
rememberSeenRunErrorId(runId); rememberSeenRunErrorId(runId);
}, []); }, []);
const groupComputerBotId = const groupComputerBotId = inGroup
inGroup && activeSnapshot?.leadBotId && bots.some((bot) => bot.id === activeSnapshot.leadBotId) ? (activeSnapshot?.leadBotId && bots.some((bot) => bot.id === activeSnapshot.leadBotId)
? activeSnapshot.leadBotId ? activeSnapshot.leadBotId
: undefined; : coordinatorBot?.id)
: undefined;
// A bot message without a sender speaks for the conversation's own bot, never as an anonymous one. // A bot message without a sender speaks for the conversation's own bot, never as an anonymous one.
const defaultSpeakerId = inGroup ? activeSnapshot?.leadBotId : active?.id; const defaultSpeakerId = inGroup ? activeSnapshot?.leadBotId : active?.id;
const transcriptMessages = useMemo(() => { const transcriptMessages = useMemo(() => {
@ -2031,6 +2045,7 @@ export function ShellPage() {
const workspaceFocusRef = useRef<{ threadId: string; focus: WorkspaceFocus } | null>(null); const workspaceFocusRef = useRef<{ threadId: string; focus: WorkspaceFocus } | null>(null);
const autoPanelRef = useRef(autoPanel); const autoPanelRef = useRef(autoPanel);
autoPanelRef.current = autoPanel; autoPanelRef.current = autoPanel;
const autoLayoutSplitRef = useRef(false);
const autoCloseTimer = useRef<number | undefined>(undefined); const autoCloseTimer = useRef<number | undefined>(undefined);
useEffect(() => () => window.clearTimeout(autoCloseTimer.current), []); useEffect(() => () => window.clearTimeout(autoCloseTimer.current), []);
// The pane follows the bot's work on focus edges only, so it never jumps while nothing changed. // The pane follows the bot's work on focus edges only, so it never jumps while nothing changed.
@ -2049,7 +2064,7 @@ export function ShellPage() {
if (!wideLayout) return; if (!wideLayout) return;
const paneState: PaneState = { const paneState: PaneState = {
open: open:
panel === "computer" panel === "computer" || panel === "browser"
? "screen" ? "screen"
: panel === "artifact" : panel === "artifact"
? "artifact" ? "artifact"
@ -2066,13 +2081,22 @@ export function ShellPage() {
if (closing === null || autoPanelRef.current !== closing) return; if (closing === null || autoPanelRef.current !== closing) return;
setPanel(null); setPanel(null);
setAutoPanel(null); setAutoPanel(null);
if (autoLayoutSplitRef.current) {
autoLayoutSplitRef.current = false;
dualSurface.setLayoutMode("stream");
}
}, AUTO_SCREEN_CLOSE_DELAY_MS); }, AUTO_SCREEN_CLOSE_DELAY_MS);
return; return;
} }
if (next.open === "artifact") setArtifactPane(workspaceFocus.artifact); if (next.open === "artifact") setArtifactPane(workspaceFocus.artifact);
if (next.open === "screen") setCanvasTab("browser");
const nextPanel: Panel = next.open === "screen" ? "computer" : next.open; const nextPanel: Panel = next.open === "screen" ? "computer" : next.open;
setPanel(nextPanel); setPanel(nextPanel);
setAutoPanel(next.auto ? nextPanel : null); setAutoPanel(next.auto ? nextPanel : null);
if (dualSurface.layoutMode === "stream") {
autoLayoutSplitRef.current = Boolean(next.auto);
dualSurface.setLayoutMode("split");
}
}, [workspaceFocus.key, activeSnapshot?.threadId, wideLayout]); }, [workspaceFocus.key, activeSnapshot?.threadId, wideLayout]);
const learnScreenPane = useCallback( const learnScreenPane = useCallback(
(choice: "opened" | "dismissed") => { (choice: "opened" | "dismissed") => {
@ -2091,6 +2115,7 @@ export function ShellPage() {
if (autoPanel === "computer" && panel === null && focus?.pane === "screen" && !focus.takeover) { if (autoPanel === "computer" && panel === null && focus?.pane === "screen" && !focus.takeover) {
learnScreenPane("dismissed"); learnScreenPane("dismissed");
} }
autoLayoutSplitRef.current = false;
setAutoPanel(null); setAutoPanel(null);
}, [panel]); }, [panel]);
const openArtifactBeside = useCallback((artifact: WorkspaceArtifact) => { const openArtifactBeside = useCallback((artifact: WorkspaceArtifact) => {
@ -3886,6 +3911,7 @@ export function ShellPage() {
panel && panel &&
(active || (active ||
activeGroup || activeGroup ||
coordinatorBot ||
panel === "create" || panel === "create" ||
panel === "create-group" || panel === "create-group" ||
panel === "browser") panel === "browser")
@ -3898,6 +3924,7 @@ export function ShellPage() {
panel && panel &&
(active || (active ||
activeGroup || activeGroup ||
coordinatorBot ||
panel === "create" || panel === "create" ||
panel === "create-group" || panel === "create-group" ||
panel === "browser"), panel === "browser"),
@ -4116,56 +4143,46 @@ export function ShellPage() {
layoutMode={showCanvasOnDesktop ? dualSurface.layoutMode : "stream"} layoutMode={showCanvasOnDesktop ? dualSurface.layoutMode : "stream"}
orientation={dualSurface.orientation} orientation={dualSurface.orientation}
onSelectMode={handleSelectLayoutMode} onSelectMode={handleSelectLayoutMode}
onToggleOrientation={dualSurface.toggleOrientation} onToggleOrientation={() => {
dualSurface.toggleOrientation();
if (!showCanvasOnDesktop) {
handleSelectLayoutMode("split");
}
}}
disabled={dualSurface.isMobile} disabled={dualSurface.isMobile}
className="hidden md:flex" className="hidden md:flex"
/> />
{active || groupComputerBotId ? ( {active || activeGroup || inProject || groupComputerBotId ? (
<button <button
type="button" type="button"
title={t`Agent computer`} title={t`Agent computer`}
aria-label={t`Agent computer`} aria-label={t`Agent computer`}
onClick={() => { onClick={() => {
if (inProject && !active) { if (isPanelVisible) {
if (showCanvasOnDesktop) {
setPanel(null);
dualSurface.setLayoutMode("stream");
} else {
dualSurface.setLayoutMode("split");
}
return;
}
// The computer belongs to a bot; from a group, open its lead's computer.
if (!active && groupComputerBotId) {
navigate(`/app/${groupComputerBotId}`);
setCanvasTab("browser");
setPanel("computer");
dualSurface.setLayoutMode("split");
return;
}
const isCurrentlyOpen =
(panel === "computer" || panel === "browser") &&
dualSurface.layoutMode !== "stream";
if (isCurrentlyOpen) {
setPanel(null); setPanel(null);
dualSurface.setLayoutMode("stream"); dualSurface.setLayoutMode("stream");
} else { return;
if (transcriptRunning) learnScreenPane("opened"); }
setCanvasTab("browser"); if (inProject && !active) {
setPanel("computer"); if (dualSurface.isMobile) {
dualSurface.setLayoutMode("split"); setCanvasTab("overview");
if (active) { setPanel("computer");
// Refresh run/computer so Take control isn't stuck on a stale busyBotName.
void refreshThread(active.id).catch(() => undefined);
} }
dualSurface.setLayoutMode("split");
return;
}
if (transcriptRunning) learnScreenPane("opened");
setCanvasTab(inProject ? "overview" : "browser");
setPanel("computer");
dualSurface.setLayoutMode("split");
if (active) {
// Refresh run/computer so Take control isn't stuck on a stale busyBotName.
void refreshThread(active.id).catch(() => undefined);
} else if (groupComputerBotId) {
void refreshComputerScreen(groupComputerBotId).catch(() => undefined);
} }
}} }}
data-active={ data-active={isPanelVisible ? "" : undefined}
(panel === "computer" || panel === "browser" || (inProject && !active)) &&
showCanvasOnDesktop
? ""
: undefined
}
className="app-no-drag rk-touch-target grid size-control-sm min-h-control-sm min-w-control-sm place-items-center rounded-lg hover:bg-accent data-active:bg-accent" className="app-no-drag rk-touch-target grid size-control-sm min-h-control-sm min-w-control-sm place-items-center rounded-lg hover:bg-accent data-active:bg-accent"
> >
<PanelRight strokeWidth={1.7} className="text-foreground/75 size-icon-sm" /> <PanelRight strokeWidth={1.7} className="text-foreground/75 size-icon-sm" />