@@ -27,6 +27,7 @@ import {
2727 type GoalSnapshotV2,
2828} from '@qwen-code/sdk/daemon';
2929import type { WebShellApi } from './App';
30+ import { DEFAULT_SESSION_ACTION_ITEMS } from './components/sidebar/WebShellSidebar';
3031import type { Message } from './adapters/types';
3132import type {
3233 VoiceStatusRevision,
@@ -653,6 +654,8 @@ const {
653654 settings: DaemonSettingDescriptor[];
654655 } | null,
655656 latestSplitViewProps: null as {
657+ onPendingPanesChange?: (ids: string[]) => void;
658+ showSessionDetails?: boolean;
656659 includeOtherWorkspaces?: boolean;
657660 workspaceCwd?: string;
658661 sessionWorkflowEnabled?: boolean;
@@ -1315,9 +1318,14 @@ vi.mock('./components/dialogs/DialogShell', async () => {
13151318 };
13161319});
13171320
1318- vi.mock('./components/sidebar/WebShellSidebar', async () => {
1321+ vi.mock('./components/sidebar/WebShellSidebar', async (importOriginal ) => {
13191322 const React = await import('react');
1323+ const actual =
1324+ await importOriginal<
1325+ typeof import('./components/sidebar/WebShellSidebar')
1326+ >();
13201327 return {
1328+ DEFAULT_SESSION_ACTION_ITEMS: actual.DEFAULT_SESSION_ACTION_ITEMS,
13211329 WebShellSidebar: (props: {
13221330 collapsed?: boolean;
13231331 onOpenSettings?: () => void;
@@ -1747,6 +1755,7 @@ vi.doMock('./components/SplitView', async () => {
17471755 onExit?: () => void;
17481756 sessionIds?: string[];
17491757 onPanesChange?: (ids: string[]) => void;
1758+ onPendingPanesChange?: (ids: string[]) => void;
17501759 includeOtherWorkspaces?: boolean;
17511760 workspaceCwd?: string;
17521761 sessionWorkflowEnabled?: boolean;
@@ -10515,6 +10524,12 @@ describe('App session workflow', () => {
1051510524 await Promise.resolve();
1051610525 });
1051710526
10527+ await act(async () => {
10528+ container
10529+ .querySelector<HTMLButtonElement>('[data-testid="split-report-panes"]')
10530+ ?.click();
10531+ });
10532+
1051810533 testState.settings = [sessionWorkflowSetting()];
1051910534 rerender();
1052010535 await flush();
@@ -28888,6 +28903,91 @@ describe('App session callbacks', () => {
2888828903 ).toBeNull();
2888928904 });
2889028905
28906+ it.each<undefined | Array<'details'>>([undefined, [], ['details']])(
28907+ 'applies the session-details allowlist to split panes: %j',
28908+ async (items) => {
28909+ const { container } = renderApp({
28910+ sidebar: { sessionActions: { items } },
28911+ });
28912+ await flush();
28913+ await act(async () => {
28914+ container
28915+ .querySelector<HTMLButtonElement>('[data-testid="open-split-view"]')
28916+ ?.click();
28917+ });
28918+ expect(testState.latestSplitViewProps?.showSessionDetails).toBe(
28919+ (items ?? DEFAULT_SESSION_ACTION_ITEMS).includes('details'),
28920+ );
28921+ },
28922+ );
28923+
28924+ it.each([false, true])(
28925+ 'does not rerender App for other split sessions (outer pending: %s)',
28926+ async (outerPending) => {
28927+ const { container, rerender } = renderApp();
28928+ await flush();
28929+ await act(async () => {
28930+ container
28931+ .querySelector<HTMLButtonElement>('[data-testid="open-split-view"]')
28932+ ?.click();
28933+ });
28934+ await flush();
28935+ const report = testState.latestSplitViewProps!.onPendingPanesChange!;
28936+ const ownerIds = outerPending ? [mockConnection.sessionId!] : [];
28937+ await act(async () => report(ownerIds));
28938+ rerender();
28939+ expect(testState.latestSplitViewProps!.onPendingPanesChange).toBe(report);
28940+ expect(mockUseDaemonActivePromptBridge).toHaveBeenCalled();
28941+ mockUseDaemonActivePromptBridge.mockClear();
28942+ for (const ids of [['foreign-session'], ['another-session'], []]) {
28943+ await act(async () => report([...ownerIds, ...ids]));
28944+ expect(mockUseDaemonActivePromptBridge).not.toHaveBeenCalled();
28945+ }
28946+ },
28947+ );
28948+
28949+ it('keeps the outer approval notice until its current session is reported', async () => {
28950+ const { container, rerender } = renderApp();
28951+ await flush();
28952+ await act(async () => {
28953+ container
28954+ .querySelector<HTMLButtonElement>('[data-testid="open-split-view"]')
28955+ ?.click();
28956+ });
28957+ await act(async () => {
28958+ testState.blocks = [makePendingPermissionBlock()];
28959+ rerender();
28960+ });
28961+ expect(
28962+ container.querySelector('[data-testid="split-initial"]')?.textContent,
28963+ ).toContain(mockConnection.sessionId);
28964+ const notice = () =>
28965+ container.querySelector('[data-testid="split-approval-notice"]');
28966+ expect(notice()).not.toBeNull();
28967+ await act(async () => {
28968+ testState.latestSplitViewProps?.onPendingPanesChange?.([
28969+ mockConnection.sessionId!,
28970+ ]);
28971+ });
28972+ expect(notice()).toBeNull();
28973+ const previous = testState.latestSplitViewProps!.onPendingPanesChange!;
28974+ const previousSessionId = mockConnection.sessionId!;
28975+ await act(async () => {
28976+ mockConnection.sessionId = 'outer-session-2';
28977+ rerender();
28978+ });
28979+ const next = testState.latestSplitViewProps!.onPendingPanesChange!;
28980+ expect(next).not.toBe(previous);
28981+ await act(async () => next([previousSessionId]));
28982+ expect(notice()).not.toBeNull();
28983+ await act(async () => next(['outer-session-2']));
28984+ expect(notice()).toBeNull();
28985+ await act(async () => {
28986+ testState.latestSplitViewProps?.onPendingPanesChange?.([]);
28987+ });
28988+ expect(notice()).not.toBeNull();
28989+ });
28990+
2889128991 it('surfaces the outer approval as a split notice and returns to chat when clicked', async () => {
2889228992 // The overlay is suppressed under the split, so the outer approval would be
2889328993 // invisible; a notice banner (with a way back) is the only signal.
@@ -28900,6 +29000,12 @@ describe('App session callbacks', () => {
2890029000 ?.click();
2890129001 await Promise.resolve();
2890229002 });
29003+ await act(async () => {
29004+ container
29005+ .querySelector<HTMLButtonElement>('[data-testid="split-report-panes"]')
29006+ ?.click();
29007+ });
29008+
2890329009 await act(async () => {
2890429010 testState.blocks = [makePendingPermissionBlock()];
2890529011 rerender();
0 commit comments