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)) }} />