authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-14 11:41:36-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-14 11:41:36-07:00
logce905637099a8e9c8b2b9a0b651ca9b33debee24
tree6109e2c00443c58233e30759a7d3c2ebfb7c0a2b
parent31e68229d6561c2062e1ff69597897a665bcde62
signature Signed by PGP key B5690EEEBB952194

Preserve diff surface when toggling right panel (#3083)


3 files changed, 97 insertions(+), 12 deletions(-)

apps/web/src/components/ChatView.browser.tsx+82
......@@ -1546,6 +1546,18 @@ function dispatchChatNewShortcut(): void {
15461546 );
15471547}
15481548
1549function dispatchConfiguredDiffToggleShortcut(): void {
1550 window.dispatchEvent(
1551 new KeyboardEvent("keydown", {
1552 key: "g",
1553 shiftKey: true,
1554 altKey: true,
1555 bubbles: true,
1556 cancelable: true,
1557 }),
1558 );
1559}
1560
15491561function releaseModShortcut(key?: string): void {
15501562 window.dispatchEvent(
15511563 new KeyboardEvent("keyup", {
......@@ -3383,6 +3395,76 @@ describe("ChatView timeline estimator parity (full app)", () => {
33833395 }
33843396 });
33853397
3398 it("uses the configured diff toggle binding without discarding its surface", async () => {
3399 const mounted = await mountChatView({
3400 viewport: DEFAULT_VIEWPORT,
3401 snapshot: createSnapshotForTargetUser({
3402 targetMessageId: "msg-user-target-diff-hotkey" as MessageId,
3403 targetText: "diff hotkey target",
3404 }),
3405 configureFixture: (nextFixture) => {
3406 nextFixture.serverConfig = {
3407 ...nextFixture.serverConfig,
3408 keybindings: [
3409 {
3410 command: "diff.toggle",
3411 shortcut: {
3412 key: "g",
3413 metaKey: false,
3414 ctrlKey: false,
3415 shiftKey: true,
3416 altKey: true,
3417 modKey: false,
3418 },
3419 whenAst: {
3420 type: "not",
3421 node: { type: "identifier", name: "terminalFocus" },
3422 },
3423 },
3424 ],
3425 };
3426 },
3427 });
3428
3429 try {
3430 await waitForServerConfigToApply();
3431 dispatchConfiguredDiffToggleShortcut();
3432 await vi.waitFor(() => {
3433 expect(
3434 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, THREAD_REF),
3435 ).toEqual({
3436 isOpen: true,
3437 activeSurfaceId: "diff",
3438 surfaces: [{ id: "diff", kind: "diff" }],
3439 });
3440 });
3441
3442 dispatchConfiguredDiffToggleShortcut();
3443 await vi.waitFor(() => {
3444 expect(
3445 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, THREAD_REF),
3446 ).toEqual({
3447 isOpen: false,
3448 activeSurfaceId: "diff",
3449 surfaces: [{ id: "diff", kind: "diff" }],
3450 });
3451 });
3452
3453 dispatchConfiguredDiffToggleShortcut();
3454 await vi.waitFor(() => {
3455 expect(
3456 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, THREAD_REF),
3457 ).toEqual({
3458 isOpen: true,
3459 activeSurfaceId: "diff",
3460 surfaces: [{ id: "diff", kind: "diff" }],
3461 });
3462 });
3463 } finally {
3464 await mounted.cleanup();
3465 }
3466 });
3467
33863468 it("focuses the composer and inserts printable text typed from the page background", async () => {
33873469 const mounted = await mountChatView({
33883470 viewport: DEFAULT_VIEWPORT,
apps/web/src/components/ChatView.tsx+6-8
......@@ -2263,9 +2263,9 @@ export default function ChatView(props: ChatViewProps) {
22632263 // openness; when both fight, `selectActiveRightPanelKindWithUrl` lets
22642264 // diff win (URL is truth).
22652265 // - The two toggles below treat the panels as mutually exclusive: opening
2266 // one always tears down the other in BOTH the URL and the store. Without
2267 // this, e.g. clicking the browser button while diff is URL-pinned would
2268 // just toggle the (overridden) store value and look like a no-op.
2266 // one updates BOTH the URL and the store. Without this, e.g. clicking the
2267 // browser button while diff is URL-pinned would just toggle the
2268 // (overridden) store value and look like a no-op.
22692269 const onTogglePreview = useCallback(() => {
22702270 if (!activeThreadRef) return;
22712271 if (previewPanelOpen) {
......@@ -2303,11 +2303,9 @@ export default function ChatView(props: ChatViewProps) {
23032303 const diffPanelOpen = activeRightPanelKind === "diff";
23042304 if (!diffPanelOpen) {
23052305 onDiffPanelOpen?.();
2306 if (activeThreadRef) {
2307 useRightPanelStore.getState().open(activeThreadRef, "diff");
2308 }
2309 } else if (activeThreadRef) {
2310 useRightPanelStore.getState().closeSurface(activeThreadRef, "diff");
2306 }
2307 if (activeThreadRef) {
2308 useRightPanelStore.getState().toggle(activeThreadRef, "diff");
23112309 }
23122310 void navigate({
23132311 to: "/$environmentId/$threadId",
apps/web/src/rightPanelStore.test.ts+9-4
......@@ -111,11 +111,16 @@ describe("rightPanelStore", () => {
111111 expect(useRightPanelStore.getState().byThreadKey).toEqual({});
112112 });
113113
114 it("toggle opens then closes the same kind", () => {
115 useRightPanelStore.getState().toggle(refA, "preview");
116 expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("preview");
117 useRightPanelStore.getState().toggle(refA, "preview");
114 it("toggle hides the panel without discarding the active surface", () => {
115 useRightPanelStore.getState().toggle(refA, "diff");
116 expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff");
117 useRightPanelStore.getState().toggle(refA, "diff");
118118 expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBeNull();
119 expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({
120 isOpen: false,
121 activeSurfaceId: "diff",
122 surfaces: [{ id: "diff", kind: "diff" }],
123 });
119124 });
120125
121126 it("toggle to a different kind switches active", () => {