mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
fix(web-shell): keep "+ Add session" clickable when the catalog is empty (#15835)
* fix(web-shell): keep + Add session clickable when the catalog is empty The add button was gated on the cached session catalog already containing an addable session, so the picker could never open to refresh itself in the one case its reload-on-open effect exists for. Gate the button on pane capacity alone and let the picker report an empty list after reloading. Closes #15678 * test(web-shell): pin the empty-picker guard with a negative assertion * test(web-shell): assert add-session button disables at MAX_PANES The PR narrowed canAdd to the capacity term only, and the new test pinned just the removed half (empty catalog stays enabled). Without a cap assertion, dropping the pane-count check would leave every test green while the picker could open a seventh pane. * test(web-shell): pin the add button stays enabled below MAX_PANES --------- Co-authored-by: 2SinceJuly <[email protected]>
This commit is contained in:
1 parent
f3af3dd857
commit
3a4a077362
4 files changed
+87
-2
No files matched your search
@@ -110,6 +110,12 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pickerEmpty {
|
||||
padding: 7px 10px;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pickerItemLabel {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as React from 'react';
|
||||
import { act } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { I18nProvider } from '../i18n';
|
||||
import { MAX_SPLIT_PANES } from '../utils/splitUrl';
|
||||
|
||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
||||
|
||||
@@ -779,6 +780,30 @@ describe('SplitView', () => {
|
||||
render({ sessionIds: sessionsState.map((s) => s.sessionId) });
|
||||
// Eight requested, but only six live panes mount.
|
||||
expect(panes()).toHaveLength(6);
|
||||
// At the cap there is no room left, so the entry point is disabled.
|
||||
const addButton = container!.querySelector(
|
||||
'button[aria-haspopup="listbox"]',
|
||||
) as HTMLButtonElement;
|
||||
expect(addButton.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the add button enabled one below MAX_PANES', () => {
|
||||
sessionsState = Array.from({ length: MAX_SPLIT_PANES + 2 }, (_, i) => ({
|
||||
sessionId: `x${i}`,
|
||||
workspaceCwd: '/w',
|
||||
displayName: `Pane ${i}`,
|
||||
}));
|
||||
// One slot spare and a session left to add, so the entry point must work.
|
||||
render({
|
||||
sessionIds: sessionsState
|
||||
.slice(0, MAX_SPLIT_PANES - 1)
|
||||
.map((session) => session.sessionId),
|
||||
});
|
||||
expect(panes()).toHaveLength(MAX_SPLIT_PANES - 1);
|
||||
const addButton = container!.querySelector(
|
||||
'button[aria-haspopup="listbox"]',
|
||||
) as HTMLButtonElement;
|
||||
expect(addButton.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('isolates a crashing pane so the rest of the split survives', () => {
|
||||
@@ -1072,6 +1097,45 @@ describe('SplitView', () => {
|
||||
expect(hiddenSlots()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('keeps the add button enabled when the catalog has no other sessions', () => {
|
||||
sessionsState = [
|
||||
{ sessionId: 's3', workspaceCwd: '/w', displayName: 'Three' },
|
||||
];
|
||||
render({ sessionIds: ['s3'] });
|
||||
const addButton = container!.querySelector(
|
||||
'button[aria-haspopup="listbox"]',
|
||||
) as HTMLButtonElement;
|
||||
// The cached catalog is not authoritative: another session may exist
|
||||
// elsewhere, so an empty list must not disable the entry point.
|
||||
expect(addButton.disabled).toBe(false);
|
||||
openPicker();
|
||||
expect(pickerOptions()).toEqual([]);
|
||||
expect(container!.querySelector('[role="listbox"]')?.textContent).toContain(
|
||||
'No other sessions',
|
||||
);
|
||||
});
|
||||
|
||||
it('picks up a session created elsewhere after opening the empty picker', () => {
|
||||
sessionsState = [
|
||||
{ sessionId: 's3', workspaceCwd: '/w', displayName: 'Three' },
|
||||
];
|
||||
render({ sessionIds: ['s3'] });
|
||||
openPicker();
|
||||
expect(pickerOptions()).toEqual([]);
|
||||
// A session is created elsewhere while the picker was open.
|
||||
sessionsState = [
|
||||
...sessionsState,
|
||||
{ sessionId: 's6', workspaceCwd: '/w', displayName: 'Six' },
|
||||
];
|
||||
act(() =>
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
|
||||
),
|
||||
);
|
||||
openPicker();
|
||||
expect(pickerOptions()).toEqual(['Six']);
|
||||
});
|
||||
|
||||
it('reloads the session list when the picker opens (never a stale list)', () => {
|
||||
render({ sessionIds: ['s1'] });
|
||||
// `useSessions` only fetches on mount; nothing reloads until the user acts.
|
||||
@@ -1104,6 +1168,11 @@ describe('SplitView', () => {
|
||||
);
|
||||
openPicker();
|
||||
expect(pickerOptions()).toEqual(['Two', 'Three', 'Four', 'Five']);
|
||||
// The empty row is `role="presentation"`, so only a text read sees it: a
|
||||
// populated picker must not keep advertising "No other sessions".
|
||||
expect(
|
||||
container!.querySelector('[role="listbox"]')?.textContent,
|
||||
).not.toContain('No other sessions');
|
||||
});
|
||||
|
||||
it('mirrors the live pane set up to the parent as panes change', () => {
|
||||
|
||||
@@ -471,7 +471,10 @@ export function SplitView({
|
||||
() => allSessions.filter((session) => !paneIds.includes(session.sessionId)),
|
||||
[allSessions, paneIds],
|
||||
);
|
||||
const canAdd = paneIds.length < MAX_PANES && available.length > 0;
|
||||
// The cached catalog may lag behind sessions created elsewhere, so the button
|
||||
// must stay clickable whenever another pane fits. The picker reloads on open
|
||||
// and reports an empty list itself.
|
||||
const canAdd = paneIds.length < MAX_PANES;
|
||||
// Only offer per-pane maximize once there's another pane to maximize against —
|
||||
// a lone pane already fills the split.
|
||||
const canMaximize = paneIds.length > 1;
|
||||
@@ -532,8 +535,13 @@ export function SplitView({
|
||||
>
|
||||
+ {t('splitView.addPane')}
|
||||
</button>
|
||||
{pickerOpen && available.length > 0 && (
|
||||
{pickerOpen && (
|
||||
<ul className={styles.picker} role="listbox">
|
||||
{available.length === 0 && (
|
||||
<li className={styles.pickerEmpty} role="presentation">
|
||||
{t('splitView.noOtherSessions')}
|
||||
</li>
|
||||
)}
|
||||
{available.map((session) => (
|
||||
<li key={session.sessionId} role="option" aria-selected="false">
|
||||
<button
|
||||
|
||||
@@ -4295,6 +4295,7 @@ const EN: Messages = {
|
||||
'splitView.goToApproval': 'Go to it',
|
||||
'splitView.pendingCount': (v) => `${v?.count ?? 0} awaiting input`,
|
||||
'splitView.nextPending': 'Go to the next session awaiting input',
|
||||
'splitView.noOtherSessions': 'No other sessions',
|
||||
'splitView.empty': 'No sessions in the split. Add one to get started.',
|
||||
'splitView.composerPlaceholder': 'Message this session…',
|
||||
'channels.title': 'Channels',
|
||||
@@ -8620,6 +8621,7 @@ const ZH: Messages = {
|
||||
'splitView.goToApproval': '前往处理',
|
||||
'splitView.pendingCount': (v) => `${v?.count ?? 0} 个会话待处理`,
|
||||
'splitView.nextPending': '前往下一个待处理会话',
|
||||
'splitView.noOtherSessions': '没有其他会话',
|
||||
'splitView.empty': '分屏中还没有会话,添加一个开始。',
|
||||
'splitView.composerPlaceholder': '给这个会话发消息…',
|
||||
'channels.title': '频道',
|
||||
|
||||
Reference in new issue
Block a user