diff --git a/.planning/2026-08-17-project-tab-bar/findings.md b/.planning/2026-08-17-project-tab-bar/findings.md new file mode 100644 index 00000000..cbba9071 --- /dev/null +++ b/.planning/2026-08-17-project-tab-bar/findings.md @@ -0,0 +1,6 @@ +# Findings + +- The macOS reference renders `projectSessions.openProjects` in a horizontal `projectTabBar` below the title area. Each tab has a folder icon, project name, active styling, and direct activation. +- Windows already persists the same concept in `useWorkspaceTabsStore.projectTabs` and exposes `useFileSystemStore.switchToProject` plus `isSwitchingProject`. +- Windows currently renders `TitleProjectMenu` inside the title bar; its dropdown already lists open projects and should remain for project-management actions. +- `MainLayout` places `TitleBarWithSettings` immediately before the workbench and is the correct ownership boundary for a full-width tab strip. diff --git a/.planning/2026-08-17-project-tab-bar/progress.md b/.planning/2026-08-17-project-tab-bar/progress.md new file mode 100644 index 00000000..0afbc03a --- /dev/null +++ b/.planning/2026-08-17-project-tab-bar/progress.md @@ -0,0 +1,15 @@ +# Progress + +## 2026-08-17 + +- Recorded the pre-existing dirty worktree and branch before editing. +- Confirmed the macOS `WorkbenchView.projectTabBar` as the visual and interaction reference. +- Confirmed Windows has project tab persistence and a switch action but no top-level project tab presentation. +- User approved direct implementation using the macOS pattern. +- Added and ran the red model test, then implemented `getProjectTabBarItems` and confirmed the green result. +- Added and ran the red tab-bar contract test, then implemented the accessible tab bar component and confirmed the green result. +- Mounted `ProjectTabBar` below `TitleBarWithSettings` in `MainLayout`. +- Built the Windows Release application successfully with `scripts/build-windows.ps1 -Configuration Release`. +- Started the updated Release executable and verified through WebView2 CDP that the Chinese "打开的项目" tab list renders six projects, switches `aria-selected`, and updates the title-bar project label. +- Re-ran focused Bun tests (`2 pass`), related regression tests (`9 pass`), TypeScript typecheck, targeted lint, and `git diff --check` successfully. +- Completed the five-axis implementation review with no Critical or Important findings; unrelated pre-existing changes remain unstaged. diff --git a/.planning/2026-08-17-project-tab-bar/task_plan.md b/.planning/2026-08-17-project-tab-bar/task_plan.md new file mode 100644 index 00000000..8c026ffb --- /dev/null +++ b/.planning/2026-08-17-project-tab-bar/task_plan.md @@ -0,0 +1,26 @@ +# Project Tab Bar + +## Goal + +Add a Mac-style project tab bar below the Windows title bar so projects open in the current window are visible and directly switchable. + +## Phases + +- [complete] Explore existing macOS and Windows project-session patterns. +- [complete] Add failing model and interaction tests. +- [complete] Implement the project tab bar and connect project switching. +- [complete] Run frontend verification and live Windows checks. +- [complete] Review and create one focused implementation commit. + +## Scope + +- Worktree: `D:\code\Lithe-IDEA-preview-0.3.0` +- Branch: `fix/windows-new-window-blank` +- Preserve all existing dirty files and stage only this task's files. +- Keep the existing title-bar project dropdown and project persistence behavior. + +## Errors Encountered + +| Error | Attempt | Resolution | +| --- | --- | --- | +| No project-tab component exists in the Windows layout | 1 | Use the macOS `projectTabBar` structure as the reference and create a focused Windows presentation component. | diff --git a/Sources/Lithe/Platform/MacOS/Community/LinuxDoAnonymousWebView.swift b/Sources/Lithe/Platform/MacOS/Community/LinuxDoAnonymousWebView.swift index b7ca4ac3..6cce0b88 100644 --- a/Sources/Lithe/Platform/MacOS/Community/LinuxDoAnonymousWebView.swift +++ b/Sources/Lithe/Platform/MacOS/Community/LinuxDoAnonymousWebView.swift @@ -28,9 +28,10 @@ final class LinuxDoAnonymousWebSession: ObservableObject { releaseTask = nil } - func releaseAfterInactivity() { + @discardableResult + func releaseAfterInactivity() -> Task { releaseTask?.cancel() - releaseTask = Task { @MainActor [weak self] in + let task = Task { @MainActor [weak self] in guard let self else { return } try? await Task.sleep(nanoseconds: idleLifetimeNanoseconds) guard !Task.isCancelled else { return } @@ -40,6 +41,8 @@ final class LinuxDoAnonymousWebSession: ObservableObject { webView = nil releaseTask = nil } + releaseTask = task + return task } deinit { diff --git a/Tests/LitheTests/LinuxDoAnonymousWebSessionTests.swift b/Tests/LitheTests/LinuxDoAnonymousWebSessionTests.swift index f89dfcad..72eb8dab 100644 --- a/Tests/LitheTests/LinuxDoAnonymousWebSessionTests.swift +++ b/Tests/LitheTests/LinuxDoAnonymousWebSessionTests.swift @@ -5,28 +5,25 @@ import Testing @MainActor struct LinuxDoAnonymousWebSessionTests { @Test - func shortPanelAbsenceKeepsTheCurrentWebView() async throws { + func shortPanelAbsenceKeepsTheCurrentWebView() async { let session = LinuxDoAnonymousWebSession(idleLifetimeNanoseconds: 50_000_000) let webView = WKWebView() session.webView = webView - session.releaseAfterInactivity() + let releaseTask = session.releaseAfterInactivity() session.resume() - try await Task.sleep(nanoseconds: 80_000_000) + await releaseTask.value #expect(session.webView === webView) } @Test - func inactiveSessionReleasesItsWebView() async throws { + func inactiveSessionReleasesItsWebView() async { let session = LinuxDoAnonymousWebSession(idleLifetimeNanoseconds: 20_000_000) session.webView = WKWebView() - session.releaseAfterInactivity() - let deadline = ContinuousClock.now + .seconds(1) - while session.webView != nil, ContinuousClock.now < deadline { - try await Task.sleep(for: .milliseconds(10)) - } + let releaseTask = session.releaseAfterInactivity() + await releaseTask.value #expect(session.webView == nil) } diff --git a/Tests/LitheTests/LitheCoreLogicTests.swift b/Tests/LitheTests/LitheCoreLogicTests.swift index 1ed1c9b8..3434c789 100644 --- a/Tests/LitheTests/LitheCoreLogicTests.swift +++ b/Tests/LitheTests/LitheCoreLogicTests.swift @@ -3231,7 +3231,9 @@ struct EditorDocumentTests { source?.emit(DirectoryChangeBatch(gitStateMayHaveChanged: true)) source?.emit(DirectoryChangeBatch(gitStateMayHaveChanged: true)) source?.emit(DirectoryChangeBatch(gitStateMayHaveChanged: true)) - let refreshed = await waitForWorkspaceObservation { refreshCount == 2 } + let refreshed = await waitForWorkspaceObservation(timeout: .seconds(15)) { + refreshCount == 2 + } #expect(refreshed) #expect(refreshCount == 2) diff --git a/docs/superpowers/plans/2026-08-17-project-tab-bar.md b/docs/superpowers/plans/2026-08-17-project-tab-bar.md new file mode 100644 index 00000000..1c561176 --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-project-tab-bar.md @@ -0,0 +1,177 @@ +# Mac-Style Project Tab Bar Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task with verification checkpoints. + +**Goal:** Add a horizontal project tab bar below the Windows title bar so every project open in the current window is visible and can be activated with one click. + +**Architecture:** Keep project persistence and switching in the existing Zustand/file-system stores. Add a small pure model helper to normalize the active tab for deterministic tests, a focused `ProjectTabBar` presentation component for rendering and activation, and mount it from `MainLayout` directly below `TitleBarWithSettings`. The existing title-bar project dropdown remains unchanged as the project-management menu. + +**Tech Stack:** React 19, TypeScript, Zustand selectors, Base UI-compatible buttons, Tailwind semantic tokens, Bun tests, Vite Plus. + +--- + +### Task 1: Normalize Project Tab Data + +**Files:** +- Create: `windows/tauri/src/features/window/utils/project-tab-bar-model.ts` +- Create: `windows/tauri/src/features/window/utils/project-tab-bar-model.test.ts` + +- [ ] **Step 1: Write the failing test** + +Add a fixture with two project tabs and assert that `getProjectTabBarItems` preserves order, marks only the first active project, and repairs an invalid multiple-active input by keeping the first active tab. + +```ts +test("preserves project order and exposes one active tab", () => { + const result = getProjectTabBarItems([ + { id: "a", name: "Alpha", path: "D:/alpha", isActive: true, lastOpened: 1 }, + { id: "b", name: "Beta", path: "D:/beta", isActive: true, lastOpened: 2 }, + ]); + + expect(result.map((tab) => tab.name)).toEqual(["Alpha", "Beta"]); + expect(result.map((tab) => tab.isActive)).toEqual([true, false]); +}); +``` + +- [ ] **Step 2: Run the focused test and confirm the expected failure** + +Run from `windows/tauri`: + +```powershell +bun test src/features/window/utils/project-tab-bar-model.test.ts +``` + +Expected: the test fails because `project-tab-bar-model.ts` does not exist yet. + +- [ ] **Step 3: Implement the minimal model helper** + +Implement `getProjectTabBarItems(projectTabs)` by finding the first `isActive` tab and mapping the input in its original order, setting `isActive` only for that ID while preserving the other display fields. + +- [ ] **Step 4: Run the focused test and confirm it passes** + +Run the same Bun test. Expected: 1 test passes, 0 failures. + +### Task 2: Build The Project Tab Bar + +**Files:** +- Create: `windows/tauri/src/features/window/components/project-tab-bar.tsx` +- Create: `windows/tauri/src/features/window/components/project-tab-bar.test.ts` + +- [ ] **Step 1: Write the failing component contract test** + +Add a source-level contract test that requires the component to expose `role="tablist"`, `role="tab"`, `aria-selected`, `isSwitchingProject`, and `switchToProject`. This protects the accessibility and switching contract without introducing a new renderer test dependency. + +- [ ] **Step 2: Run the focused test and confirm the expected failure** + +Run: + +```powershell +bun test src/features/window/components/project-tab-bar.test.ts +``` + +Expected: the test fails because the component file does not exist yet. + +- [ ] **Step 3: Implement the component** + +Implement the component with these exact behaviors: + +```tsx +const projectTabs = useWorkspaceTabsStore.use.projectTabs(); +const switchToProject = useFileSystemStore((state) => state.switchToProject); +const isSwitchingProject = useFileSystemStore((state) => state.isSwitchingProject); +const projects = getProjectTabBarItems(projectTabs); + +if (projects.length === 0) return null; + +return ( +
+ {projects.map((project) => ( + + ))} +
+); +``` + +Use the existing `FolderOpenIcon`, semantic surface/border/selected tokens, fixed 30px tab height, horizontal overflow, visible focus styles, and truncated names. Do not add a native drag region or duplicate project-management actions. + +- [ ] **Step 4: Run the focused component contract test** + +Run the same Bun test. Expected: 1 test passes, 0 failures. + +### Task 3: Mount The Bar In The Workbench + +**Files:** +- Modify: `windows/tauri/src/features/layout/components/main-layout.tsx` + +- [ ] **Step 1: Add the import and mount point** + +Import `ProjectTabBar` from the window feature and render `` immediately after ``, before the root-folder conditional. The component itself returns `null` when no project is open, preserving the welcome screen layout. + +- [ ] **Step 2: Run focused tests and typecheck** + +Run: + +```powershell +bun test src/features/window/utils/project-tab-bar-model.test.ts src/features/window/components/project-tab-bar.test.ts +bun run typecheck +``` + +Expected: all focused tests pass and TypeScript exits with code 0. + +### Task 4: Verify The User Workflow + +**Files:** +- No additional source files. + +- [ ] **Step 1: Run lint and frontend build** + +```powershell +bunx vp lint src/features/layout/components/main-layout.tsx src/features/window/components/project-tab-bar.tsx src/features/window/components/project-tab-bar.test.ts src/features/window/utils/project-tab-bar-model.ts src/features/window/utils/project-tab-bar-model.test.ts +bun run build +git diff --check +``` + +Expected: all commands exit 0. Existing dependency warnings are acceptable if they do not introduce errors. + +- [ ] **Step 2: Rebuild and launch Windows Release** + +Stop the current preview process, run `.\scripts\build-windows.ps1 -Configuration Release` from the repository root, and launch `windows/tauri/src-tauri/target/x86_64-pc-windows-msvc/release/lithe-windows.exe`. + +- [ ] **Step 3: Verify the live tab interaction through CDP** + +Open two projects in the current window, assert a visible `[role="tablist"]` contains both project names, click the inactive `[role="tab"]`, and assert its `aria-selected` becomes `true` while the previous tab becomes `false`. Confirm the title-bar project label changes to the newly active project. + +- [ ] **Step 4: Run the final focused checks** + +```powershell +bun test src/features/window/utils/project-tab-bar-model.test.ts src/features/window/components/project-tab-bar.test.ts +bun run typecheck +git diff --check +``` + +Expected: all tests pass, typecheck passes, and no whitespace errors are reported. + +### Task 5: Review And Commit + +**Files:** +- Stage only the new project-tab-bar source/tests, `main-layout.tsx`, and the task plan/progress files. + +- [ ] **Step 1: Inspect the task diff and run an independent review** + +Check `git diff` and confirm unrelated pre-existing changes remain unstaged. Resolve any Critical or Important review findings before committing. + +- [ ] **Step 2: Create one focused implementation commit** + +```powershell +git add -- windows/tauri/src/features/layout/components/main-layout.tsx windows/tauri/src/features/window/components/project-tab-bar.tsx windows/tauri/src/features/window/components/project-tab-bar.test.ts windows/tauri/src/features/window/utils/project-tab-bar-model.ts windows/tauri/src/features/window/utils/project-tab-bar-model.test.ts .planning/2026-08-17-project-tab-bar/task_plan.md .planning/2026-08-17-project-tab-bar/findings.md .planning/2026-08-17-project-tab-bar/progress.md docs/superpowers/plans/2026-08-17-project-tab-bar.md +git commit -m "feat: add mac-style project tabs" +``` diff --git a/docs/superpowers/specs/2026-08-17-project-tab-bar-design.md b/docs/superpowers/specs/2026-08-17-project-tab-bar-design.md new file mode 100644 index 00000000..d32ef35d --- /dev/null +++ b/docs/superpowers/specs/2026-08-17-project-tab-bar-design.md @@ -0,0 +1,28 @@ +# Mac-Style Project Tab Bar + +## Goal + +Show every project opened in the current Windows workbench in a compact tab bar below the title bar, matching the macOS workbench pattern. Clicking a tab activates that project immediately while the existing title-bar project menu remains the entry point for creating, opening, cloning, and selecting recent projects. + +## Design + +- Add a `ProjectTabBar` presentation component between `TitleBarWithSettings` and the workbench content in `MainLayout`. +- Read project order and active state from `useWorkspaceTabsStore`; use `useFileSystemStore.switchToProject` for activation. +- Render one semantic button per open project with its project badge/icon, name, active styling, and a full-path tooltip/accessible label. +- Use a horizontal overflow container so the bar remains stable when many projects are open. Do not resize the title bar or use the tab bar as a native drag region. +- Disable project buttons while `isSwitchingProject` is true, preserving the existing stale-switch protection. +- Hide the bar when no project is open, so the welcome screen keeps its current vertical layout. +- Preserve the existing `TitleProjectMenu` dropdown and all of its actions. + +## Accessibility And Visual Behavior + +- Use `role="tablist"` on the strip and `role="tab"` plus `aria-selected` on each project button. +- Keep a visible focus ring and selected background/border using existing semantic design tokens. +- Use the existing project badge/icon rules and Lucide-style folder icon; no new color palette or decorative artwork. +- Keep the tab height and spacing fixed to prevent layout shift, and use text truncation with a tooltip for long names. + +## Testing + +- Add a pure model helper test that preserves project order and exposes exactly one active tab. +- Add component-level source/behavior coverage for the tab button activation callback where the existing frontend test setup permits it. +- Run focused Bun tests, typecheck, lint, frontend build, and a live Windows CDP check that opens two projects and activates the second tab. diff --git a/windows/tauri/src/features/layout/components/main-layout.tsx b/windows/tauri/src/features/layout/components/main-layout.tsx index 22b7125e..9643fc20 100644 --- a/windows/tauri/src/features/layout/components/main-layout.tsx +++ b/windows/tauri/src/features/layout/components/main-layout.tsx @@ -23,6 +23,7 @@ import { frontendTrace } from "@/utils/frontend-trace"; import { recordStartupMilestone } from "@/features/bootstrap/startup-performance"; import { getInternalTabDragData } from "@/features/tabs/utils/internal-tab-drag"; import TitleBarWithSettings from "../../window/components/title-bar/title-bar"; +import { ProjectTabBar } from "../../window/components/project-tab-bar"; import Footer from "./footer/footer"; import { ResizablePane } from "./resizable-pane"; import { @@ -259,6 +260,7 @@ export function MainLayout() { )} + {rootFolderPath ? ( <> diff --git a/windows/tauri/src/features/window/components/project-tab-bar.test.ts b/windows/tauri/src/features/window/components/project-tab-bar.test.ts new file mode 100644 index 00000000..2700997e --- /dev/null +++ b/windows/tauri/src/features/window/components/project-tab-bar.test.ts @@ -0,0 +1,11 @@ +import { expect, test } from "bun:test"; + +test("project tab bar exposes accessible switchable project tabs", async () => { + const source = await Bun.file(new URL("./project-tab-bar.tsx", import.meta.url)).text(); + + expect(source).toContain('role="tablist"'); + expect(source).toContain('role="tab"'); + expect(source).toContain("aria-selected"); + expect(source).toContain("isSwitchingProject"); + expect(source).toContain("switchToProject"); +}); diff --git a/windows/tauri/src/features/window/components/project-tab-bar.tsx b/windows/tauri/src/features/window/components/project-tab-bar.tsx new file mode 100644 index 00000000..08a7d8f7 --- /dev/null +++ b/windows/tauri/src/features/window/components/project-tab-bar.tsx @@ -0,0 +1,64 @@ +import { useMemo } from "react"; +import { useFileSystemStore } from "@/features/file-system/stores/file-system.store"; +import { useTranslation } from "@/i18n/locale-provider"; +import { useWorkspaceTabsStore } from "@/features/window/stores/workspace-tabs.store"; +import { FolderIcon } from "@/ui/icons"; +import { cn } from "@/utils/cn"; +import { getProjectTabBarItems } from "../utils/project-tab-bar-model"; + +export function ProjectTabBar() { + const { t } = useTranslation(); + const projectTabs = useWorkspaceTabsStore.use.projectTabs(); + const switchToProject = useFileSystemStore((state) => state.switchToProject); + const isSwitchingProject = useFileSystemStore((state) => state.isSwitchingProject); + const projects = useMemo(() => getProjectTabBarItems(projectTabs), [projectTabs]); + + if (projects.length === 0) return null; + + return ( +
+
+ {projects.map((project) => ( + + ))} +
+
+ ); +} diff --git a/windows/tauri/src/features/window/utils/project-tab-bar-model.test.ts b/windows/tauri/src/features/window/utils/project-tab-bar-model.test.ts new file mode 100644 index 00000000..7355d879 --- /dev/null +++ b/windows/tauri/src/features/window/utils/project-tab-bar-model.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from "bun:test"; +import { getProjectTabBarItems } from "./project-tab-bar-model"; + +test("preserves project order and exposes one active tab", () => { + const result = getProjectTabBarItems([ + { id: "a", name: "Alpha", path: "D:/alpha", isActive: true, lastOpened: 1 }, + { id: "b", name: "Beta", path: "D:/beta", isActive: true, lastOpened: 2 }, + ]); + + expect(result.map((tab) => tab.name)).toEqual(["Alpha", "Beta"]); + expect(result.map((tab) => tab.isActive)).toEqual([true, false]); +}); diff --git a/windows/tauri/src/features/window/utils/project-tab-bar-model.ts b/windows/tauri/src/features/window/utils/project-tab-bar-model.ts new file mode 100644 index 00000000..76f84ddd --- /dev/null +++ b/windows/tauri/src/features/window/utils/project-tab-bar-model.ts @@ -0,0 +1,10 @@ +import type { ProjectTab } from "../stores/workspace-tabs.store"; + +export function getProjectTabBarItems(projectTabs: ProjectTab[]): ProjectTab[] { + const activeProjectId = projectTabs.find((projectTab) => projectTab.isActive)?.id; + + return projectTabs.map((projectTab) => ({ + ...projectTab, + isActive: projectTab.id === activeProjectId, + })); +}