fix: reserve Windows titlebar chrome space for Code actions

This commit is contained in:
2026-09-15 10:14:44 +08:00
parent a39fbc1619
commit dae64e52b9
6 changed files with 144 additions and 7 deletions

View File

@@ -0,0 +1,51 @@
# Task: Fix Windows coding titlebar logo overlap
## Identity
- Task ID: 20260915-windows-logo-overlap-5106511f
- Mode: Feature
- Branch: codex/20260915-windows-logo-overlap-5106511f-windows-logo-overlap
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260915-windows-logo-overlap-5106511f
- Base commit: a39fbc1619470ad8d47a1b9131b2cada5b940ec5
- Owner: codex
- Status: Ready for Integration
## Scope
- Reproduce and fix the Windows Code titlebar overlap between the Makelore wordmark and the developer-browser toggle. Cover the actual Renderer geometry with the shared Electron fixture.
## Intent And Constraints
- Concurrent Task Gate passed: bundled check/start/status succeeded and match this task's feature mode, branch, isolated worktree and base commit. Main is occupied by the platform-embedding integration task; all task operations remain in this linked worktree.
- Planning Gate passed after reading the agent entry and memory index, own task record, positioning, integrated state, decision index, system overview, business rules, success criteria, evidence index and previous Windows titlebar integration record.
- Read the Scope, Intent And Constraints and Promotion Candidates of the 200 registered peers. No concrete semantic conflict with this narrow layout correction was found. Several older peer records remain planning placeholders; their coordination scope is unknown. Do not modify them.
- Project positioning contains placeholders and some shared history describes retired modules. Use current AGENTS.md and implementation for product boundaries. The old titlebar source record is absent; its readable integration record documents the existing 148 px Windows control reservation.
- Keep the light visual system, Main-owned browser contract, platform chrome behavior and Code/Pi configuration boundaries. No subagents, package, deployment or unrelated cleanup.
- Plan: reproduce browser/wordmark geometry overlap in Electron, isolate the responsible spacing, make a narrow correction, then run related unit tests, typecheck, lint, build and Electron layout/interaction checks.
## Outcome
- Reproduced the reported Windows Code overlap against the built Electron app twice: the browser button ended at x=1084 while the wordmark started at x=1036. The minimized layout regression failed in 1.7 seconds before the fix.
- Root cause: CodingConversationHeader used the macOS-sized 160 px padding for both custom-titlebar platforms. Windows chrome needs 148 px for caption controls plus 96 px for the wordmark, so browser and run actions occupied the wordmark region.
- Shared the existing wordmark/caption dimensions in `src/components/layout/titlebar-metrics.ts`. Windows now ends the portaled conversation header at the wordmark boundary and retains its ordinary 12 px inner padding. This reserves space for all actions and keeps their hit area outside caption controls. macOS keeps its existing 160 px padding, and Linux keeps its inline header.
- Updated README's platform-specific titlebar description. No backend, runtime, persistence, application identity or canonical-memory change.
- Added a Windows Electron regression for geometry, browser open/close, sidebar collapse, active/settled run states, minimum window width, 125% zoom and real maximize/restore interaction. The existing broad Code UI test also checks browser/wordmark separation.
## Verification
- Toolchain: exact pinned pnpm 10.33.4; `pnpm install --frozen-lockfile` passed without lockfile changes.
- Red: `pnpm exec playwright test tests/e2e/pi-coding-first-chat.spec.ts --grep 'Windows Code titlebar' --reporter=list` failed on the actual overlapping button bounds before the fix. The earlier existing-UI assertion independently reproduced the same coordinates.
- Green: the same focused Electron command passed in 4.3 seconds. It covers 1280 px, the application's actual minimum of 1024 px, and 1280 px at 125% content zoom; browser open/closed; sidebar collapsed; running/idle; caption-control hit testing and maximize/restore. The initial test attempt used unsupported 1000 px and was corrected after confirming Main's 1024 px minimum; this was a fixture expectation, not a product defect.
- `pnpm exec playwright test tests/e2e/pi-coding-first-chat.spec.ts --grep 'PI feature UI isolates' --reporter=list`: passed on the fixed production build, including existing browser and Conversation interaction assertions.
- Related unit tests: `title-bar.test.tsx`, `coding-chat-panel.test.tsx`, `agent-browser-panel.test.tsx`: 3 files / 42 tests passed.
- `pnpm run typecheck`, scoped ESLint for all changed TypeScript files, `pnpm run build:vite` (Renderer/Main/Preload/utility), and `git diff --check`: passed.
- Visually inspected the default-size Electron screenshot at `test-results/pi-coding-first-chat-Windo-0b8c4-he-logo-and-window-controls/windows-code-titlebar.png`: browser and stop controls, wordmark and caption controls occupy separate regions. Screenshot/build outputs remain ignored.
- No full unit suite or packaged/installed-client verification was run; the focused tests cover the changed Renderer seam. No temporary product instrumentation was introduced.
## Follow-ups
- Integrate this source branch and rebuild the Windows client for the installed application to receive the correction. This task does not merge main, push or package.
## Promotion Candidates
- None. This restores the existing titlebar separation contract; the README already records the platform-specific behavior.

