Skip to content

Commit 84b6fa9

Browse files
GCWingGCWing
andauthored
fix(flow-chat): clarify permission scope menu (#2710)
Co-authored-by: GCWing <gcwing@foxmail.com>
1 parent 0e0a52f commit 84b6fa9

6 files changed

Lines changed: 410 additions & 109 deletions

File tree

src/web-ui/src/flow_chat/components/ChatInput.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5557,11 +5557,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
55575557
: {
55585558
mode: permissionMode,
55595559
saving: permissionModeSaving,
5560-
scopeLabel: activePermissionTurnId
5561-
? t('chatInput.permissionMode.activeTurnScope')
5562-
: temporaryPermissionMode
5563-
? t('chatInput.permissionMode.turnScope')
5564-
: t('chatInput.permissionMode.sessionScope'),
5560+
scopeLabel: t('chatInput.permissionMode.sessionScope'),
55655561
overridden: permissionModeOverridden,
55665562
nextTurnMode: temporaryPermissionMode
55675563
? chatInputPermissionMode(temporaryPermissionMode)

src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.test.tsx

Lines changed: 130 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ describe('ChatInputWorkspaceStrip git refresh behavior', () => {
375375
expect(document.querySelector('[data-testid="chat-input-permission-menu"]')).toBeNull();
376376
});
377377

378-
it('chooses the scope per click instead of through a separate toggle', async () => {
378+
it('keeps session selection primary and opens one-off choices as a second level', async () => {
379379
const onChange = vi.fn();
380380
const onChangeForNextTurn = vi.fn();
381381
await act(async () => {
@@ -409,10 +409,17 @@ describe('ChatInputWorkspaceStrip git refresh behavior', () => {
409409
expect(onChange).toHaveBeenCalledWith('auto');
410410
expect(onChangeForNextTurn).not.toHaveBeenCalled();
411411

412-
// The trailing button is the one-off scope, and never writes the session.
412+
// One-off scope is a named second level, not an unexplained checkbox on
413+
// every session row.
413414
await act(async () => {
414415
trigger?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
415416
});
417+
expect(document.querySelector('[role="menuitemcheckbox"]')).toBeNull();
418+
await act(async () => {
419+
document
420+
.querySelector<HTMLButtonElement>('[data-testid="chat-input-permission-turn-scope"]')
421+
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
422+
});
416423
await act(async () => {
417424
document
418425
.querySelector<HTMLButtonElement>(
@@ -435,7 +442,7 @@ describe('ChatInputWorkspaceStrip git refresh behavior', () => {
435442
mode: 'ask',
436443
activeTurn: true,
437444
nextTurnMode: null,
438-
scopeLabel: 'chatInput.permissionMode.activeTurnScope',
445+
scopeLabel: 'This session',
439446
onChange: vi.fn(),
440447
onChangeForNextTurn,
441448
}}
@@ -450,21 +457,122 @@ describe('ChatInputWorkspaceStrip git refresh behavior', () => {
450457
await act(async () => {
451458
trigger?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
452459
});
460+
expect(document.body.textContent).toContain('This session');
461+
expect(
462+
document.querySelector('[data-testid="chat-input-permission-turn-scope"]')?.textContent,
463+
).toContain('chatInput.permissionMode.activeTurnSettings');
464+
await act(async () => {
465+
document
466+
.querySelector<HTMLButtonElement>('[data-testid="chat-input-permission-turn-scope"]')
467+
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
468+
});
453469

454470
const oneOff = document.querySelector<HTMLButtonElement>(
455471
'[data-testid="chat-input-permission-next-turn-auto"]',
456472
);
457473
expect(oneOff?.getAttribute('aria-label')).toBe(
458474
'chatInput.permissionMode.activeTurnOnly',
459475
);
460-
expect(oneOff?.getAttribute('role')).toBe('menuitemcheckbox');
476+
expect(oneOff?.getAttribute('role')).toBe('menuitemradio');
477+
expect(document.body.textContent).toContain('chatInput.permissionMode.activeTurnScope');
461478

462479
await act(async () => {
463480
oneOff?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
464481
});
465482
expect(onChangeForNextTurn).toHaveBeenCalledWith('auto');
466483
});
467484

485+
it('clears a one-off override through the explicit follow-session choice', async () => {
486+
const onChangeForNextTurn = vi.fn();
487+
await act(async () => {
488+
root.render(
489+
<ChatInputWorkspaceStrip
490+
repositoryPath=""
491+
workspaceLabel=""
492+
permissionControl={{
493+
mode: 'auto',
494+
nextTurnMode: 'full_access',
495+
onChange: vi.fn(),
496+
onChangeForNextTurn,
497+
}}
498+
/>
499+
);
500+
});
501+
502+
await act(async () => {
503+
container
504+
.querySelector<HTMLButtonElement>('[data-testid="chat-input-permission-trigger"]')
505+
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
506+
});
507+
await act(async () => {
508+
document
509+
.querySelector<HTMLButtonElement>('[data-testid="chat-input-permission-turn-scope"]')
510+
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
511+
});
512+
513+
const followSession = document.querySelector<HTMLButtonElement>(
514+
'[data-testid="chat-input-permission-follow-session"]',
515+
);
516+
expect(followSession?.getAttribute('role')).toBe('menuitemradio');
517+
expect(followSession?.getAttribute('aria-checked')).toBe('false');
518+
519+
await act(async () => {
520+
followSession?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
521+
});
522+
expect(onChangeForNextTurn).toHaveBeenCalledWith('full_access');
523+
expect(document.querySelector('[data-testid="chat-input-permission-menu"]')).toBeNull();
524+
});
525+
526+
it('opens the one-off level on click or Right Arrow, never on hover', async () => {
527+
await act(async () => {
528+
root.render(
529+
<ChatInputWorkspaceStrip
530+
repositoryPath=""
531+
workspaceLabel=""
532+
permissionControl={{
533+
mode: 'ask',
534+
onChange: vi.fn(),
535+
onChangeForNextTurn: vi.fn(),
536+
}}
537+
/>
538+
);
539+
});
540+
541+
await act(async () => {
542+
container
543+
.querySelector<HTMLButtonElement>('[data-testid="chat-input-permission-trigger"]')
544+
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
545+
});
546+
const scope = document.querySelector<HTMLButtonElement>(
547+
'[data-testid="chat-input-permission-turn-scope"]',
548+
);
549+
await act(async () => {
550+
scope?.dispatchEvent(new MouseEvent('pointerenter', { bubbles: true }));
551+
});
552+
expect(document.querySelector('[data-testid="chat-input-permission-follow-session"]'))
553+
.toBeNull();
554+
555+
await act(async () => {
556+
scope?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
557+
});
558+
expect(document.querySelector('[data-testid="chat-input-permission-follow-session"]'))
559+
.not.toBeNull();
560+
expect(document.querySelector('[role="menuitemcheckbox"]')).toBeNull();
561+
expect(
562+
document
563+
.querySelector('[data-testid="chat-input-permission-turn-back"]')
564+
?.getAttribute('aria-label'),
565+
).toBe('chatInput.permissionMode.backToSessionSettings');
566+
567+
await act(async () => {
568+
document
569+
.querySelector<HTMLElement>('[data-testid="chat-input-permission-menu"]')
570+
?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
571+
});
572+
expect(document.querySelector('[data-testid="chat-input-permission-turn-scope"]'))
573+
.not.toBeNull();
574+
});
575+
468576
it('keeps the mode descriptions out of the row and in the accessible name', async () => {
469577
await act(async () => {
470578
root.render(
@@ -515,6 +623,15 @@ describe('ChatInputWorkspaceStrip git refresh behavior', () => {
515623
await act(async () => {
516624
trigger?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
517625
});
626+
expect(document.querySelector('[role="menuitemcheckbox"]')).toBeNull();
627+
expect(
628+
document.querySelector('[data-testid="chat-input-permission-turn-scope"]')?.textContent,
629+
).toContain('chatInput.permissionMode.fullAccess.label');
630+
await act(async () => {
631+
document
632+
.querySelector<HTMLButtonElement>('[data-testid="chat-input-permission-turn-scope"]')
633+
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
634+
});
518635
expect(
519636
document
520637
.querySelector('[data-testid="chat-input-permission-next-turn-full_access"]')
@@ -542,7 +659,7 @@ describe('ChatInputWorkspaceStrip git refresh behavior', () => {
542659
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
543660
});
544661
expect(
545-
document.querySelector('[data-testid="chat-input-permission-next-turn-ask"]'),
662+
document.querySelector('[data-testid="chat-input-permission-turn-scope"]'),
546663
).toBeNull();
547664
});
548665

@@ -581,6 +698,14 @@ describe('ChatInputWorkspaceStrip git refresh behavior', () => {
581698
expect(
582699
document.querySelector('[data-testid="chat-input-permission-selected-full_access"]'),
583700
).toBeNull();
701+
expect(
702+
document.querySelector('[data-testid="chat-input-permission-next-turn-full_access"]'),
703+
).toBeNull();
704+
await act(async () => {
705+
document
706+
.querySelector<HTMLButtonElement>('[data-testid="chat-input-permission-turn-scope"]')
707+
?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
708+
});
584709
expect(
585710
document
586711
.querySelector('[data-testid="chat-input-permission-next-turn-full_access"]')

0 commit comments

Comments
 (0)