From fb4b9bdd46380a8bd4967110d42129899e7baa44 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Wed, 23 Sep 2026 15:27:33 +0800 Subject: [PATCH] fix: float Git branch picker above workspace content Render the searchable branch picker in the shared floating portal so opening it no longer pushes down changes or commit history. Clamp it to the viewport and retain search focus, outside dismissal, and Escape focus restoration. Update existing migration fixtures for activity history and supervision schema changes. Focused suites passed: 149 storage tests, 41 UI tests, and the Electron overlay test; typecheck and lint passed. The full suite did not complete cleanly and reported failures outside this change. Release note: The Git branch picker now opens over the workspace without displacing the file list or commit history. --- docs/features/assistant-workbar/prd.md | 5 +- src/main/assistant/assistant-database.test.ts | 62 ++++++++++++++++++- src/main/assistant/heartbeat-database.test.ts | 12 +++- .../assistant/supervision-history.test.ts | 5 +- .../magic-notes/magic-note-storage.test.ts | 5 +- src/renderer/src/WorkspaceFilesPanel.test.tsx | 10 ++- src/renderer/src/WorkspaceGitTools.tsx | 60 ++++++++++++++++-- src/renderer/src/styles.css | 6 +- tests/support/overlay-layering-driver.mjs | 19 ++++++ tests/support/overlay-layering-regression.tsx | 8 +++ 10 files changed, 176 insertions(+), 16 deletions(-) diff --git a/docs/features/assistant-workbar/prd.md b/docs/features/assistant-workbar/prd.md index 549064fb..fae35711 100644 --- a/docs/features/assistant-workbar/prd.md +++ b/docs/features/assistant-workbar/prd.md @@ -419,8 +419,9 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发 不自动 force、reset、discard 或 stash,不因浏览分支而自动 Fetch。 分支触发器复用共享模型选择按钮,带分支图标和展开箭头;Fetch 使用共享次按钮,二者在 紧凑工作栏内保持同一行,长分支名省略并提供完整名称提示。 - 展开的分支选择面板不超过 360px 或工作栏可用宽度;搜索使用共享表单标签、输入框与主题样式, - 分支列表独立限高滚动,创建按钮以紧凑次操作放在列表下方。Escape 收起面板并将焦点返回分支触发器。 + 分支选择面板浮在内容上方,展开时不挤占变更列表和提交历史;宽度不超过 360px,并限制在窗口内。 + 搜索使用共享表单标签、输入框与主题样式,分支列表独立限高滚动,创建按钮以紧凑次操作放在列表下方。 + 点击面板外或焦点移出时收起;Escape 收起面板并将焦点返回分支触发器。 - FR-W5:未提交更改支持列表和目录树切换;两种视图均可打开单文件 Diff。 - FR-W6:Git 工作区底部提供默认收起的提交历史区,独立滚动,可调整高度。 历史标题默认停在面板底部,首次展开向上占用当前 Git 内容区的一半高度;收起后标题回到底部。 diff --git a/src/main/assistant/assistant-database.test.ts b/src/main/assistant/assistant-database.test.ts index c5c988a9..2bc7e662 100644 --- a/src/main/assistant/assistant-database.test.ts +++ b/src/main/assistant/assistant-database.test.ts @@ -168,7 +168,11 @@ it('migrates schema 35 conversations to unpinned while retaining existing data a const original = database.getConversation(header.id) database.close() const legacy = new DatabaseSync(path) - legacy.exec('ALTER TABLE conversations DROP COLUMN pinned; PRAGMA user_version = 35;') + legacy.exec(`ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; + PRAGMA user_version = 35;`) legacy.close() try { database.initialize('C:\\Workspace') @@ -311,6 +315,8 @@ it('preserves existing supervision data when upgrading schema 41 and reopening', ALTER TABLE supervision_entity_changes DROP COLUMN source_reference_ids_json; ALTER TABLE supervision_relations DROP COLUMN source_reference_ids_json; ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 41;`) legacy.close() try { @@ -1068,6 +1074,9 @@ describe('AssistantDatabase', () => { DROP TABLE model_usage_calls; ALTER TABLE projects DROP COLUMN runtime_selection_json; ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 3; `) oldDatabase.close() @@ -1225,6 +1234,9 @@ describe('AssistantDatabase', () => { legacy.exec(` DROP TABLE project_execution_spaces; ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 26; `) legacy.close() @@ -1335,6 +1347,9 @@ describe('AssistantDatabase', () => { runtime_bundle_digest TEXT NOT NULL ); ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 30; `) legacy.close() @@ -1407,6 +1422,9 @@ describe('AssistantDatabase', () => { (project_id, runtime_bundle_digest) VALUES ('${project.id}', 'sha256:${'d'.repeat(64)}'); ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 30; `) legacy.close() @@ -1498,6 +1516,9 @@ describe('AssistantDatabase', () => { DROP INDEX projects_built_in_default_unique; ALTER TABLE projects DROP COLUMN built_in_default; ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 24; `) legacy.close() @@ -1539,6 +1560,9 @@ describe('AssistantDatabase', () => { DROP INDEX projects_built_in_default_unique; ALTER TABLE projects DROP COLUMN built_in_default; ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 24; `) legacy.close() @@ -1581,6 +1605,9 @@ describe('AssistantDatabase', () => { DROP INDEX projects_built_in_default_unique; ALTER TABLE projects DROP COLUMN built_in_default; ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 24; `) legacy.close() @@ -1619,6 +1646,9 @@ describe('AssistantDatabase', () => { DROP INDEX projects_built_in_default_unique; ALTER TABLE projects DROP COLUMN built_in_default; ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 24; `) legacy.close() @@ -1646,6 +1676,9 @@ describe('AssistantDatabase', () => { DROP INDEX projects_built_in_default_unique; ALTER TABLE projects DROP COLUMN built_in_default; ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 24; `) legacy.close() @@ -1670,6 +1703,9 @@ describe('AssistantDatabase', () => { versionFive.exec(` DROP TABLE computer_control_actions; ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 5; `) versionFive.close() @@ -1791,6 +1827,9 @@ describe('AssistantDatabase', () => { raw.exec(` DROP INDEX IF EXISTS idx_tasks_schedule; ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 21; COMMIT; `) @@ -1881,6 +1920,9 @@ describe('AssistantDatabase', () => { legacy.exec(` DELETE FROM magic_todos; ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 9; `) legacy.close() @@ -1932,7 +1974,11 @@ describe('AssistantDatabase', () => { now, now ) - legacy.exec('ALTER TABLE conversations DROP COLUMN pinned; PRAGMA user_version = 16') + legacy.exec(`ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; + PRAGMA user_version = 16`) legacy.close() const migrated = new AssistantDatabase(databasePath) @@ -2877,7 +2923,11 @@ describe('AssistantDatabase', () => { WHERE id = ?` ) .run(entry.id) - legacy.exec('ALTER TABLE conversations DROP COLUMN pinned; PRAGMA user_version = 23') + legacy.exec(`ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; + PRAGMA user_version = 23`) legacy.close() const migrated = new AssistantDatabase(databasePath) @@ -3013,6 +3063,9 @@ describe('AssistantDatabase', () => { INSERT INTO channel_events(channel, event_id, claimed_at) VALUES ('weixin', 'legacy-event', 1); ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 18; `) legacy.close() @@ -3520,6 +3573,9 @@ describe('AssistantDatabase', () => { ALTER TABLE task_events DROP COLUMN remote_operation_id; ALTER TABLE task_events DROP COLUMN remote_binding_id; ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; PRAGMA user_version = 31; `) legacy.close() diff --git a/src/main/assistant/heartbeat-database.test.ts b/src/main/assistant/heartbeat-database.test.ts index 5e2bbb09..515dda3b 100644 --- a/src/main/assistant/heartbeat-database.test.ts +++ b/src/main/assistant/heartbeat-database.test.ts @@ -76,7 +76,11 @@ describe('AssistantDatabase heartbeat persistence', () => { database.close() const raw = new DatabaseSync(path) - raw.exec('ALTER TABLE conversations DROP COLUMN pinned; PRAGMA user_version = 2') + raw.exec(`ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; + PRAGMA user_version = 2`) raw.close() const migrated = new AssistantDatabase(path) @@ -128,7 +132,11 @@ describe('AssistantDatabase heartbeat persistence', () => { 'DELETE FROM heartbeat_config_projects WHERE config_id = ?' ) .run(config.id) - raw.exec('ALTER TABLE conversations DROP COLUMN pinned; PRAGMA user_version = 20') + raw.exec(`ALTER TABLE conversations DROP COLUMN pinned; + ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; + PRAGMA user_version = 20`) raw.close() const migrated = new AssistantDatabase(path) diff --git a/src/main/assistant/supervision-history.test.ts b/src/main/assistant/supervision-history.test.ts index 06ab2a9e..2efc4b32 100644 --- a/src/main/assistant/supervision-history.test.ts +++ b/src/main/assistant/supervision-history.test.ts @@ -37,7 +37,10 @@ it('supervision preserves result history and protected identities within scope t const original = read(a.id) db.close() const legacy = new DatabaseSync(path) - legacy.exec('ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; PRAGMA user_version = 42;') + legacy.exec(`ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; + PRAGMA user_version = 42;`) legacy.close() db.initialize(process.cwd()) expect(read(a.id)).toEqual(original) diff --git a/src/main/magic-notes/magic-note-storage.test.ts b/src/main/magic-notes/magic-note-storage.test.ts index 70954a24..089d31b8 100644 --- a/src/main/magic-notes/magic-note-storage.test.ts +++ b/src/main/magic-notes/magic-note-storage.test.ts @@ -218,7 +218,10 @@ describe('Magic note SQLite and filesystem storage', () => { const entry = note.entries[0]! sql.prepare('UPDATE magic_note_entries SET content_json = ? WHERE id = ?').run(JSON.stringify(entry.content), entry.id) } - sql.exec('PRAGMA user_version = 37') + sql.exec(`ALTER TABLE supervision_results DROP COLUMN graph_snapshot_json; + DROP TABLE activity_history_records; + ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json; + PRAGMA user_version = 37`) rmSync(join(directory, 'notes'), { recursive: true }) const progress: number[] = [] upgradeAssistantStorage(path, (value) => progress.push(value.processed)) diff --git a/src/renderer/src/WorkspaceFilesPanel.test.tsx b/src/renderer/src/WorkspaceFilesPanel.test.tsx index 1b08332b..c9c86072 100644 --- a/src/renderer/src/WorkspaceFilesPanel.test.tsx +++ b/src/renderer/src/WorkspaceFilesPanel.test.tsx @@ -69,7 +69,7 @@ describe('WorkspaceFilesPanel', () => { expect(screen.queryByRole('navigation', { name: '路径' })).not.toBeInTheDocument() }) - it('styles branch search as a shared field and preserves filtering, switching and creation', async () => { + it('floats branch search outside the workspace and preserves filtering, switching and creation', async () => { let current = 'main' const manage = vi.fn(async (_project: string, action: WorkspaceManagementAction): Promise => { if (action.kind === 'switchBranch' || action.kind === 'createBranch') { @@ -90,6 +90,10 @@ describe('WorkspaceFilesPanel', () => { const trigger = await screen.findByRole('button', { name: 'main' }) fireEvent.click(trigger) const search = screen.getByRole('textbox', { name: '搜索本地或远程分支 / 新分支名称' }) + const panel = screen.getByRole('dialog', { name: '分支' }) + expect(panel.closest('.workspace-git')).toBeNull() + expect(panel.closest('.floating-portal')).not.toBeNull() + expect(trigger).toHaveAttribute('aria-controls', panel.id) expect(search.closest('label')).toHaveClass('field') expect(search.previousElementSibling?.tagName).toBe('SPAN') expect(search).toHaveFocus() @@ -110,6 +114,9 @@ describe('WorkspaceFilesPanel', () => { fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' }) expect(screen.queryByRole('textbox')).not.toBeInTheDocument() expect(trigger).toHaveFocus() + fireEvent.click(trigger) + fireEvent.pointerDown(document.body) + expect(screen.queryByRole('dialog', { name: '分支' })).not.toBeInTheDocument() expect(manage.mock.calls.some(([, action]) => action.kind === 'fetch')).toBe(false) }) @@ -205,6 +212,7 @@ describe('WorkspaceFilesPanel', () => { expect(css).toMatch(/\.workspace-git__branch-trigger\s*\{[^}]*width: auto;[^}]*height: 32px;[^}]*flex: 0 1 auto;[^}]*text-align: left/) expect(css).toMatch(/\.workspace-files__header\s*\{[^}]*justify-content: space-between/) expect(css).toMatch(/\.workspace-git__branches\s*\{[^}]*width: min\(100%, 360px\);[^}]*border-radius: var\(--radius-control\)/) + expect(css).toMatch(/\.workspace-git__branches\s*\{[^}]*position: fixed;[^}]*max-height: calc\(100vh - 32px\);[^}]*overflow: auto/) expect(css).toMatch(/\.workspace-git__branch-list\s*\{[^}]*max-height: 240px;[^}]*overflow: auto/) expect(css).toMatch(/\.workspace-git__create-branch\s*\{[^}]*justify-self: start/) }) diff --git a/src/renderer/src/WorkspaceGitTools.tsx b/src/renderer/src/WorkspaceGitTools.tsx index 465d019a..66bb99ec 100644 --- a/src/renderer/src/WorkspaceGitTools.tsx +++ b/src/renderer/src/WorkspaceGitTools.tsx @@ -1,7 +1,8 @@ -import { useEffect, useEffectEvent, useRef, useState } from 'react' +import { useEffect, useEffectEvent, useId, useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { ChevronDown, ChevronLeft, ChevronRight, Download, FileText, GitBranch } from 'lucide-react' import type { WorkspaceManagementAction, WorkspaceManagementResult } from '../../shared/workspace-management-contracts' +import { FloatingPortal } from './FloatingPortal' type Result = Extract @@ -27,6 +28,8 @@ export function WorkspaceGitTools({ projectId, refreshToken, onRefresh, viewCont const returnTrigger = useRef(null) const fileTrigger = useRef(null) const branchTrigger = useRef(null) + const branchPanel = useRef(null) + const branchPanelId = useId() const diffBack = useRef(null) const commitBack = useRef(null) const action = (value: WorkspaceManagementAction): Promise => window.goodbuddy.workspace.manage(projectId, value) @@ -43,6 +46,53 @@ export function WorkspaceGitTools({ projectId, refreshToken, onRefresh, viewCont return () => { branchCounter.current++ } }, [projectId, refreshToken]) useEffect(() => { if (path) diffBack.current?.focus(); else if (commit) commitBack.current?.focus() }, [path, commit]) + const positionBranches = useEffectEvent(() => { + const panel = branchPanel.current + const anchor = branchTrigger.current + if (!panel || !anchor) return + const viewport = window.visualViewport + const left = viewport?.offsetLeft ?? 0 + const top = viewport?.offsetTop ?? 0 + const width = viewport?.width ?? window.innerWidth + const height = viewport?.height ?? window.innerHeight + const rect = anchor.getBoundingClientRect() + const panelWidth = Math.max(0, Math.min(360, width - 32)) + panel.style.width = `${panelWidth}px` + panel.style.maxHeight = `${Math.max(0, height - 32)}px` + const panelHeight = panel.getBoundingClientRect().height + const below = rect.bottom + 8 + const y = below + panelHeight <= top + height - 16 ? below : rect.top - 8 - panelHeight + panel.style.left = `${Math.max(left + 16, Math.min(rect.left, left + width - panelWidth - 16))}px` + panel.style.top = `${Math.max(top + 16, Math.min(y, top + height - panelHeight - 16))}px` + }) + useLayoutEffect(() => { if (branchOpen) positionBranches() }) + useEffect(() => { + if (!branchOpen) return + branchPanel.current?.querySelector('input')?.focus({ preventScroll: true }) + const outside = (event: Event): void => { + if (event.target instanceof Node && !branchTrigger.current?.contains(event.target) && + !branchPanel.current?.contains(event.target)) setBranchOpen(false) + } + const reposition = (): void => positionBranches() + const observer = new ResizeObserver(reposition) + if (branchPanel.current) observer.observe(branchPanel.current) + if (branchTrigger.current) observer.observe(branchTrigger.current) + document.addEventListener('pointerdown', outside) + document.addEventListener('focusin', outside) + window.addEventListener('resize', reposition) + window.addEventListener('scroll', reposition, true) + window.visualViewport?.addEventListener('resize', reposition) + window.visualViewport?.addEventListener('scroll', reposition) + return () => { + observer.disconnect() + document.removeEventListener('pointerdown', outside) + document.removeEventListener('focusin', outside) + window.removeEventListener('resize', reposition) + window.removeEventListener('scroll', reposition, true) + window.visualViewport?.removeEventListener('resize', reposition) + window.visualViewport?.removeEventListener('scroll', reposition) + } + }, [branchOpen]) const run = async (value: WorkspaceManagementAction): Promise => { if (busy) return setBusy(true); setError('') @@ -92,16 +142,16 @@ export function WorkspaceGitTools({ projectId, refreshToken, onRefresh, viewCont
- +
{viewControl}
- {branchOpen &&
{ if (event.key === 'Escape') { setBranchOpen(false); branchTrigger.current?.focus() } }}> - + {branchOpen && } +
}
{children}
diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css index b4d825f1..af0c423f 100644 --- a/src/renderer/src/styles.css +++ b/src/renderer/src/styles.css @@ -17995,14 +17995,18 @@ details.settings-section > :not(summary) + :not(summary) { border-color: var(--border-default); } .workspace-git__branches { + position: fixed; display: grid; width: min(100%, 360px); + max-height: calc(100vh - 32px); + overflow: auto; min-width: 0; gap: var(--space-2); padding: var(--space-3); border: 1px solid var(--border-default); border-radius: var(--radius-control); - background: var(--surface-subtle); + background: var(--surface-raised); + box-shadow: var(--shadow-dialog); color: var(--text-primary); font-size: var(--font-body); } diff --git a/tests/support/overlay-layering-driver.mjs b/tests/support/overlay-layering-driver.mjs index a3073fd5..a676cc02 100644 --- a/tests/support/overlay-layering-driver.mjs +++ b/tests/support/overlay-layering-driver.mjs @@ -57,6 +57,25 @@ app.whenReady().then(async () => { await js(`window.dispatchEvent(new CustomEvent('fixture-theme', { detail: '${theme}' }))`) for (const [width, height] of [[1280, 800], [960, 720], [720, 640], [640, 420]]) { win.setContentSize(width, height); await settle() + const workspaceBounds = () => js(`['.workspace-git__changes', '.workspace-git__history-dock'].map(selector => { + const r = document.querySelector(selector).getBoundingClientRect(); return [r.x, r.y, r.width, r.height] + })`) + const closedBounds = await workspaceBounds() + await click('.workspace-git__branch-trigger') + await wait('!!document.querySelector(".workspace-git__branches")') + assert.deepEqual(await workspaceBounds(), closedBounds, 'Branch popup moved workspace content') + assert(await js(`(() => { + const panel = document.querySelector('.workspace-git__branches') + const r = panel.getBoundingClientRect() + return getComputedStyle(panel).position === 'fixed' && !!panel.closest('.floating-portal') && + !panel.closest('.workspace-git') && r.left >= 16 && r.top >= 16 && + r.right <= innerWidth - 16 && r.bottom <= innerHeight - 16 && + document.activeElement.matches('.workspace-git__branches input') + })()`), 'Branch popup must float within viewport and focus search') + await click('.workspace-git__branches input') + await screenshot(`${theme}-${width}-branch-menu`) + await key('Escape') + assert(await js('!document.querySelector(".workspace-git__branches") && document.activeElement.matches(".workspace-git__branch-trigger")')) await notify() await click('#settings') await wait('!!document.querySelector("[aria-haspopup=menu][aria-expanded=false]")') diff --git a/tests/support/overlay-layering-regression.tsx b/tests/support/overlay-layering-regression.tsx index 2aefbfd7..a4c421dd 100644 --- a/tests/support/overlay-layering-regression.tsx +++ b/tests/support/overlay-layering-regression.tsx @@ -4,6 +4,7 @@ import { AppNotificationViewport } from '../../src/renderer/src/App' import { SettingsPanel } from '../../src/renderer/src/SettingsPanel' import { ApplicationCenter } from '../../src/renderer/src/ApplicationCenter' import LocalInferencePage from '../../src/renderer/src/LocalInferencePage' +import { WorkspaceGitTools } from '../../src/renderer/src/WorkspaceGitTools' import { defaultRuntimeSettings } from '../../src/shared/contracts' import { UiLocaleProvider } from '../../src/renderer/src/i18n/UiLocaleProvider' import { applyAppearanceTheme } from '../../src/renderer/src/theme' @@ -16,6 +17,8 @@ const snapshot = { ocrModels: { rootDirectory: '', selectedDownloadSource: 'modelscope', catalog: [], installed: [], operations: [] } } Object.defineProperty(window, 'goodbuddy', { value: { + workspace: { manage: async () => ({ kind: 'branches', current: 'main', branches: + Array.from({ length: 20 }, (_, index) => ({ name: index ? `feature/branch-${index}` : 'main', remote: false })) }) }, settings: { getRuntime: async () => ({ ...defaultRuntimeSettings, modelProfiles: [], embeddingConnections: [], opencodeModelSource: { kind: 'platform' }, continueModelSource: { kind: 'platform' }, deepseekHarnessModelSource: { kind: 'platform' } }), detectAgentRuntimes: async () => ({}) }, @@ -44,6 +47,11 @@ function Fixture(): React.JSX.Element {
+
+ {}} viewControl={null}> +
Uncommitted workspace changes
+
+
{ if ('dismiss' in action) setNotifications(items => items.filter(item => item.id !== action.dismiss)) }} />