Skip to content

Commit d1d0970

Browse files
authored
Merge pull request #121 from Mucheen/fix/windows-new-window-blank
feat(windows): add mac-style project tabs
2 parents 8ac7855 + d9eacd9 commit d1d0970

13 files changed

Lines changed: 365 additions & 12 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Findings
2+
3+
- 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.
4+
- Windows already persists the same concept in `useWorkspaceTabsStore.projectTabs` and exposes `useFileSystemStore.switchToProject` plus `isSwitchingProject`.
5+
- Windows currently renders `TitleProjectMenu` inside the title bar; its dropdown already lists open projects and should remain for project-management actions.
6+
- `MainLayout` places `TitleBarWithSettings` immediately before the workbench and is the correct ownership boundary for a full-width tab strip.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Progress
2+
3+
## 2026-08-17
4+
5+
- Recorded the pre-existing dirty worktree and branch before editing.
6+
- Confirmed the macOS `WorkbenchView.projectTabBar` as the visual and interaction reference.
7+
- Confirmed Windows has project tab persistence and a switch action but no top-level project tab presentation.
8+
- User approved direct implementation using the macOS pattern.
9+
- Added and ran the red model test, then implemented `getProjectTabBarItems` and confirmed the green result.
10+
- Added and ran the red tab-bar contract test, then implemented the accessible tab bar component and confirmed the green result.
11+
- Mounted `ProjectTabBar` below `TitleBarWithSettings` in `MainLayout`.
12+
- Built the Windows Release application successfully with `scripts/build-windows.ps1 -Configuration Release`.
13+
- 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.
14+
- Re-ran focused Bun tests (`2 pass`), related regression tests (`9 pass`), TypeScript typecheck, targeted lint, and `git diff --check` successfully.
15+
- Completed the five-axis implementation review with no Critical or Important findings; unrelated pre-existing changes remain unstaged.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Project Tab Bar
2+
3+
## Goal
4+
5+
Add a Mac-style project tab bar below the Windows title bar so projects open in the current window are visible and directly switchable.
6+
7+
## Phases
8+
9+
- [complete] Explore existing macOS and Windows project-session patterns.
10+
- [complete] Add failing model and interaction tests.
11+
- [complete] Implement the project tab bar and connect project switching.
12+
- [complete] Run frontend verification and live Windows checks.
13+
- [complete] Review and create one focused implementation commit.
14+
15+
## Scope
16+
17+
- Worktree: `D:\code\Lithe-IDEA-preview-0.3.0`
18+
- Branch: `fix/windows-new-window-blank`
19+
- Preserve all existing dirty files and stage only this task's files.
20+
- Keep the existing title-bar project dropdown and project persistence behavior.
21+
22+
## Errors Encountered
23+
24+
| Error | Attempt | Resolution |
25+
| --- | --- | --- |
26+
| 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. |

Sources/Lithe/Platform/MacOS/Community/LinuxDoAnonymousWebView.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ final class LinuxDoAnonymousWebSession: ObservableObject {
2828
releaseTask = nil
2929
}
3030

