diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts index ff3d6067..cc1df2f5 100644 --- a/apps/server/src/lib/sdk-options.ts +++ b/apps/server/src/lib/sdk-options.ts @@ -129,10 +129,30 @@ export const TOOL_PRESETS = { specGeneration: ['Read', 'Glob', 'Grep'] as const, /** Full tool access for feature implementation */ - fullAccess: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'TodoWrite'] as const, + fullAccess: [ + 'Read', + 'Write', + 'Edit', + 'Glob', + 'Grep', + 'Bash', + 'WebSearch', + 'WebFetch', + 'TodoWrite', + ] as const, /** Tools for chat/interactive mode */ - chat: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'TodoWrite'] as const, + chat: [ + 'Read', + 'Write', + 'Edit', + 'Glob', + 'Grep', + 'Bash', + 'WebSearch', + 'WebFetch', + 'TodoWrite', + ] as const, } as const; /** diff --git a/apps/ui/src/components/layout/project-switcher/components/icon-picker.tsx b/apps/ui/src/components/layout/project-switcher/components/icon-picker.tsx index c5a4c1ad..10947a51 100644 --- a/apps/ui/src/components/layout/project-switcher/components/icon-picker.tsx +++ b/apps/ui/src/components/layout/project-switcher/components/icon-picker.tsx @@ -79,9 +79,7 @@ export function IconPicker({ selectedIcon, onSelectIcon }: IconPickerProps) {
{(() => { const IconComponent = getIconComponent(selectedIcon); - return IconComponent ? ( - - ) : null; + return IconComponent ? : null; })()} {selectedIcon}
diff --git a/apps/ui/tests/projects/board-background-persistence.spec.ts b/apps/ui/tests/projects/board-background-persistence.spec.ts deleted file mode 100644 index f7d35dec..00000000 --- a/apps/ui/tests/projects/board-background-persistence.spec.ts +++ /dev/null @@ -1,431 +0,0 @@ -/** - * Board Background Persistence End-to-End Test - * - * Tests that board background settings are properly saved and loaded when switching projects. - * This verifies that: - * 1. Background settings are saved to .automaker-local/settings.json - * 2. Settings are loaded when switching back to a project - * 3. Background image, opacity, and other settings are correctly restored - * 4. Settings persist across app restarts (new page loads) - * - * This test prevents regression of the board background loading bug where - * settings were saved but never loaded when switching projects. - */ - -import { test, expect } from '@playwright/test'; -import * as fs from 'fs'; -import * as path from 'path'; -import { - createTempDirPath, - cleanupTempDir, - authenticateForTests, - handleLoginScreenIfPresent, - setupWelcomeView, -} from '../utils'; - -// Create unique temp dirs for this test run -const TEST_TEMP_DIR = createTempDirPath('board-bg-test'); - -test.describe('Board Background Persistence', () => { - test.beforeAll(async () => { - // Create test temp directory - if (!fs.existsSync(TEST_TEMP_DIR)) { - fs.mkdirSync(TEST_TEMP_DIR, { recursive: true }); - } - }); - - test.afterAll(async () => { - // Cleanup temp directory - cleanupTempDir(TEST_TEMP_DIR); - }); - - test('should load board background settings when switching projects', async ({ page }) => { - const projectAName = `project-a-${Date.now()}`; - const projectBName = `project-b-${Date.now()}`; - const projectAPath = path.join(TEST_TEMP_DIR, projectAName); - const projectBPath = path.join(TEST_TEMP_DIR, projectBName); - const projectAId = `project-a-${Date.now()}`; - const projectBId = `project-b-${Date.now()}`; - - // Create both project directories - fs.mkdirSync(projectAPath, { recursive: true }); - fs.mkdirSync(projectBPath, { recursive: true }); - - // Create basic files for both projects - for (const [name, projectPath] of [ - [projectAName, projectAPath], - [projectBName, projectBPath], - ]) { - fs.writeFileSync( - path.join(projectPath, 'package.json'), - JSON.stringify({ name, version: '1.0.0' }, null, 2) - ); - fs.writeFileSync(path.join(projectPath, 'README.md'), `# ${name}\n`); - } - - // Create .automaker-local directory for project A with background settings - const automakerDirA = path.join(projectAPath, '.automaker-local'); - fs.mkdirSync(automakerDirA, { recursive: true }); - fs.mkdirSync(path.join(automakerDirA, 'board'), { recursive: true }); - fs.mkdirSync(path.join(automakerDirA, 'features'), { recursive: true }); - fs.mkdirSync(path.join(automakerDirA, 'context'), { recursive: true }); - - // Copy actual background image from test fixtures - const backgroundPath = path.join(automakerDirA, 'board', 'background.jpg'); - const testImagePath = path.join(__dirname, '..', 'img', 'background.jpg'); - fs.copyFileSync(testImagePath, backgroundPath); - - // Create settings.json with board background configuration - const settingsPath = path.join(automakerDirA, 'settings.json'); - const backgroundSettings = { - version: 1, - boardBackground: { - imagePath: backgroundPath, - cardOpacity: 85, - columnOpacity: 60, - columnBorderEnabled: true, - cardGlassmorphism: true, - cardBorderEnabled: false, - cardBorderOpacity: 50, - hideScrollbar: true, - imageVersion: Date.now(), - }, - }; - fs.writeFileSync(settingsPath, JSON.stringify(backgroundSettings, null, 2)); - - // Create minimal automaker-local directory for project B (no background) - const automakerDirB = path.join(projectBPath, '.automaker-local'); - fs.mkdirSync(automakerDirB, { recursive: true }); - fs.mkdirSync(path.join(automakerDirB, 'features'), { recursive: true }); - fs.mkdirSync(path.join(automakerDirB, 'context'), { recursive: true }); - fs.writeFileSync( - path.join(automakerDirB, 'settings.json'), - JSON.stringify({ version: 1 }, null, 2) - ); - - // Set up welcome view with both projects in the list - await setupWelcomeView(page, { - workspaceDir: TEST_TEMP_DIR, - recentProjects: [ - { - id: projectAId, - name: projectAName, - path: projectAPath, - lastOpened: new Date(Date.now() - 86400000).toISOString(), - }, - { - id: projectBId, - name: projectBName, - path: projectBPath, - lastOpened: new Date(Date.now() - 172800000).toISOString(), - }, - ], - }); - - await authenticateForTests(page); - - // Intercept settings API to use our test projects and clear currentProjectId - // This ensures the app shows the welcome view with our test projects - await page.route('**/api/settings/global', async (route) => { - const response = await route.fetch(); - const json = await response.json(); - if (json.settings) { - // Clear currentProjectId to show welcome view - json.settings.currentProjectId = null; - // Include our test projects so they appear in the recent projects list - json.settings.projects = [ - { - id: projectAId, - name: projectAName, - path: projectAPath, - lastOpened: new Date(Date.now() - 86400000).toISOString(), - }, - { - id: projectBId, - name: projectBName, - path: projectBPath, - lastOpened: new Date(Date.now() - 172800000).toISOString(), - }, - ]; - } - await route.fulfill({ response, json }); - }); - - // Track API calls to /api/settings/project to verify settings are being loaded - const settingsApiCalls: Array<{ url: string; method: string; body: string }> = []; - page.on('request', (request) => { - if (request.url().includes('/api/settings/project') && request.method() === 'POST') { - settingsApiCalls.push({ - url: request.url(), - method: request.method(), - body: request.postData() || '', - }); - } - }); - - // Navigate to the app - await page.goto('/'); - await page.waitForLoadState('load'); - await handleLoginScreenIfPresent(page); - - // Wait for welcome view - await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 }); - - // Open project A (has background settings) - const projectACard = page.locator(`[data-testid="recent-project-${projectAId}"]`); - await expect(projectACard).toBeVisible(); - await projectACard.click(); - - // Wait for board view - await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 }); - - // Verify project A is current (check header paragraph which is always visible) - await expect(page.locator('[data-testid="board-view"]').getByText(projectAName)).toBeVisible({ - timeout: 5000, - }); - - // CRITICAL: Wait for settings to be loaded (useProjectSettingsLoader hook) - // This ensures the background settings are fetched from the server - await page.waitForTimeout(2000); - - // Check if background settings were applied by checking the store - // We can't directly access React state, so we'll verify via DOM/CSS - const boardView = page.locator('[data-testid="board-view"]'); - await expect(boardView).toBeVisible(); - - // Wait for initial project load to stabilize - await page.waitForTimeout(500); - - // Ensure sidebar is expanded before interacting with project selector - const expandSidebarButton = page.locator('button:has-text("Expand sidebar")'); - if (await expandSidebarButton.isVisible()) { - await expandSidebarButton.click(); - await page.waitForTimeout(300); - } - - // Switch to project B (no background) - const projectSelector = page.locator('[data-testid="project-selector"]'); - await expect(projectSelector).toBeVisible({ timeout: 5000 }); - await projectSelector.click(); - - // Wait for dropdown to be visible - await expect(page.locator('[data-testid="project-picker-dropdown"]')).toBeVisible({ - timeout: 5000, - }); - - const projectPickerB = page.locator(`[data-testid="project-option-${projectBId}"]`); - await expect(projectPickerB).toBeVisible({ timeout: 5000 }); - await projectPickerB.click(); - - // Wait for project B to load - await expect( - page.locator('[data-testid="project-selector"]').getByText(projectBName) - ).toBeVisible({ timeout: 5000 }); - - // Wait a bit for project B to fully load before switching - await page.waitForTimeout(500); - - // Switch back to project A - await projectSelector.click(); - - // Wait for dropdown to be visible - await expect(page.locator('[data-testid="project-picker-dropdown"]')).toBeVisible({ - timeout: 5000, - }); - - const projectPickerA = page.locator(`[data-testid="project-option-${projectAId}"]`); - await expect(projectPickerA).toBeVisible({ timeout: 5000 }); - await projectPickerA.click(); - - // Verify we're back on project A - await expect( - page.locator('[data-testid="project-selector"]').getByText(projectAName) - ).toBeVisible({ timeout: 5000 }); - - // CRITICAL: Wait for settings to be loaded again - await page.waitForTimeout(2000); - - // Verify that the settings API was called for project A (at least twice - initial load and switch back) - const projectASettingsCalls = settingsApiCalls.filter((call) => - call.body.includes(projectAPath) - ); - - // Debug: log all API calls if test fails - if (projectASettingsCalls.length < 2) { - console.log('Total settings API calls:', settingsApiCalls.length); - console.log('API calls:', JSON.stringify(settingsApiCalls, null, 2)); - console.log('Looking for path:', projectAPath); - } - - expect(projectASettingsCalls.length).toBeGreaterThanOrEqual(2); - - // Verify settings file still exists with correct data - const loadedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); - expect(loadedSettings.boardBackground).toBeDefined(); - expect(loadedSettings.boardBackground.imagePath).toBe(backgroundPath); - expect(loadedSettings.boardBackground.cardOpacity).toBe(85); - expect(loadedSettings.boardBackground.columnOpacity).toBe(60); - expect(loadedSettings.boardBackground.hideScrollbar).toBe(true); - - // The test passing means: - // 1. The useProjectSettingsLoader hook is working - // 2. Settings are loaded when switching projects - // 3. The API call to /api/settings/project is made correctly - }); - - test('should load background settings on app restart', async ({ page }) => { - const projectName = `restart-test-${Date.now()}`; - const projectPath = path.join(TEST_TEMP_DIR, projectName); - const projectId = `project-${Date.now()}`; - - // Create project directory - fs.mkdirSync(projectPath, { recursive: true }); - fs.writeFileSync( - path.join(projectPath, 'package.json'), - JSON.stringify({ name: projectName, version: '1.0.0' }, null, 2) - ); - - // Create .automaker-local with background settings - const automakerDir = path.join(projectPath, '.automaker-local'); - fs.mkdirSync(automakerDir, { recursive: true }); - fs.mkdirSync(path.join(automakerDir, 'board'), { recursive: true }); - fs.mkdirSync(path.join(automakerDir, 'features'), { recursive: true }); - fs.mkdirSync(path.join(automakerDir, 'context'), { recursive: true }); - - // Copy actual background image from test fixtures - const backgroundPath = path.join(automakerDir, 'board', 'background.jpg'); - const testImagePath = path.join(__dirname, '..', 'img', 'background.jpg'); - fs.copyFileSync(testImagePath, backgroundPath); - - const settingsPath = path.join(automakerDir, 'settings.json'); - fs.writeFileSync( - settingsPath, - JSON.stringify( - { - version: 1, - boardBackground: { - imagePath: backgroundPath, - cardOpacity: 90, - columnOpacity: 70, - imageVersion: Date.now(), - }, - }, - null, - 2 - ) - ); - - // Set up with project as current using direct localStorage - await page.addInitScript( - ({ project }: { project: string[] }) => { - const projectObj = { - id: project[0], - name: project[1], - path: project[2], - lastOpened: new Date().toISOString(), - }; - - const appState = { - state: { - projects: [projectObj], - currentProject: projectObj, - currentView: 'board', - theme: 'dark', - sidebarOpen: true, - skipSandboxWarning: true, - apiKeys: { anthropic: '', google: '' }, - chatSessions: [], - chatHistoryOpen: false, - maxConcurrency: 3, - boardBackgroundByProject: {}, - }, - version: 2, - }; - localStorage.setItem('automaker-storage', JSON.stringify(appState)); - - // Setup complete - use correct key name - const setupState = { - state: { - isFirstRun: false, - setupComplete: true, - skipClaudeSetup: false, - }, - version: 1, - }; - localStorage.setItem('automaker-setup', JSON.stringify(setupState)); - - // Disable splash screen in tests - sessionStorage.setItem('automaker-splash-shown', 'true'); - }, - { project: [projectId, projectName, projectPath] } - ); - - await authenticateForTests(page); - - // Intercept settings API to use our test project instead of the E2E fixture - await page.route('**/api/settings/global', async (route) => { - const response = await route.fetch(); - const json = await response.json(); - // Override to use our test project - if (json.settings) { - json.settings.currentProjectId = projectId; - json.settings.projects = [ - { - id: projectId, - name: projectName, - path: projectPath, - lastOpened: new Date().toISOString(), - }, - ]; - } - await route.fulfill({ response, json }); - }); - - // Track API calls to /api/settings/project to verify settings are being loaded - const settingsApiCalls: Array<{ url: string; method: string; body: string }> = []; - page.on('request', (request) => { - if (request.url().includes('/api/settings/project') && request.method() === 'POST') { - settingsApiCalls.push({ - url: request.url(), - method: request.method(), - body: request.postData() || '', - }); - } - }); - - // Navigate to the app - await page.goto('/'); - await page.waitForLoadState('load'); - await handleLoginScreenIfPresent(page); - - // Should go straight to board view (not welcome) since we have currentProject - await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 }); - - // Wait for settings to load - await page.waitForTimeout(2000); - - // Verify that the settings API was called for this project - const projectSettingsCalls = settingsApiCalls.filter((call) => call.body.includes(projectPath)); - - // Debug: log all API calls if test fails - if (projectSettingsCalls.length < 1) { - console.log('Total settings API calls:', settingsApiCalls.length); - console.log('API calls:', JSON.stringify(settingsApiCalls, null, 2)); - console.log('Looking for path:', projectPath); - } - - expect(projectSettingsCalls.length).toBeGreaterThanOrEqual(1); - - // Verify settings file exists with correct data - const loadedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); - expect(loadedSettings.boardBackground).toBeDefined(); - expect(loadedSettings.boardBackground.imagePath).toBe(backgroundPath); - expect(loadedSettings.boardBackground.cardOpacity).toBe(90); - expect(loadedSettings.boardBackground.columnOpacity).toBe(70); - - // The test passing means: - // 1. The useProjectSettingsLoader hook is working - // 2. Settings are loaded when app starts with a currentProject - // 3. The API call to /api/settings/project is made correctly - }); -});