mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
fix(web-shell): make mobile access opt-in and simplify split composer (#12537)
Co-authored-by: 钉萁 <[email protected]>
This commit is contained in:
1 parent
d0cd622a68
commit
906418aa9b
7 files changed
+78
-25
No files matched your search
@@ -422,6 +422,8 @@ daemon 参数的完整含义和配置方式见
|
||||
| `settings` | `WebShellSettingsOptions` | 可选。控制原生 `/settings` 页面的呈现;见 [原生设置呈现](#原生设置呈现)。 |
|
||||
| `modelManagement` | `WebShellModelManagementOptions` | 可选。控制 WebShell 内模型新增/删除交互,默认均允许;见 [模型增删交互](#模型增删交互)。 |
|
||||
|
||||
移动访问二维码入口由 `header.showMobileAccess?: boolean` 控制,默认隐藏,适用于主聊天和分屏页头。独立入口 `main.tsx` 显式设为 `true`,保留本地 Qwen Code 用户的入口。
|
||||
|
||||
宿主可以通过 `onContextUsageOpen?: (sessionId: string) => void` 接管上下文
|
||||
详情的打开操作:
|
||||
|
||||
|
||||
@@ -15286,6 +15286,19 @@ describe('App session callbacks', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('hides Mobile access by default in built-in and custom chat headers', async () => {
|
||||
const { container, rerender } = renderApp();
|
||||
await flush();
|
||||
expect(container.querySelector('[aria-label="Mobile access"]')).toBeNull();
|
||||
|
||||
const renderChatHeader = vi.fn(() => null);
|
||||
rerender({ renderChatHeader });
|
||||
await flush();
|
||||
expect(renderChatHeader).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ onOpenLocalControlSettings: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it('exposes the Local Control settings deep link to a custom chat header', async () => {
|
||||
const renderChatHeader = vi.fn(
|
||||
({ onOpenLocalControlSettings }: ChatHeaderRenderInfo) => (
|
||||
@@ -15294,7 +15307,10 @@ describe('App session callbacks', () => {
|
||||
</button>
|
||||
),
|
||||
);
|
||||
const { container } = renderApp({ renderChatHeader });
|
||||
const { container } = renderApp({
|
||||
renderChatHeader,
|
||||
header: { showMobileAccess: true },
|
||||
});
|
||||
await flush();
|
||||
|
||||
expect(renderChatHeader).toHaveBeenCalledWith(
|
||||
@@ -15319,7 +15335,7 @@ describe('App session callbacks', () => {
|
||||
mockConnection.sessionContext = { kind: 'standalone' };
|
||||
mockConnection.workspaceCwd = '';
|
||||
const renderChatHeader = vi.fn(() => null);
|
||||
renderApp({ renderChatHeader });
|
||||
renderApp({ renderChatHeader, header: { showMobileAccess: true } });
|
||||
await flush();
|
||||
|
||||
expect(renderChatHeader).toHaveBeenCalledWith(
|
||||
@@ -15331,7 +15347,7 @@ describe('App session callbacks', () => {
|
||||
});
|
||||
|
||||
it('opens Settings deep-linked to Daemon from the main chat header QR entry', async () => {
|
||||
const { container } = renderApp();
|
||||
const { container } = renderApp({ header: { showMobileAccess: true } });
|
||||
await flush();
|
||||
|
||||
const entry = container.querySelector<HTMLButtonElement>(
|
||||
@@ -32117,9 +32133,27 @@ describe('App session callbacks', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('hides the split header Mobile access entry by default and when disabled', async () => {
|
||||
const props = { sidebar: false as const, splitSessionIds: ['s1'] };
|
||||
const { container, rerender } = renderApp(props);
|
||||
await flush();
|
||||
expect(container.querySelector('[aria-label="Mobile access"]')).toBeNull();
|
||||
|
||||
rerender({ ...props, header: { showMobileAccess: true } });
|
||||
await flush();
|
||||
expect(
|
||||
container.querySelector('[aria-label="Mobile access"]'),
|
||||
).not.toBeNull();
|
||||
|
||||
rerender({ ...props, header: { showMobileAccess: false } });
|
||||
await flush();
|
||||
expect(container.querySelector('[aria-label="Mobile access"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not add a token usage pane action unless it is enabled', async () => {
|
||||
const { container } = renderApp({
|
||||
sidebar: false,
|
||||
header: { showMobileAccess: true },
|
||||
splitSessionIds: ['s1'],
|
||||
});
|
||||
await flush();
|
||||
@@ -32142,6 +32176,7 @@ describe('App session callbacks', () => {
|
||||
it('deep-links Settings to Daemon from the QR entry and clears the link on any panel close', async () => {
|
||||
const { container, rerender } = renderApp({
|
||||
sidebar: false,
|
||||
header: { showMobileAccess: true },
|
||||
splitSessionIds: ['s1'],
|
||||
});
|
||||
await flush();
|
||||
|
||||
@@ -3249,6 +3249,7 @@ export function App({
|
||||
() => resolveSidebarOptions(sidebar),
|
||||
[sidebar],
|
||||
);
|
||||
const showMobileAccess = header?.showMobileAccess ?? false;
|
||||
const chatHeaderItems = header?.items ?? DEFAULT_CHAT_HEADER_ITEMS;
|
||||
const chatHeaderEnabled =
|
||||
chatHeaderItems.length > 0 && Boolean(header || renderChatHeader);
|
||||
@@ -9411,13 +9412,16 @@ export function App({
|
||||
setSettingsInitialCategory('Daemon');
|
||||
openPanel('settings');
|
||||
}, [openPanel]);
|
||||
// Built-in pane actions: Local Control QR entry is always shown; usage
|
||||
// actions follow the same opt-ins as the chat header.
|
||||
// Built-in pane actions follow the same opt-ins as the chat header.
|
||||
// Hosts can override via `renderPaneHeaderActions` to replace or extend it.
|
||||
const defaultPaneHeaderActions = useCallback<PaneHeaderActionsRenderer>(
|
||||
({ sessionId, sessionActions }) => (
|
||||
<>
|
||||
<LocalControlQrButton onOpenSettings={handleOpenLocalControlSettings} />
|
||||
{showMobileAccess && (
|
||||
<LocalControlQrButton
|
||||
onOpenSettings={handleOpenLocalControlSettings}
|
||||
/>
|
||||
)}
|
||||
{contextUsageHeaderItemVisible && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -9452,6 +9456,7 @@ export function App({
|
||||
),
|
||||
[
|
||||
handleOpenLocalControlSettings,
|
||||
showMobileAccess,
|
||||
openTokenUsagePanel,
|
||||
openContextUsagePanel,
|
||||
t,
|
||||
@@ -19472,9 +19477,10 @@ export function App({
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
onOpenLocalControlSettings: workspaceContextActive
|
||||
? handleOpenLocalControlSettings
|
||||
: undefined,
|
||||
onOpenLocalControlSettings:
|
||||
showMobileAccess && workspaceContextActive
|
||||
? handleOpenLocalControlSettings
|
||||
: undefined,
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
@@ -19523,7 +19529,7 @@ export function App({
|
||||
: undefined
|
||||
}
|
||||
onOpenLocalControlSettings={
|
||||
workspaceContextActive
|
||||
showMobileAccess && workspaceContextActive
|
||||
? handleOpenLocalControlSettings
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -1698,7 +1698,7 @@ describe('ChatPane', () => {
|
||||
expect(latestChatEditorProps.builtinAtProviders).toBeUndefined();
|
||||
});
|
||||
|
||||
it('shows the pane workspace as a toolbar chip on a multi-workspace daemon', () => {
|
||||
it('keeps the workspace in the split header and only shows the toolbar chip when embedded', () => {
|
||||
connectionState.capabilities = {
|
||||
features: [],
|
||||
workspaceCwd: '/work/web-shell',
|
||||
@@ -1715,6 +1715,19 @@ describe('ChatPane', () => {
|
||||
};
|
||||
// The split view hands each pane its own workspace explicitly.
|
||||
render({ title: 'Add pagination', workspaceCwd: '/work/api' });
|
||||
expect(latestChatEditorProps.visibleToolbarActions).not.toContain(
|
||||
'workspace',
|
||||
);
|
||||
expect(
|
||||
container!.querySelector('[data-web-shell-pane-workspace]')?.textContent,
|
||||
).toContain('Payments API');
|
||||
|
||||
rerender({
|
||||
title: 'Add pagination',
|
||||
workspaceCwd: '/work/api',
|
||||
embedded: true,
|
||||
});
|
||||
expect(container!.querySelector('header')).toBeNull();
|
||||
expect(latestChatEditorProps.visibleToolbarActions).toContain('workspace');
|
||||
expect(latestChatEditorProps.workspaceName).toBe('Payments API');
|
||||
expect(latestChatEditorProps.workspaceTitle).toBe('/work/api');
|
||||
|
||||
@@ -192,8 +192,8 @@ export interface ChatPaneProps {
|
||||
onApprovalChange?: (sessionId: string, pending: boolean) => void;
|
||||
/**
|
||||
* The workspace this pane's session lives in. Passed explicitly by the split
|
||||
* view (which knows it per session) and shown as a composer-toolbar chip on a
|
||||
* multi-workspace daemon; falls back to the connection's own workspace.
|
||||
* view (which knows it per session); falls back to the connection's own
|
||||
* workspace.
|
||||
*/
|
||||
workspaceCwd?: string;
|
||||
/**
|
||||
@@ -1498,10 +1498,8 @@ export function ChatPane({
|
||||
title || connection.displayName || connection.sessionId?.slice(0, 8) || '';
|
||||
const sessionStamp = sessionSummary?.updatedAt || sessionSummary?.createdAt;
|
||||
|
||||
// On a multi-workspace daemon, surface this pane's workspace as a composer-
|
||||
// toolbar chip (next to where the git-branch chip sits), so it's clear which
|
||||
// workspace a message goes to. Multi-workspace-ness comes from the shared
|
||||
// workspace provider (the pane's own session connection may not carry it).
|
||||
// Multi-workspace-ness comes from the shared workspace provider (the
|
||||
// pane's own session connection may not carry it).
|
||||
const showWorkspaceChip =
|
||||
hasMultipleWorkspaces(workspace.capabilities) && !!paneWorkspaceCwd;
|
||||
const prepareContextCompression = useCallback(() => {
|
||||
@@ -1533,11 +1531,11 @@ export function ChatPane({
|
||||
// `React.memo`, and a fresh `[...]` each render would defeat it.
|
||||
const paneToolbarActions = useMemo(
|
||||
() =>
|
||||
(showWorkspaceChip
|
||||
(embedded && showWorkspaceChip
|
||||
? [...PANE_TOOLBAR_ACTIONS, 'workspace' as const]
|
||||
: PANE_TOOLBAR_ACTIONS
|
||||
).filter((action) => action !== 'plan' || planControlVisible),
|
||||
[showWorkspaceChip, planControlVisible],
|
||||
[embedded, showWorkspaceChip, planControlVisible],
|
||||
);
|
||||
const headerActions =
|
||||
connection.sessionId && renderHeaderActions
|
||||
@@ -1548,12 +1546,8 @@ export function ChatPane({
|
||||
})
|
||||
: null;
|
||||
|
||||
// Also surface the workspace in the pane HEADER (always visible at the top),
|
||||
// not just the composer chip at the bottom — on a narrow split the composer
|
||||
// chip collapses to a bare folder icon, so the header is where you tell panes
|
||||
// apart. A stable per-workspace accent color (same palette as the sidebar
|
||||
// session-group dots) lets same-workspace panes read as a group at a glance,
|
||||
// and keeps them distinguishable even when the header name ellipsizes.
|
||||
// The header identifies each split pane's workspace; only embedded panes
|
||||
// without a header need the composer chip. The accent matches sidebar dots.
|
||||
const workspaceLabel =
|
||||
showWorkspaceChip && paneWorkspaceCwd
|
||||
? workspaceLabelForCwd(
|
||||
|
||||
@@ -193,6 +193,8 @@ export type WebShellChatHeaderItem =
|
||||
| 'contextUsage';
|
||||
|
||||
export interface WebShellChatHeaderOptions {
|
||||
/** Show the mobile-access QR entry in chat headers. Defaults to false. */
|
||||
showMobileAccess?: boolean;
|
||||
/** Built-in header actions to show. Token and context usage are opt-in. */
|
||||
items?: readonly WebShellChatHeaderItem[];
|
||||
}
|
||||
|
||||
@@ -325,6 +325,7 @@ export function StandaloneApp({ daemonToken }: { daemonToken?: string }) {
|
||||
? MACOS_TITLEBAR_CLASS
|
||||
: undefined,
|
||||
header: {
|
||||
showMobileAccess: true,
|
||||
items: [
|
||||
'title',
|
||||
'environment',
|
||||
|
||||
Reference in new issue
Block a user