Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .planning/2026-08-17-project-tab-bar/findings.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 15 additions & 0 deletions .planning/2026-08-17-project-tab-bar/progress.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 26 additions & 0 deletions .planning/2026-08-17-project-tab-bar/task_plan.md
Original file line number Diff line number Diff line change
@@ -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. |
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ final class LinuxDoAnonymousWebSession: ObservableObject {
releaseTask = nil
}

func releaseAfterInactivity() {
@discardableResult
func releaseAfterInactivity() -> Task<Void, Never> {
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 }
Expand All @@ -40,6 +41,8 @@ final class LinuxDoAnonymousWebSession: ObservableObject {
webView = nil
releaseTask = nil
}
releaseTask = task
return task
}

deinit {
Expand Down
15 changes: 6 additions & 9 deletions Tests/LitheTests/LinuxDoAnonymousWebSessionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 3 additions & 1 deletion Tests/LitheTests/LitheCoreLogicTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
177 changes: 177 additions & 0 deletions docs/superpowers/plans/2026-08-17-project-tab-bar.md
Original file line number Diff line number Diff line change
@@ -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 (
<div role="tablist" aria-label={t("titleProject.openProjects")}>
{projects.map((project) => (
<button
key={project.id}
type="button"
role="tab"
aria-selected={project.isActive}
disabled={isSwitchingProject || project.isActive}
title={project.path}
onClick={() => void switchToProject(project.id)}
>
{project.name}
</button>
))}
</div>
);
```

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 `<ProjectTabBar />` immediately after `<TitleBarWithSettings />`, 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"
```
28 changes: 28 additions & 0 deletions docs/superpowers/specs/2026-08-17-project-tab-bar-design.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -259,6 +260,7 @@ export function MainLayout() {
)}

<TitleBarWithSettings />
<ProjectTabBar />

{rootFolderPath ? (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
Loading
Loading