| author | |
| committer | |
| log | ce905637099a8e9c8b2b9a0b651ca9b33debee24 |
| tree | 6109e2c00443c58233e30759a7d3c2ebfb7c0a2b |
| parent | 31e68229d6561c2062e1ff69597897a665bcde62 |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 97 insertions(+), 12 deletions(-)
apps/web/src/components/ChatView.browser.tsx+82| ... | @@ -1546,6 +1546,18 @@ function dispatchChatNewShortcut(): void { | ... | @@ -1546,6 +1546,18 @@ function dispatchChatNewShortcut(): void { |
| 1546 | ); | 1546 | ); |
| 1547 | } | 1547 | } |
| 1548 | 1548 | ||
| 1549 | function 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 | |||
| 1549 | function releaseModShortcut(key?: string): void { | 1561 | function releaseModShortcut(key?: string): void { |
| 1550 | window.dispatchEvent( | 1562 | window.dispatchEvent( |
| 1551 | new KeyboardEvent("keyup", { | 1563 | new KeyboardEvent("keyup", { |
| ... | @@ -3383,6 +3395,76 @@ describe("ChatView timeline estimator parity (full app)", () => { | ... | @@ -3383,6 +3395,76 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 3383 | } | 3395 | } |
| 3384 | }); | 3396 | }); |
| 3385 | 3397 | ||
| 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 | |||
| 3386 | it("focuses the composer and inserts printable text typed from the page background", async () => { | 3468 | it("focuses the composer and inserts printable text typed from the page background", async () => { |
| 3387 | const mounted = await mountChatView({ | 3469 | const mounted = await mountChatView({ |
| 3388 | viewport: DEFAULT_VIEWPORT, | 3470 | viewport: DEFAULT_VIEWPORT, |
apps/web/src/components/ChatView.tsx+6-8| ... | @@ -2263,9 +2263,9 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2263,9 +2263,9 @@ export default function ChatView(props: ChatViewProps) { |
| 2263 | // openness; when both fight, `selectActiveRightPanelKindWithUrl` lets | 2263 | // openness; when both fight, `selectActiveRightPanelKindWithUrl` lets |
| 2264 | // diff win (URL is truth). | 2264 | // diff win (URL is truth). |
| 2265 | // - The two toggles below treat the panels as mutually exclusive: opening | 2265 | // - 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 | 2266 | // one updates BOTH the URL and the store. Without this, e.g. clicking the |
| 2267 | // this, e.g. clicking the browser button while diff is URL-pinned would | 2267 | // browser button while diff is URL-pinned would just toggle the |
| 2268 | // just toggle the (overridden) store value and look like a no-op. | 2268 | // (overridden) store value and look like a no-op. |
| 2269 | const onTogglePreview = useCallback(() => { | 2269 | const onTogglePreview = useCallback(() => { |
| 2270 | if (!activeThreadRef) return; | 2270 | if (!activeThreadRef) return; |
| 2271 | if (previewPanelOpen) { | 2271 | if (previewPanelOpen) { |
| ... | @@ -2303,11 +2303,9 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2303,11 +2303,9 @@ export default function ChatView(props: ChatViewProps) { |
| 2303 | const diffPanelOpen = activeRightPanelKind === "diff"; | 2303 | const diffPanelOpen = activeRightPanelKind === "diff"; |
| 2304 | if (!diffPanelOpen) { | 2304 | if (!diffPanelOpen) { |
| 2305 | onDiffPanelOpen?.(); | 2305 | onDiffPanelOpen?.(); |
| 2306 | if (activeThreadRef) { | 2306 | } |
| 2307 | useRightPanelStore.getState().open(activeThreadRef, "diff"); | 2307 | if (activeThreadRef) { |
| 2308 | } | 2308 | useRightPanelStore.getState().toggle(activeThreadRef, "diff"); |
| 2309 | } else if (activeThreadRef) { | ||
| 2310 | useRightPanelStore.getState().closeSurface(activeThreadRef, "diff"); | ||
| 2311 | } | 2309 | } |
| 2312 | void navigate({ | 2310 | void navigate({ |
| 2313 | to: "/$environmentId/$threadId", | 2311 | to: "/$environmentId/$threadId", |
apps/web/src/rightPanelStore.test.ts+9-4| ... | @@ -111,11 +111,16 @@ describe("rightPanelStore", () => { | ... | @@ -111,11 +111,16 @@ describe("rightPanelStore", () => { |
| 111 | expect(useRightPanelStore.getState().byThreadKey).toEqual({}); | 111 | expect(useRightPanelStore.getState().byThreadKey).toEqual({}); |
| 112 | }); | 112 | }); |
| 113 | 113 | ||
| 114 | it("toggle opens then closes the same kind", () => { | 114 | it("toggle hides the panel without discarding the active surface", () => { |
| 115 | useRightPanelStore.getState().toggle(refA, "preview"); | 115 | useRightPanelStore.getState().toggle(refA, "diff"); |
| 116 | expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("preview"); | 116 | expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff"); |
| 117 | useRightPanelStore.getState().toggle(refA, "preview"); | 117 | useRightPanelStore.getState().toggle(refA, "diff"); |
| 118 | expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBeNull(); | 118 | 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 | }); | ||
| 119 | }); | 124 | }); |
| 120 | 125 | ||
| 121 | it("toggle to a different kind switches active", () => { | 126 | it("toggle to a different kind switches active", () => { |