View File

@@ -144,7 +144,7 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p
- 首次发送会立即生成稳定的乐观用户消息HTTP 202 只表示本地 Agent 已接收。后续失败不会删除已接受消息,不确定交付不会自动重发;准备失败可在目标 Conversation 上手动恢复。
- 运行中的 Conversation 可把新消息作为 steer 引导当前回答或 follow-up 排到下一轮,并显示队列位置;队列正常在 `agent_settled` 后释放,用户可中止当前运行。若 Pi 已终结但该握手丢失Main 会在 30 秒宽限后仅探测目标线程:权威 idle 状态会补齐持久化最终回复并 exactly-once 释放 ownership矛盾状态则显式失败该目标且不会重放已接受请求。select/confirm/input/editor 交互在输入区上方回答select 除预设选项外支持自由输入;交互提交或取消后立即从输入区移除,失效请求会就地提示并允许重试或取消。
- 隐藏 Conversation 的红色新消息标记只在出现新的待回答交互,或当前任务进入 completed、failed、aborted 终态时设置;助手流式文字、思考、工具过程和单个工具失败不会提前触发红点。
- 核心聊天页标题栏左侧只显示 Conversation 名称,必要的中止/恢复入口保留在右侧;不再展示 Pi 空闲/运行副标题。对话列表栏只显示项目名称和项目设置入口不显示“Pi 本地对话”副标题;全局品牌 Logo 贴齐窗口最右边缘。归档、未读、整段 Conversation 分支和编程工具抽屉均不占用标题栏。从已持久化的 user 消息仍可“从这里创建新对话分支”assistant 消息和未持久化消息不提供该动作;分支只创建新的 Conversation 历史不表示文件回滚。Pi 的工具调用与浏览器附件继续在对应的对话过程节点中按需展开;分享、待办、全局运行时和 revert/unrevert 不属于该产品界面。
- 核心聊天页标题栏左侧只显示 Conversation 名称,必要的中止/恢复入口保留在右侧;不再展示 Pi 空闲/运行副标题。对话列表栏只显示项目名称和项目设置入口不显示“Pi 本地对话”副标题;全局品牌 Logo 在 macOS 贴齐窗口最右边缘,在 Windows 位于窗口控制按钮左侧Windows 的开发浏览器、中止和恢复按钮使用 Logo 左侧的独立区域。归档、未读、整段 Conversation 分支和编程工具抽屉均不占用标题栏。从已持久化的 user 消息仍可“从这里创建新对话分支”assistant 消息和未持久化消息不提供该动作;分支只创建新的 Conversation 历史不表示文件回滚。Pi 的工具调用与浏览器附件继续在对应的对话过程节点中按需展开;分享、待办、全局运行时和 revert/unrevert 不属于该产品界面。
## 运行内核与项目数据

View File