31-
func releaseAfterInactivity() {
31+
@discardableResult
32+
func releaseAfterInactivity() -> Task<Void, Never> {
3233
releaseTask?.cancel()
33-
releaseTask = Task { @MainActor [weak self] in
34+
let task = Task { @MainActor [weak self] in
3435
guard let self else { return }
3536
try? await Task.sleep(nanoseconds: idleLifetimeNanoseconds)
3637
guard !Task.isCancelled else { return }
@@ -40,6 +41,8 @@ final class LinuxDoAnonymousWebSession: ObservableObject {
4041
webView = nil
4142
releaseTask = nil
4243
}
44+
releaseTask = task
45+
return task
4346
}
4447

4548
deinit {

Tests/LitheTests/LinuxDoAnonymousWebSessionTests.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,25 @@ import Testing
55
@MainActor
66
struct LinuxDoAnonymousWebSessionTests {
77
@Test
8-
func shortPanelAbsenceKeepsTheCurrentWebView() async throws {
8+
func shortPanelAbsenceKeepsTheCurrentWebView() async {
99
let session = LinuxDoAnonymousWebSession(idleLifetimeNanoseconds: 50_000_000)
1010
let webView = WKWebView()
1111
session.webView = webView
1212

13-
session.releaseAfterInactivity()
13+
let releaseTask = session.releaseAfterInactivity()
1414
session.resume()
15-
try await Task.sleep(nanoseconds: 80_000_000)
15+
await releaseTask.value
1616

1717
#expect(session.webView === webView)
1818
}
1919

2020
@Test
21-
func inactiveSessionReleasesItsWebView() async throws {
21+
func inactiveSessionReleasesItsWebView() async {
2222
let session = LinuxDoAnonymousWebSession(idleLifetimeNanoseconds: 20_000_000)
2323
session.webView = WKWebView()
2424

25-
session.releaseAfterInactivity()
26-
let deadline = ContinuousClock.now + .seconds(1)
27-
while session.webView != nil, ContinuousClock.now < deadline {
28-
try await Task.sleep(for: .milliseconds(10))
29-
}
25+
let releaseTask = session.releaseAfterInactivity()
26+
await releaseTask.value
3027

3128
#expect(session.webView == nil)
3229
}

Tests/LitheTests/LitheCoreLogicTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3231,7 +3231,9 @@ struct EditorDocumentTests {
32313231
source?.emit(DirectoryChangeBatch(gitStateMayHaveChanged: true))
32323232
source?.emit(DirectoryChangeBatch(gitStateMayHaveChanged: true))
32333233
source?.emit(DirectoryChangeBatch(gitStateMayHaveChanged: true))
3234-
let refreshed = await waitForWorkspaceObservation { refreshCount == 2 }
3234+
let refreshed = await waitForWorkspaceObservation(timeout: .seconds(15)) {
3235+
refreshCount == 2
3236+
}
32353237

32363238
#expect(refreshed)
32373239
#expect(refreshCount == 2)
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Mac-Style Project Tab Bar Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task with verification checkpoints.
4+
5+
**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.
6+
7+
**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.
8+
9+
**Tech Stack:** React 19, TypeScript, Zustand selectors, Base UI-compatible buttons, Tailwind semantic tokens, Bun tests, Vite Plus.
10+
11+
---
12+
13+
### Task 1: Normalize Project Tab Data
14+
15+
**Files:**
16+
- Create: `windows/tauri/src/features/window/utils/project-tab-bar-model.ts`
17+
- Create: `windows/tauri/src/features/window/utils/project-tab-bar-model.test.ts`
18+
19+
- [ ] **Step 1: Write the failing test**
20+
21+
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.
22+
23+
```ts
24+
test("preserves project order and exposes one active tab", () => {
25+
const result = getProjectTabBarItems([
26+
{ id: "a", name: "Alpha", path: "D:/alpha", isActive: true, lastOpened: 1 },
27+
{ id: "b", name: "Beta", path: "D:/beta", isActive: true, lastOpened: 2 },
28+
]);
29+
30+
expect(result.map((tab) => tab.name)).toEqual(["Alpha", "Beta"]);
31+
expect(result.map((tab) => tab.isActive)).toEqual([true, false]);
32+
});
33+
```
34+
35+
- [ ] **Step 2: Run the focused test and confirm the expected failure**
36+
37+
Run from `windows/tauri`:
38+
39+
```powershell
40+
bun test src/features/window/utils/project-tab-bar-model.test.ts
41+
```
42+
43+
Expected: the test fails because `project-tab-bar-model.ts` does not exist yet.
44+
45+
- [ ] **Step 3: Implement the minimal model helper**
46+
47+
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.
48+
49+
- [ ] **Step 4: Run the focused test and confirm it passes**
50+
51+
Run the same Bun test. Expected: 1 test passes, 0 failures.
52+
53+
### Task 2: Build The Project Tab Bar
54+
55+
**Files:**
56+
- Create: `windows/tauri/src/features/window/components/project-tab-bar.tsx`
57+
- Create: `windows/tauri/src/features/window/components/project-tab-bar.test.ts`
58+
59+
- [ ] **Step 1: Write the failing component contract test**
60+
61+
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.
62+
63+
- [ ] **Step 2: Run the focused test and confirm the expected failure**
64+
65+
Run:
66+
67+
```powershell
68+
bun test src/features/window/components/project-tab-bar.test.ts
69+
```
70+
71+
Expected: the test fails because the component file does not exist yet.
72+
73+
- [ ] **Step 3: Implement the component**
74+
75+
Implement the component with these exact behaviors:
76+
77+
```tsx
78+
const projectTabs = useWorkspaceTabsStore.use.projectTabs();
79+
const switchToProject = useFileSystemStore((state) => state.switchToProject);
80+
const isSwitchingProject = useFileSystemStore((state) => state.isSwitchingProject);
81+
const projects = getProjectTabBarItems(projectTabs);
82+
83+
if (projects.length === 0) return null;
84+
85+
return (
86+
<div role="tablist" aria-label={t("titleProject.openProjects")}>
87+
{projects.map((project) => (
88+
<button
89+
key={project.id}
90+
type="button"
91+
role="tab"
92+
aria-selected={project.isActive}
93+
disabled={isSwitchingProject || project.isActive}
94+
title={project.path}
95+
onClick={() => void switchToProject(project.id)}
96+
>
97+
{project.name}
98+
</button>
99+
))}
100+
</div>
101+
);
102+
```
103+
104+
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.
105+
106+
- [ ] **Step 4: Run the focused component contract test**
107+
108+
Run the same Bun test. Expected: 1 test passes, 0 failures.
109+
110+
### Task 3: Mount The Bar In The Workbench
111+
112+
**Files:**
113+
- Modify: `windows/tauri/src/features/layout/components/main-layout.tsx`
114+
115+
- [ ] **Step 1: Add the import and mount point**
116+
117+
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.
118+
119+
- [ ] **Step 2: Run focused tests and typecheck**
120+
121+
Run:
122+
123+
```powershell
124+
bun test src/features/window/utils/project-tab-bar-model.test.ts src/features/window/components/project-tab-bar.test.ts
125+
bun run typecheck
126+
```
127+
128+
Expected: all focused tests pass and TypeScript exits with code 0.
129+
130+
### Task 4: Verify The User Workflow
131+
132+
**Files:**
133+
- No additional source files.
134+
135+
- [ ] **Step 1: Run lint and frontend build**
136+
137+
```powershell
138+
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
139+
bun run build
140+
git diff --check
141+
```
142+
143+
Expected: all commands exit 0. Existing dependency warnings are acceptable if they do not introduce errors.
144+
145+
- [ ] **Step 2: Rebuild and launch Windows Release**
146+
147+
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`.
148+
149+
- [ ] **Step 3: Verify the live tab interaction through CDP**
150+
151+
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.
152+
153+
- [ ] **Step 4: Run the final focused checks**
154+
155+
```powershell
156+
bun test src/features/window/utils/project-tab-bar-model.test.ts src/features/window/components/project-tab-bar.test.ts
157+
bun run typecheck
158+
git diff --check
159+
```
160+
161+
Expected: all tests pass, typecheck passes, and no whitespace errors are reported.
162+
163+
### Task 5: Review And Commit
164+
165+
**Files:**
166+
- Stage only the new project-tab-bar source/tests, `main-layout.tsx`, and the task plan/progress files.
167+
168+
- [ ] **Step 1: Inspect the task diff and run an independent review**
169+
170+
Check `git diff` and confirm unrelated pre-existing changes remain unstaged. Resolve any Critical or Important review findings before committing.
171+
172+
- [ ] **Step 2: Create one focused implementation commit**
173+
174+
```powershell
175+
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
176+
git commit -m "feat: add mac-style project tabs"
177+
```
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Mac-Style Project Tab Bar
2+
3+
## Goal
4+
5+
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.
6+
7+
## Design
8+
9+
- Add a `ProjectTabBar` presentation component between `TitleBarWithSettings` and the workbench content in `MainLayout`.
10+
- Read project order and active state from `useWorkspaceTabsStore`; use `useFileSystemStore.switchToProject` for activation.
11+
- Render one semantic button per open project with its project badge/icon, name, active styling, and a full-path tooltip/accessible label.
12+
- 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.
13+
- Disable project buttons while `isSwitchingProject` is true, preserving the existing stale-switch protection.
14+
- Hide the bar when no project is open, so the welcome screen keeps its current vertical layout.
15+
- Preserve the existing `TitleProjectMenu` dropdown and all of its actions.
16+
17+
## Accessibility And Visual Behavior
18+
19+
- Use `role="tablist"` on the strip and `role="tab"` plus `aria-selected` on each project button.
20+
- Keep a visible focus ring and selected background/border using existing semantic design tokens.
21+
- Use the existing project badge/icon rules and Lucide-style folder icon; no new color palette or decorative artwork.
22+
- Keep the tab height and spacing fixed to prevent layout shift, and use text truncation with a tooltip for long names.
23+
24+
## Testing
25+
26+
- Add a pure model helper test that preserves project order and exposes exactly one active tab.
27+
- Add component-level source/behavior coverage for the tab button activation callback where the existing frontend test setup permits it.
28+
- Run focused Bun tests, typecheck, lint, frontend build, and a live Windows CDP check that opens two projects and activates the second tab.

windows/tauri/src/features/layout/components/main-layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { frontendTrace } from "@/utils/frontend-trace";
2323
import { recordStartupMilestone } from "@/features/bootstrap/startup-performance";
2424
import { getInternalTabDragData } from "@/features/tabs/utils/internal-tab-drag";
2525
import TitleBarWithSettings from "../../window/components/title-bar/title-bar";
26+
import { ProjectTabBar } from "../../window/components/project-tab-bar";
2627
import Footer from "./footer/footer";
2728
import { ResizablePane } from "./resizable-pane";
2829
import {
@@ -259,6 +260,7 @@ export function MainLayout() {
259260
)}
260261

261262
<TitleBarWithSettings />
263+
<ProjectTabBar />
262264

263265
{rootFolderPath ? (
264266
<>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { expect, test } from "bun:test";
2+
3+
test("project tab bar exposes accessible switchable project tabs", async () => {
4+
const source = await Bun.file(new URL("./project-tab-bar.tsx", import.meta.url)).text();
5+
6+
expect(source).toContain('role="tablist"');
7+
expect(source).toContain('role="tab"');
8+
expect(source).toContain("aria-selected");
9+
expect(source).toContain("isSwitchingProject");
10+
expect(source).toContain("switchToProject");
11+
});

0 commit comments

Comments
 (0)