diff --git a/apps/ui/src/components/ui/course-promo-badge.tsx b/apps/ui/src/components/ui/course-promo-badge.tsx index 4a888fb6..74353ae6 100644 --- a/apps/ui/src/components/ui/course-promo-badge.tsx +++ b/apps/ui/src/components/ui/course-promo-badge.tsx @@ -7,6 +7,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { useAppStore } from "@/store/app-store"; interface CoursePromoBadgeProps { sidebarOpen?: boolean; @@ -14,8 +15,10 @@ interface CoursePromoBadgeProps { export function CoursePromoBadge({ sidebarOpen = true }: CoursePromoBadgeProps) { const [dismissed, setDismissed] = React.useState(false); + const hideMarketingContent = useAppStore((state) => state.hideMarketingContent); - if (dismissed) { + // If marketing content is hidden globally or dismissed locally, don't render + if (hideMarketingContent || dismissed) { return null; } diff --git a/apps/ui/src/components/ui/log-viewer.tsx b/apps/ui/src/components/ui/log-viewer.tsx index a49a8b23..af339adf 100644 --- a/apps/ui/src/components/ui/log-viewer.tsx +++ b/apps/ui/src/components/ui/log-viewer.tsx @@ -379,7 +379,7 @@ function LogEntryItem({ entry, isExpanded, onToggle }: LogEntryItemProps) { {formattedContent.map((part, index) => (
{part.type === "json" ? ( -
+                    
                       {part.content}
                     
) : ( @@ -418,6 +418,8 @@ export function LogViewer({ output, className }: LogViewerProps) { const [searchQuery, setSearchQuery] = useState(""); const [hiddenTypes, setHiddenTypes] = useState>(new Set()); const [hiddenCategories, setHiddenCategories] = useState>(new Set()); + // Track if user has "Expand All" mode active - new entries will auto-expand when this is true + const [expandAllMode, setExpandAllMode] = useState(false); // Parse entries and compute initial expanded state together const { entries, initialExpandedIds } = useMemo(() => { @@ -442,16 +444,27 @@ export function LogViewer({ output, className }: LogViewerProps) { const appliedInitialRef = useRef>(new Set()); // Apply initial expanded state for new entries + // Also auto-expand all entries when expandAllMode is active const effectiveExpandedIds = useMemo(() => { const result = new Set(expandedIds); - initialExpandedIds.forEach((id) => { - if (!appliedInitialRef.current.has(id)) { - appliedInitialRef.current.add(id); - result.add(id); - } - }); + + // If expand all mode is active, expand all filtered entries + if (expandAllMode) { + entries.forEach((entry) => { + result.add(entry.id); + }); + } else { + // Otherwise, only auto-expand entries based on initial state (shouldCollapseByDefault) + initialExpandedIds.forEach((id) => { + if (!appliedInitialRef.current.has(id)) { + appliedInitialRef.current.add(id); + result.add(id); + } + }); + } + return result; - }, [expandedIds, initialExpandedIds]); + }, [expandedIds, initialExpandedIds, expandAllMode, entries]); // Calculate stats for tool categories const stats = useMemo(() => { @@ -507,6 +520,10 @@ export function LogViewer({ output, className }: LogViewerProps) { }, [entries, hiddenTypes, hiddenCategories, searchQuery]); const toggleEntry = (id: string) => { + // When user manually collapses an entry, turn off expand all mode + if (effectiveExpandedIds.has(id)) { + setExpandAllMode(false); + } setExpandedIds((prev) => { const next = new Set(prev); if (next.has(id)) { @@ -519,10 +536,14 @@ export function LogViewer({ output, className }: LogViewerProps) { }; const expandAll = () => { + // Enable expand all mode so new entries will also be expanded + setExpandAllMode(true); setExpandedIds(new Set(filteredEntries.map((e) => e.id))); }; const collapseAll = () => { + // Disable expand all mode when collapsing all + setExpandAllMode(false); setExpandedIds(new Set()); }; @@ -565,7 +586,7 @@ export function LogViewer({ output, className }: LogViewerProps) {

No log entries yet. Logs will appear here as the process runs.

{output && output.trim() && ( -
+
{output}
)} @@ -699,10 +720,16 @@ export function LogViewer({ output, className }: LogViewerProps) {
); diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index bec00c75..728485bc 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -459,6 +459,9 @@ export interface AppState { // Audio Settings muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false) + // Marketing Settings + hideMarketingContent: boolean; // When true, hide marketing content like the "Become a 10x Dev" badge (default: false) + // Enhancement Model Settings enhancementModel: AgentModel; // Model used for feature enhancement (default: sonnet) @@ -670,6 +673,9 @@ export interface AppActions { // Audio Settings actions setMuteDoneSound: (muted: boolean) => void; + // Marketing Settings actions + setHideMarketingContent: (hide: boolean) => void; + // Enhancement Model actions setEnhancementModel: (model: AgentModel) => void; @@ -824,6 +830,7 @@ const initialState: AppState = { showProfilesOnly: false, // Default to showing all options (not profiles only) keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts muteDoneSound: false, // Default to sound enabled (not muted) + hideMarketingContent: false, // Default to showing marketing content enhancementModel: "sonnet", // Default to sonnet for feature enhancement aiProfiles: DEFAULT_AI_PROFILES, projectAnalysis: null, @@ -1487,6 +1494,9 @@ export const useAppStore = create()( // Audio Settings actions setMuteDoneSound: (muted) => set({ muteDoneSound: muted }), + // Marketing Settings actions + setHideMarketingContent: (hide) => set({ hideMarketingContent: hide }), + // Enhancement Model actions setEnhancementModel: (model) => set({ enhancementModel: model }), @@ -2331,6 +2341,7 @@ export const useAppStore = create()( showProfilesOnly: state.showProfilesOnly, keyboardShortcuts: state.keyboardShortcuts, muteDoneSound: state.muteDoneSound, + hideMarketingContent: state.hideMarketingContent, enhancementModel: state.enhancementModel, // Profiles and sessions aiProfiles: state.aiProfiles, diff --git a/apps/ui/src/styles/global.css b/apps/ui/src/styles/global.css index 6dc31c53..04e15212 100644 --- a/apps/ui/src/styles/global.css +++ b/apps/ui/src/styles/global.css @@ -1657,6 +1657,78 @@ border-radius: 0; } +/* Styled scrollbar for code blocks and log entries (horizontal/vertical) */ +.scrollbar-styled { + scrollbar-width: thin; + scrollbar-color: var(--muted-foreground) transparent; +} + +.scrollbar-styled::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.scrollbar-styled::-webkit-scrollbar-track { + background: transparent; + border-radius: 3px; +} + +.scrollbar-styled::-webkit-scrollbar-thumb { + background: oklch(0.35 0 0); + border-radius: 3px; +} + +.scrollbar-styled::-webkit-scrollbar-thumb:hover { + background: oklch(0.45 0 0); +} + +/* Light mode scrollbar-styled adjustments */ +.light .scrollbar-styled::-webkit-scrollbar-thumb { + background: oklch(0.75 0 0); +} + +.light .scrollbar-styled::-webkit-scrollbar-thumb:hover { + background: oklch(0.65 0 0); +} + +/* Cream theme scrollbar-styled */ +.cream .scrollbar-styled::-webkit-scrollbar-thumb { + background: oklch(0.7 0.03 60); +} + +.cream .scrollbar-styled::-webkit-scrollbar-thumb:hover { + background: oklch(0.6 0.04 60); +} + +/* Retro theme scrollbar-styled */ +.retro .scrollbar-styled::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 0; +} + +.retro .scrollbar-styled::-webkit-scrollbar-track { + background: var(--background); + border-radius: 0; +} + +/* Sunset theme scrollbar-styled */ +.sunset .scrollbar-styled::-webkit-scrollbar-thumb { + background: oklch(0.5 0.14 45); +} + +.sunset .scrollbar-styled::-webkit-scrollbar-thumb:hover { + background: oklch(0.58 0.16 45); +} + +/* Gray theme scrollbar-styled */ +.gray .scrollbar-styled::-webkit-scrollbar-thumb { + background: oklch(0.4 0.01 250); +} + +.gray .scrollbar-styled::-webkit-scrollbar-thumb:hover { + background: oklch(0.5 0.02 250); +} + /* Glass morphism utilities */ @layer utilities { .glass { diff --git a/apps/ui/tests/settings-marketing.spec.ts b/apps/ui/tests/settings-marketing.spec.ts new file mode 100644 index 00000000..9fba1921 --- /dev/null +++ b/apps/ui/tests/settings-marketing.spec.ts @@ -0,0 +1,176 @@ +/** + * Settings Marketing Content Toggle Tests + * + * Tests for the "Hide marketing content" setting in the Appearance section. + */ + +import { test, expect } from "@playwright/test"; +import * as fs from "fs"; + +import { + waitForNetworkIdle, + createTestGitRepo, + cleanupTempDir, + createTempDirPath, + setupProjectWithPathNoWorktrees, + navigateToSettings, +} from "./utils"; + +// Create unique temp dir for this test run +const TEST_TEMP_DIR = createTempDirPath("settings-marketing-tests"); + +interface TestRepo { + path: string; + cleanup: () => Promise; +} + +// Configure all tests to run serially +test.describe.configure({ mode: "serial" }); + +test.describe("Settings Marketing Content Tests", () => { + let testRepo: TestRepo; + + test.beforeAll(async () => { + // Create test temp directory + if (!fs.existsSync(TEST_TEMP_DIR)) { + fs.mkdirSync(TEST_TEMP_DIR, { recursive: true }); + } + }); + + test.beforeEach(async () => { + // Create a fresh test repo for each test + testRepo = await createTestGitRepo(TEST_TEMP_DIR); + }); + + test.afterEach(async () => { + // Cleanup test repo after each test + if (testRepo) { + await testRepo.cleanup(); + } + }); + + test.afterAll(async () => { + // Cleanup temp directory + cleanupTempDir(TEST_TEMP_DIR); + }); + + test("should show course promo badge by default", async ({ page }) => { + // Setup project without worktrees for simpler testing + await setupProjectWithPathNoWorktrees(page, testRepo.path); + await page.goto("/"); + await waitForNetworkIdle(page); + + // Wait for sidebar to load + await expect(page.locator('[data-testid="sidebar"]')).toBeVisible({ + timeout: 10000, + }); + + // Course promo badge should be visible by default + const promoBadge = page.locator('[data-testid="course-promo-badge"]'); + await expect(promoBadge).toBeVisible({ timeout: 5000 }); + }); + + test("should hide course promo badge when setting is enabled", async ({ + page, + }) => { + // Setup project + await setupProjectWithPathNoWorktrees(page, testRepo.path); + await page.goto("/"); + await waitForNetworkIdle(page); + + // Navigate to settings + await navigateToSettings(page); + + // Click on Appearance tab in settings navigation + const appearanceTab = page.getByRole("button", { name: /appearance/i }); + await appearanceTab.click(); + + // Find and click the hide marketing content checkbox + const hideMarketingCheckbox = page.locator( + '[data-testid="hide-marketing-content-checkbox"]' + ); + await expect(hideMarketingCheckbox).toBeVisible({ timeout: 5000 }); + await hideMarketingCheckbox.click(); + + // Navigate back to board to see the sidebar + await page.goto("/board"); + await waitForNetworkIdle(page); + + // Course promo badge should now be hidden + const promoBadge = page.locator('[data-testid="course-promo-badge"]'); + await expect(promoBadge).not.toBeVisible({ timeout: 5000 }); + }); + + test("should persist hide marketing setting across page reloads", async ({ + page, + }) => { + // Setup project + await setupProjectWithPathNoWorktrees(page, testRepo.path); + await page.goto("/"); + await waitForNetworkIdle(page); + + // Navigate to settings and enable hide marketing + await navigateToSettings(page); + + const appearanceTab = page.getByRole("button", { name: /appearance/i }); + await appearanceTab.click(); + + const hideMarketingCheckbox = page.locator( + '[data-testid="hide-marketing-content-checkbox"]' + ); + await hideMarketingCheckbox.click(); + + // Reload the page + await page.reload(); + await waitForNetworkIdle(page); + + // Course promo badge should still be hidden after reload + const promoBadge = page.locator('[data-testid="course-promo-badge"]'); + await expect(promoBadge).not.toBeVisible({ timeout: 5000 }); + }); + + test("should show course promo badge again when setting is disabled", async ({ + page, + }) => { + // Setup project with hide marketing already enabled via localStorage + await page.addInitScript(() => { + const state = { + state: { + hideMarketingContent: true, + projects: [], + currentProject: null, + theme: "dark", + sidebarOpen: true, + }, + version: 2, + }; + localStorage.setItem("automaker-storage", JSON.stringify(state)); + }); + + await setupProjectWithPathNoWorktrees(page, testRepo.path); + await page.goto("/"); + await waitForNetworkIdle(page); + + // Verify promo is hidden initially + const promoBadge = page.locator('[data-testid="course-promo-badge"]'); + await expect(promoBadge).not.toBeVisible({ timeout: 5000 }); + + // Navigate to settings and disable hide marketing + await navigateToSettings(page); + + const appearanceTab = page.getByRole("button", { name: /appearance/i }); + await appearanceTab.click(); + + const hideMarketingCheckbox = page.locator( + '[data-testid="hide-marketing-content-checkbox"]' + ); + await hideMarketingCheckbox.click(); // Uncheck + + // Navigate back to board + await page.goto("/board"); + await waitForNetworkIdle(page); + + // Course promo badge should now be visible again + await expect(promoBadge).toBeVisible({ timeout: 5000 }); + }); +});