Skip to content

Commit 906418a

Browse files
ytahdn钉萁
andauthored
fix(web-shell): make mobile access opt-in and simplify split composer (#12537)
Co-authored-by: 钉萁 <[email protected]>
1 parent d0cd622 commit 906418a

7 files changed

Lines changed: 78 additions & 25 deletions

File tree

‎packages/web-shell/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,8 @@ daemon 参数的完整含义和配置方式见
422422
| `settings` | `WebShellSettingsOptions` | 可选。控制原生 `/settings` 页面的呈现;见 [原生设置呈现](#原生设置呈现)。 |
423423
| `modelManagement` | `WebShellModelManagementOptions` | 可选。控制 WebShell 内模型新增/删除交互,默认均允许;见 [模型增删交互](#模型增删交互)。 |
424424

425+
移动访问二维码入口由 `header.showMobileAccess?: boolean` 控制,默认隐藏,适用于主聊天和分屏页头。独立入口 `main.tsx` 显式设为 `true`,保留本地 Qwen Code 用户的入口。
426+
425427
宿主可以通过 `onContextUsageOpen?: (sessionId: string) => void` 接管上下文
426428
详情的打开操作:
427429

‎packages/web-shell/client/App.test.tsx‎

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15286,6 +15286,19 @@ describe('App session callbacks', () => {
1528615286
).toBeNull();
1528715287
});
1528815288

15289+
it('hides Mobile access by default in built-in and custom chat headers', async () => {
15290+
const { container, rerender } = renderApp();
15291+
await flush();
15292+
expect(container.querySelector('[aria-label="Mobile access"]')).toBeNull();
15293+
15294+
const renderChatHeader = vi.fn(() => null);
15295+
rerender({ renderChatHeader });
15296+
await flush();
15297+
expect(renderChatHeader).toHaveBeenLastCalledWith(
15298+
expect.objectContaining({ onOpenLocalControlSettings: undefined }),
15299+
);
15300+
});
15301+
1528915302
it('exposes the Local Control settings deep link to a custom chat header', async () => {
1529015303
const renderChatHeader = vi.fn(
1529115304
({ onOpenLocalControlSettings }: ChatHeaderRenderInfo) => (
@@ -15294,7 +15307,10 @@ describe('App session callbacks', () => {
1529415307
</button>
1529515308
),
1529615309
);
15297-
const { container } = renderApp({ renderChatHeader });
15310+
const { container } = renderApp({
15311+
renderChatHeader,
15312+
header: { showMobileAccess: true },
15313+
});
1529815314
await flush();
1529915315

1530015316
expect(renderChatHeader).toHaveBeenCalledWith(
@@ -15319,7 +15335,7 @@ describe('App session callbacks', () => {
1531915335
mockConnection.sessionContext = { kind: 'standalone' };
1532015336
mockConnection.workspaceCwd = '';
1532115337
const renderChatHeader = vi.fn(() => null);
15322-
renderApp({ renderChatHeader });
15338+
renderApp({ renderChatHeader, header: { showMobileAccess: true } });
1532315339
await flush();
1532415340

1532515341
expect(renderChatHeader).toHaveBeenCalledWith(
@@ -15331,7 +15347,7 @@ describe('App session callbacks', () => {
1533115347
});
1533215348

1533315349
it('opens Settings deep-linked to Daemon from the main chat header QR entry', async () => {
15334-
const { container } = renderApp();
15350+
const { container } = renderApp({ header: { showMobileAccess: true } });
1533515351
await flush();
1533615352

1533715353
const entry = container.querySelector<HTMLButtonElement>(
@@ -32117,9 +32133,27 @@ describe('App session callbacks', () => {
3211732133
}
3211832134
});
3211932135

32136+
it('hides the split header Mobile access entry by default and when disabled', async () => {
32137+
const props = { sidebar: false as const, splitSessionIds: ['s1'] };
32138+
const { container, rerender } = renderApp(props);
32139+
await flush();
32140+
expect(container.querySelector('[aria-label="Mobile access"]')).toBeNull();
32141+
32142+
rerender({ ...props, header: { showMobileAccess: true } });
32143+
await flush();
32144+
expect(
32145+
container.querySelector('[aria-label="Mobile access"]'),
32146+
).not.toBeNull();
32147+
32148+
rerender({ ...props, header: { showMobileAccess: false } });
32149+
await flush();
32150+
expect(container.querySelector('[aria-label="Mobile access"]')).toBeNull();
32151+
});
32152+
3212032153
it('does not add a token usage pane action unless it is enabled', async () => {
3212132154
const { container } = renderApp({
3212232155
sidebar: false,
32156+
header: { showMobileAccess: true },
3212332157
splitSessionIds: ['s1'],
3212432158
});
3212532159
await flush();
@@ -32142,6 +32176,7 @@ describe('App session callbacks', () => {
3214232176
it('deep-links Settings to Daemon from the QR entry and clears the link on any panel close', async () => {
3214332177
const { container, rerender } = renderApp({
3214432178
sidebar: false,
32179+
header: { showMobileAccess: true },
3214532180
splitSessionIds: ['s1'],
3214632181
});
3214732182
await flush();

‎packages/web-shell/client/App.tsx‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3249,6 +3249,7 @@ export function App({
32493249
() => resolveSidebarOptions(sidebar),
32503250
[sidebar],
32513251
);
3252+
const showMobileAccess = header?.showMobileAccess ?? false;
32523253
const chatHeaderItems = header?.items ?? DEFAULT_CHAT_HEADER_ITEMS;
32533254
const chatHeaderEnabled =
32543255
chatHeaderItems.length > 0 && Boolean(header || renderChatHeader);
@@ -9411,13 +9412,16 @@ export function App({
94119412
setSettingsInitialCategory('Daemon');
94129413
openPanel('settings');
94139414
}, [openPanel]);
9414-
// Built-in pane actions: Local Control QR entry is always shown; usage
9415-
// actions follow the same opt-ins as the chat header.
9415+
// Built-in pane actions follow the same opt-ins as the chat header.
94169416
// Hosts can override via `renderPaneHeaderActions` to replace or extend it.
94179417
const defaultPaneHeaderActions = useCallback<PaneHeaderActionsRenderer>(
94189418
({ sessionId, sessionActions }) => (
94199419
<>
9420-
<LocalControlQrButton onOpenSettings={handleOpenLocalControlSettings} />
9420+
{showMobileAccess && (
9421+
<LocalControlQrButton
9422+
onOpenSettings={handleOpenLocalControlSettings}
9423+
/>
9424+
)}
94219425
{contextUsageHeaderItemVisible && (
94229426
<button
94239427
type="button"
@@ -9452,6 +9456,7 @@ export function App({
94529456
),
94539457
[
94549458
handleOpenLocalControlSettings,
9459+
showMobileAccess,
94559460
openTokenUsagePanel,
94569461
openContextUsagePanel,
94579462
t,
@@ -19472,9 +19477,10 @@ export function App({
1947219477
),
1947319478
}
1947419479
: {}),
19475-
onOpenLocalControlSettings: workspaceContextActive
19476-
? handleOpenLocalControlSettings
19477-
: undefined,
19480+
onOpenLocalControlSettings:
19481+
showMobileAccess && workspaceContextActive
19482+
? handleOpenLocalControlSettings
19483+
: undefined,
1947819484
})}
1947919485
</div>
1948019486
) : (
@@ -19523,7 +19529,7 @@ export function App({
1952319529
: undefined
1952419530
}
1952519531
onOpenLocalControlSettings={
19526-
workspaceContextActive
19532+
showMobileAccess && workspaceContextActive
1952719533
? handleOpenLocalControlSettings
1952819534
: undefined
1952919535
}

‎packages/web-shell/client/components/ChatPane.test.tsx‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1698,7 +1698,7 @@ describe('ChatPane', () => {
16981698
expect(latestChatEditorProps.builtinAtProviders).toBeUndefined();
16991699
});
17001700

1701-
it('shows the pane workspace as a toolbar chip on a multi-workspace daemon', () => {
1701+
it('keeps the workspace in the split header and only shows the toolbar chip when embedded', () => {
17021702
connectionState.capabilities = {
17031703
features: [],
17041704
workspaceCwd: '/work/web-shell',
@@ -1715,6 +1715,19 @@ describe('ChatPane', () => {
17151715
};
17161716
// The split view hands each pane its own workspace explicitly.
17171717
render({ title: 'Add pagination', workspaceCwd: '/work/api' });
1718+
expect(latestChatEditorProps.visibleToolbarActions).not.toContain(
1719+
'workspace',
1720+
);
1721+
expect(
1722+
container!.querySelector('[data-web-shell-pane-workspace]')?.textContent,
1723+
).toContain('Payments API');
1724+
1725+
rerender({
1726+
title: 'Add pagination',
1727+
workspaceCwd: '/work/api',
1728+
embedded: true,
1729+
});
1730+
expect(container!.querySelector('header')).toBeNull();
17181731
expect(latestChatEditorProps.visibleToolbarActions).toContain('workspace');
17191732
expect(latestChatEditorProps.workspaceName).toBe('Payments API');
17201733
expect(latestChatEditorProps.workspaceTitle).toBe('/work/api');

‎packages/web-shell/client/components/ChatPane.tsx‎

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,8 @@ export interface ChatPaneProps {
192192
onApprovalChange?: (sessionId: string, pending: boolean) => void;
193193
/**
194194
* The workspace this pane's session lives in. Passed explicitly by the split
195-
* view (which knows it per session) and shown as a composer-toolbar chip on a
196-
* multi-workspace daemon; falls back to the connection's own workspace.
195+
* view (which knows it per session); falls back to the connection's own
196+
* workspace.
197197
*/
198198
workspaceCwd?: string;
199199
/**
@@ -1498,10 +1498,8 @@ export function ChatPane({
14981498
title || connection.displayName || connection.sessionId?.slice(0, 8) || '';
14991499
const sessionStamp = sessionSummary?.updatedAt || sessionSummary?.createdAt;
15001500

1501-
// On a multi-workspace daemon, surface this pane's workspace as a composer-
1502-
// toolbar chip (next to where the git-branch chip sits), so it's clear which
1503-
// workspace a message goes to. Multi-workspace-ness comes from the shared
1504-
// workspace provider (the pane's own session connection may not carry it).
1501+
// Multi-workspace-ness comes from the shared workspace provider (the
1502+
// pane's own session connection may not carry it).
15051503
const showWorkspaceChip =
15061504
hasMultipleWorkspaces(workspace.capabilities) && !!paneWorkspaceCwd;
15071505
const prepareContextCompression = useCallback(() => {
@@ -1533,11 +1531,11 @@ export function ChatPane({
15331531
// `React.memo`, and a fresh `[...]` each render would defeat it.
15341532
const paneToolbarActions = useMemo(
15351533
() =>
1536-
(showWorkspaceChip
1534+
(embedded && showWorkspaceChip
15371535
? [...PANE_TOOLBAR_ACTIONS, 'workspace' as const]
15381536
: PANE_TOOLBAR_ACTIONS
15391537
).filter((action) => action !== 'plan' || planControlVisible),
1540-
[showWorkspaceChip, planControlVisible],
1538+
[embedded, showWorkspaceChip, planControlVisible],
15411539
);
15421540
const headerActions =
15431541
connection.sessionId && renderHeaderActions
@@ -1548,12 +1546,8 @@ export function ChatPane({
15481546
})
15491547
: null;
15501548

1551-
// Also surface the workspace in the pane HEADER (always visible at the top),
1552-
// not just the composer chip at the bottom — on a narrow split the composer
1553-
// chip collapses to a bare folder icon, so the header is where you tell panes
1554-
// apart. A stable per-workspace accent color (same palette as the sidebar
1555-
// session-group dots) lets same-workspace panes read as a group at a glance,
1556-
// and keeps them distinguishable even when the header name ellipsizes.
1549+
// The header identifies each split pane's workspace; only embedded panes
1550+
// without a header need the composer chip. The accent matches sidebar dots.
15571551
const workspaceLabel =
15581552
showWorkspaceChip && paneWorkspaceCwd
15591553
? workspaceLabelForCwd(

‎packages/web-shell/client/customization.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ export type WebShellChatHeaderItem =
193193
| 'contextUsage';
194194

195195
export interface WebShellChatHeaderOptions {
196+
/** Show the mobile-access QR entry in chat headers. Defaults to false. */
197+
showMobileAccess?: boolean;
196198
/** Built-in header actions to show. Token and context usage are opt-in. */
197199
items?: readonly WebShellChatHeaderItem[];
198200
}

‎packages/web-shell/client/main.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ export function StandaloneApp({ daemonToken }: { daemonToken?: string }) {
325325
? MACOS_TITLEBAR_CLASS
326326
: undefined,
327327
header: {
328+
showMobileAccess: true,
328329
items: [
329330
'title',
330331
'environment',

0 commit comments

Comments
 (0)