@@ -14,12 +14,10 @@ import { useCodingWorkspaceStore } from '@/stores/coding-workspace';
import { useSettingsStore } from '@/stores/settings';
import logoWordmarkSource from '@/assets/makelore-wordmark-source.png';
import type { SidebarPeekSource } from './sidebar-peek';
import { TITLEBAR_LOGO_WIDTH, WINDOWS_TITLEBAR_CONTROLS_WIDTH } from './titlebar-metrics';
type SidebarPeekChange = (open: boolean, source: SidebarPeekSource) => void;
// Three 44px buttons plus the control container's 8px padding on each side.
const WINDOWS_TITLEBAR_CONTROLS_WIDTH = 148;
type TitleBarProps = {
integrated?: boolean;
workspaceLayout?: boolean;
@@ -123,10 +121,11 @@ function ProductTitleBar({
<div
data-testid="titlebar-logo"
className={cn(
'pointer-events-none absolute top-1/2 z-10 h-6 w-24 -translate-y-1/2 overflow-hidden',
'pointer-events-none absolute top-1/2 z-10 h-6 -translate-y-1/2 overflow-hidden',
overlay ? 'bg-transparent' : 'bg-background',
)}
style={{
width: TITLEBAR_LOGO_WIDTH,
right: `${windowControlsWidth}px`,
}}
>

View File

@@ -0,0 +1,4 @@
export const TITLEBAR_LOGO_WIDTH = 96;
// Three 44px buttons plus the control container's 8px padding on each side.
export const WINDOWS_TITLEBAR_CONTROLS_WIDTH = 148;

View File

@@ -20,6 +20,7 @@ import {
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { useSettingsStore } from '@/stores/settings';
import { TITLEBAR_LOGO_WIDTH, WINDOWS_TITLEBAR_CONTROLS_WIDTH } from '@/components/layout/titlebar-metrics';
import type { ConversationSnapshot } from '@/types/coding-conversation';
import type { CodingConversationMetadata } from '@/types/coding-project';
@@ -110,7 +111,8 @@ export function CodingConversationHeader({
<header
className={cn(
'drag-region flex h-10 min-w-0 items-center gap-1 border-b border-border/80 px-3',
hasCustomTitleBar ? 'bg-transparent pr-40' : 'bg-background',
hasCustomTitleBar ? 'bg-transparent' : 'bg-background',
platform === 'darwin' && 'pr-40',
)}
data-testid="coding-conversation-header"
>
@@ -184,7 +186,11 @@ export function CodingConversationHeader({
<div
className="pointer-events-none fixed right-0 top-0 z-[80] h-10"
data-testid="coding-conversation-header-titlebar"
style={{ left: `${sidebarCollapsed ? WORKSPACE_COLUMN_WIDTH : WORKSPACE_COLUMN_WIDTH * 2}px` }}
style={{
left: `${sidebarCollapsed ? WORKSPACE_COLUMN_WIDTH : WORKSPACE_COLUMN_WIDTH * 2}px`,
// Keep both actions and their hit area outside the product chrome.
right: platform === 'win32' ? TITLEBAR_LOGO_WIDTH + WINDOWS_TITLEBAR_CONTROLS_WIDTH : 0,
}}
>
<div className="pointer-events-auto h-full">{header}</div>
</div>,

View File

@@ -1119,6 +1119,78 @@ test('foreground focus rehydrates a terminal Snapshot after lifecycle sleep', as
))).toBe(false);
});
test('Windows Code titlebar keeps browser actions clear of the logo and window controls', async ({
launchElectronApp,
}) => {
test.skip(process.platform !== 'win32', 'Windows custom titlebar only');
const electronApp = await launchElectronApp({ skipSetup: true });
const page = await getStableWindow(electronApp);
const hostConnection = await page.evaluate(async () => ({
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
}));
await installCodingFirstChatHost(electronApp, hostConnection, true);
await disableCodingEventSource(page);
try {
await page.reload();
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('coding-conversation-header')).toBeVisible();
const header = page.getByTestId('coding-conversation-header');
const assertChromeClear = async () => {
const logo = await page.getByTestId('titlebar-logo').boundingBox();
expect(logo).not.toBeNull();
for (const button of await header.getByRole('button').all()) {
const bounds = await button.boundingBox();
expect(bounds).not.toBeNull();
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(logo!.x);
}
const minimize = page.getByTitle('Minimize');
const minimizeBounds = await minimize.boundingBox();
expect(minimizeBounds).not.toBeNull();
expect(logo!.x + logo!.width).toBeLessThanOrEqual(minimizeBounds!.x);
// The conversation header must not intercept clicks in the window controls.
for (const title of ['Minimize', 'Maximize', 'Close']) {
await page.getByTitle(title, { exact: true }).click({ trial: true });
}
};
for (const { width, zoom } of [
{ width: 1280, zoom: 1 },
{ width: 1024, zoom: 1 },
{ width: 1280, zoom: 1.25 },
]) {
await electronApp.evaluate(({ BrowserWindow }, dimensions) => {
const window = BrowserWindow.getAllWindows()[0];
window.setSize(dimensions.width, 800);
window.webContents.setZoomFactor(dimensions.zoom);
}, { width, zoom });
await expect.poll(async () => page.evaluate(() => window.innerWidth)).toBe(Math.round(width / zoom));
await assertChromeClear();
if (width === 1280 && zoom === 1) {
await page.screenshot({ path: test.info().outputPath('windows-code-titlebar.png') });
}
await header.getByRole('button', { name: '打开开发浏览器' }).click();
await expect(page.getByTestId('agent-browser-panel')).toBeVisible();
await assertChromeClear();
await header.getByRole('button', { name: '关闭开发浏览器' }).click();
await expect(page.getByTestId('agent-browser-panel')).toHaveCount(0);
}
await page.getByTestId('titlebar-sidebar-toggle').click();
await assertChromeClear();
await settleSnapshot(electronApp);
await page.evaluate(() => window.dispatchEvent(new FocusEvent('focus')));
await expect(header.getByRole('button', { name: '中止', exact: true })).toHaveCount(0);
await assertChromeClear();
await page.getByTitle('Maximize', { exact: true }).click();
await expect(page.getByTitle('Restore', { exact: true })).toBeVisible();
await page.getByTitle('Restore', { exact: true }).click();
await expect(page.getByTitle('Maximize', { exact: true })).toBeVisible();
} finally {
await releaseSnapshot(electronApp);
}
});
test('PI feature UI isolates Conversations and exposes queue, interaction, model, and subagent state', async ({
launchElectronApp,
}) => {
@@ -1167,6 +1239,11 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
await expect(conversationHeader.getByRole('button', { name: '打开编程工具' })).toHaveCount(0);
const browserToggle = conversationHeader.getByRole('button', { name: '打开开发浏览器' });
await expect(browserToggle).toBeVisible();
if (process.platform === 'win32') {
const browserBounds = await browserToggle.boundingBox();
expect(browserBounds).not.toBeNull();
expect(browserBounds!.x + browserBounds!.width).toBeLessThanOrEqual(logoBounds!.x);
}
await browserToggle.click();
await expect(page.getByTestId('agent-browser-panel')).toBeVisible();
await expect(page.getByTestId('agent-browser-diagnostics')).toHaveAttribute('data-state', 'closed');