Skip to content

Commit 9789afd

Browse files
committed
fix(webapp): show chat open mode setting to everyone; test the real close reset
1 parent b0df83d commit 9789afd

4 files changed

Lines changed: 104 additions & 70 deletions

File tree

apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ import { useDashboardAgentOpenRequests } from "./dashboardAgentOpenRequest";
2020
import {
2121
agentHiddenContentClassName,
2222
FloatingAgentWindow,
23-
initialAgentMode,
24-
type DashboardAgentMode,
23+
useAgentPanelMode,
2524
} from "./panel-layout";
2625
import { nextPendingTurnChatId } from "./pending-turn";
2726
import { nextVisibleChat } from "./unread-counts";
@@ -124,13 +123,10 @@ export function DashboardAgent({
124123
}, [environment.id, initialUnreadWakes, initialUnreadWork]);
125124
// Every open starts from the account preference; in-chat switches (toggle, drag-to-dock)
126125
// are transient and never write it back.
127-
const [mode, setMode] = useState<DashboardAgentMode>(() => initialAgentMode(modePreference));
126+
const { mode, changeMode, resetToPreference, revertFullscreen } =
127+
useAgentPanelMode(modePreference);
128128
const fullscreen = mode === "fullscreen";
129129

130-
const changeMode = useCallback((next: DashboardAgentMode) => {
131-
setMode(next);
132-
}, []);
133-
134130
// Superseded localStorage keys; harmless to skip if storage is unavailable.
135131
useEffect(() => {
136132
try {
@@ -146,8 +142,8 @@ export function DashboardAgent({
146142
useEffect(() => {
147143
if (previousPathname.current === pathname) return;
148144
previousPathname.current = pathname;
149-
setMode((current) => (current !== "fullscreen" ? current : initialAgentMode(modePreference)));
150-
}, [pathname, modePreference]);
145+
revertFullscreen();
146+
}, [pathname, revertFullscreen]);
151147
const [newChatSeq, setNewChatSeq] = useState(0);
152148
const [requestedMessage, setRequestedMessage] = useState<
153149
{ text: string; seq: number } | undefined
@@ -180,12 +176,12 @@ export function DashboardAgent({
180176
visibleChat.current = null;
181177
// Any transient in-chat mode switch applied only until close; the next open
182178
// starts from the account preference again.
183-
setMode(initialAgentMode(modePreference));
179+
resetToPreference();
184180
setRequestedMessage(undefined);
185181
setOpenChatRequest(undefined);
186182
setWatchRequest(undefined);
187183
},
188-
[openPanel, modePreference]
184+
[openPanel, resetToPreference]
189185
);
190186

191187
const openChat = useCallback(
Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// @vitest-environment jsdom
2-
import { createElement, useCallback, useState } from "react";
2+
import { createElement } from "react";
33
import { createRoot, type Root } from "react-dom/client";
44
import { act } from "react-dom/test-utils";
55
import { afterEach, describe, expect, it } from "vitest";
6-
import { initialAgentMode, type DashboardAgentMode } from "./panel-layout";
6+
import { initialAgentMode, useAgentPanelMode, type DashboardAgentMode } from "./panel-layout";
77

88
describe("initialAgentMode", () => {
99
it("opens in the account preference when one is set", () => {
@@ -16,13 +16,6 @@ describe("initialAgentMode", () => {
1616
});
1717
});
1818

19-
type HarnessHandle = {
20-
mode: DashboardAgentMode;
21-
changeMode: (mode: DashboardAgentMode) => void;
22-
close: () => void;
23-
reopen: () => void;
24-
};
25-
2619
let container: HTMLDivElement | undefined;
2720
let root: Root | undefined;
2821

@@ -33,20 +26,13 @@ afterEach(() => {
3326
root = undefined;
3427
});
3528

36-
// Mirrors DashboardAgent.tsx's own state shape: mode starts from the preference, an
37-
// in-chat switch is transient (setMode only), and closing reverts to the preference —
38-
// so the next open starts clean regardless of what the last session left it on.
39-
function renderHarness(preference: DashboardAgentMode | undefined) {
40-
let latest!: HarnessHandle;
29+
// Renders the real hook DashboardAgent.tsx uses for its mode state — not a re-implementation
30+
// — so a regression in the actual reset wiring fails this test.
31+
function renderAgentPanelMode(preference: DashboardAgentMode | undefined) {
32+
let latest!: ReturnType<typeof useAgentPanelMode>;
4133
function Harness() {
42-
const [mode, setMode] = useState<DashboardAgentMode>(() => initialAgentMode(preference));
43-
44-
const changeMode = useCallback((next: DashboardAgentMode) => setMode(next), []);
45-
const close = useCallback(() => setMode(initialAgentMode(preference)), []);
46-
const reopen = useCallback(() => {}, []);
47-
48-
// oxlint-disable-next-line react/globals -- test harness capturing the latest state/handlers.
49-
latest = { mode, changeMode, close, reopen };
34+
// oxlint-disable-next-line react/globals -- test harness capturing the hook's return value.
35+
latest = useAgentPanelMode(preference);
5036
return null;
5137
}
5238
container = document.createElement("div");
@@ -62,18 +48,37 @@ function renderHarness(preference: DashboardAgentMode | undefined) {
6248
};
6349
}
6450

65-
describe("a transient in-chat mode switch reverts on close", () => {
66-
it("switching mode while open, then closing and reopening, lands back on the preference", () => {
67-
const harness = renderHarness("rightPanel");
51+
describe("useAgentPanelMode", () => {
52+
it("starts from the account preference", () => {
53+
const hook = renderAgentPanelMode("rightPanel");
54+
expect(hook.current.mode).toBe("rightPanel");
55+
});
56+
57+
it("defaults to floating when there is no preference", () => {
58+
const hook = renderAgentPanelMode(undefined);
59+
expect(hook.current.mode).toBe("floating");
60+
});
61+
62+
it("a transient changeMode applies immediately but resetToPreference (the close path) reverts it", () => {
63+
const hook = renderAgentPanelMode("rightPanel");
6864

69-
expect(harness.current.mode).toBe("rightPanel");
65+
act(() => hook.current.changeMode("fullscreen"));
66+
expect(hook.current.mode).toBe("fullscreen");
67+
68+
// This is exactly what DashboardAgent.tsx's setPanelOpen calls on close.
69+
act(() => hook.current.resetToPreference());
70+
expect(hook.current.mode).toBe("rightPanel");
71+
});
7072

71-
act(() => harness.current.changeMode("fullscreen"));
72-
expect(harness.current.mode).toBe("fullscreen");
73+
it("revertFullscreen (the pathname-change path) drops fullscreen but leaves other transient modes alone", () => {
74+
const hook = renderAgentPanelMode("floating");
7375

74-
act(() => harness.current.close());
75-
act(() => harness.current.reopen());
76+
act(() => hook.current.changeMode("fullscreen"));
77+
act(() => hook.current.revertFullscreen());
78+
expect(hook.current.mode).toBe("floating");
7679

77-
expect(harness.current.mode).toBe("rightPanel");
80+
act(() => hook.current.changeMode("rightPanel"));
81+
act(() => hook.current.revertFullscreen());
82+
expect(hook.current.mode).toBe("rightPanel");
7883
});
7984
});

apps/webapp/app/components/dashboard-agent/panel-layout.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Both class helpers apply to always-rendered wrappers, so switching display mode is a
22
// class change only and the open chat's transport, session and transcript survive it.
3-
import { useMemo, useRef, useState, type CSSProperties } from "react";
3+
import { useCallback, useMemo, useRef, useState, type CSSProperties } from "react";
44
import { createPortal } from "react-dom";
55
import { motion, type PanInfo } from "framer-motion";
66
import {
@@ -66,6 +66,34 @@ export function initialAgentMode(preference: DashboardAgentMode | undefined): Da
6666
return preference ?? "floating";
6767
}
6868

69+
/**
70+
* Owns the chat's mode state for `DashboardAgent`: starts from the account preference,
71+
* lets in-chat switches (toggle, drag-to-dock) apply transiently, and resets to the
72+
* preference on close or on leaving fullscreen. Extracted so this wiring — not just
73+
* `initialAgentMode` in isolation — is the exact code under test.
74+
*/
75+
export function useAgentPanelMode(modePreference: DashboardAgentMode | undefined) {
76+
const [mode, setMode] = useState<DashboardAgentMode>(() => initialAgentMode(modePreference));
77+
78+
const changeMode = useCallback((next: DashboardAgentMode) => {
79+
setMode(next);
80+
}, []);
81+
82+
// Any transient in-chat mode switch applied only until close; the next open starts
83+
// from the account preference again.
84+
const resetToPreference = useCallback(() => {
85+
setMode(initialAgentMode(modePreference));
86+
}, [modePreference]);
87+
88+
// Pathname changes must drop fullscreen back to the preference, but leave any other
89+
// transient mode (e.g. rightPanel) alone.
90+
const revertFullscreen = useCallback(() => {
91+
setMode((current) => (current !== "fullscreen" ? current : initialAgentMode(modePreference)));
92+
}, [modePreference]);
93+
94+
return { mode, changeMode, resetToPreference, revertFullscreen };
95+
}
96+
6997
function agentTakeoverClassName(fullscreen: boolean): string {
7098
return fullscreen ? "absolute inset-0 z-10 flex flex-col bg-background-bright" : "h-full";
7199
}

apps/webapp/app/routes/account._index/route.tsx

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,36 +1123,41 @@ export default function Page() {
11231123
</div>
11241124
</div>
11251125
</div>
1126-
1127-
<div className={cn(SETTINGS_SECTION_GAP, "w-full border-b border-grid-dimmed pb-3")}>
1128-
<Header2>Interface</Header2>
1129-
</div>
1130-
{sidebarContext && (
1131-
<div className="flex min-h-16 w-full items-center border-b border-grid-dimmed">
1132-
<div className="flex w-full items-center justify-between gap-4">
1133-
<div className={cn("flex-1", SETTINGS_ROW_TITLE_GAP)}>
1134-
<Label>App sidebar</Label>
1135-
<SettingsRowDescription>
1136-
Customize sidebar item visibility, order and rename favorites
1137-
</SettingsRowDescription>
1138-
</div>
1139-
<div className="flex flex-none items-center">
1140-
<CustomizeSidebarButton context={sidebarContext} />
1141-
</div>
1142-
</div>
1126+
</>
1127+
)}
1128+
{/* Always visible: the Interface section holds the chat-open-mode preference,
1129+
which every user (not just admins/flag holders) can set. */}
1130+
<div className={cn(SETTINGS_SECTION_GAP, "w-full border-b border-grid-dimmed pb-3")}>
1131+
<Header2>Interface</Header2>
1132+
</div>
1133+
{sidebarContext && (
1134+
<div className="flex min-h-16 w-full items-center border-b border-grid-dimmed">
1135+
<div className="flex w-full items-center justify-between gap-4">
1136+
<div className={cn("flex-1", SETTINGS_ROW_TITLE_GAP)}>
1137+
<Label>App sidebar</Label>
1138+
<SettingsRowDescription>
1139+
Customize sidebar item visibility, order and rename favorites
1140+
</SettingsRowDescription>
11431141
</div>
1144-
)}
1145-
<div className="flex min-h-16 w-full items-center border-b border-grid-dimmed">
1146-
<div className="flex w-full items-center justify-between gap-4">
1147-
<div className={cn("flex-1", SETTINGS_ROW_TITLE_GAP)}>
1148-
<Label>Ask Trigger chat</Label>
1149-
<SettingsRowDescription>Choose where the chat opens</SettingsRowDescription>
1150-
</div>
1151-
<div className="flex flex-none items-center">
1152-
<ChatOpenModePicker />
1153-
</div>
1142+
<div className="flex flex-none items-center">
1143+
<CustomizeSidebarButton context={sidebarContext} />
11541144
</div>
11551145
</div>
1146+
</div>
1147+
)}
1148+
<div className="flex min-h-16 w-full items-center border-b border-grid-dimmed">
1149+
<div className="flex w-full items-center justify-between gap-4">
1150+
<div className={cn("flex-1", SETTINGS_ROW_TITLE_GAP)}>
1151+
<Label>Ask Trigger chat</Label>
1152+
<SettingsRowDescription>Choose where the chat opens</SettingsRowDescription>
1153+
</div>
1154+
<div className="flex flex-none items-center">
1155+
<ChatOpenModePicker />
1156+
</div>
1157+
</div>
1158+
</div>
1159+
{showThemeSwitcher && (
1160+
<>
11561161
<div className="flex min-h-16 w-full items-center border-b border-grid-dimmed">
11571162
<div className="flex w-full items-center justify-between gap-4">
11581163
<div className={cn("flex-1", SETTINGS_ROW_TITLE_GAP)}>

0 commit comments

Comments
 (0)