Implement project picker keyboard shortcut and enhance feature management

- Added a new keyboard shortcut 'P' to open the project picker dropdown.
- Implemented functionality to select projects using number keys, allowing users to quickly switch between projects.
- Updated the feature list to include a new feature for project selection via keyboard shortcuts.
- Removed obsolete coding_prompt.md and added initializer_prompt.md for better session management.
- Introduced context management for features, enabling reading, writing, and deleting context files.
- Updated package dependencies to include @radix-ui/react-checkbox for enhanced UI components.

This commit enhances user experience by streamlining project selection and improves the overall feature management process.

🤖 Generated with Claude Code
This commit is contained in:
Cody Seibert
2025-12-09 12:20:07 -05:00
parent 95355f53f4
commit 9bae205312
39 changed files with 1551 additions and 4168 deletions

View File

@@ -1,196 +0,0 @@
import { test, expect } from "@playwright/test";
import { setupMockProject, clickElement } from "./utils";
// Helper function to navigate to context view and wait for either loading or main view
async function navigateToContextAndOpenDialog(page: any) {
// Click on context nav
const contextNav = page.locator('[data-testid="nav-context"]');
await contextNav.waitFor({ state: "visible", timeout: 10000 });
await contextNav.click();
// Wait for either the context view or the loading view
// The loading view might stay visible if the electron API is mocked
await page.waitForSelector(
'[data-testid="context-view"], [data-testid="context-view-loading"], [data-testid="context-view-no-project"]',
{ timeout: 10000 }
);
// If we have the main context view, click the add button
const contextView = page.locator('[data-testid="context-view"]');
const isContextViewVisible = await contextView.isVisible().catch(() => false);
if (isContextViewVisible) {
// Click add context file button
const addFileBtn = page.locator('[data-testid="add-context-file"]');
await addFileBtn.click();
} else {
// If context view isn't visible, we might be in loading state
// For testing purposes, simulate opening the dialog via keyboard or other means
// Skip this test scenario
test.skip();
return;
}
// Wait for dialog to appear
const dialog = page.locator('[data-testid="add-context-dialog"]');
await dialog.waitFor({ state: "visible", timeout: 5000 });
}
test.describe("Add Context File Dialog", () => {
test.beforeEach(async ({ page }) => {
await setupMockProject(page);
await page.goto("/");
await page.waitForLoadState("networkidle");
});
test("should show file name input and content textarea in add context dialog", async ({
page,
}) => {
await navigateToContextAndOpenDialog(page);
// Verify file name input is visible
const fileNameInput = page.locator('[data-testid="new-file-name"]');
await expect(fileNameInput).toBeVisible();
// Verify content textarea is visible when text type is selected (default)
const contentTextarea = page.locator('[data-testid="new-file-content"]');
await expect(contentTextarea).toBeVisible();
// Verify placeholder text
await expect(contentTextarea).toHaveAttribute(
"placeholder",
"Enter context content here or drag & drop a .txt or .md file..."
);
});
test("should allow typing content in the textarea", async ({ page }) => {
await navigateToContextAndOpenDialog(page);
const contentTextarea = page.locator('[data-testid="new-file-content"]');
const testContent =
"# Test Context\n\nThis is test content for the context file.";
await contentTextarea.fill(testContent);
await expect(contentTextarea).toHaveValue(testContent);
});
test("should show textarea only for text file type", async ({ page }) => {
await navigateToContextAndOpenDialog(page);
// Verify textarea is visible when text type is selected (default)
const contentTextarea = page.locator('[data-testid="new-file-content"]');
await expect(contentTextarea).toBeVisible();
// Switch to image type
await clickElement(page, "add-image-type");
// Verify textarea is no longer visible
await expect(contentTextarea).not.toBeVisible();
// Verify image upload input is attached instead
const imageUploadInput = page.locator('[data-testid="image-upload-input"]');
await expect(imageUploadInput).toBeAttached();
// Switch back to text type
await clickElement(page, "add-text-type");
// Verify textarea is visible again
const contentTextareaAgain = page.locator('[data-testid="new-file-content"]');
await expect(contentTextareaAgain).toBeVisible();
});
test("should display drag and drop helper text", async ({ page }) => {
await navigateToContextAndOpenDialog(page);
// Check for helper text about drag and drop
const helperText = page.locator(
"text=Drag & drop .txt or .md files to import their content"
);
await expect(helperText).toBeVisible();
});
test("should populate content from dropped .txt file", async ({ page }) => {
await navigateToContextAndOpenDialog(page);
const contentTextarea = page.locator('[data-testid="new-file-content"]');
const testContent = "This is content from a text file.";
// Create a data transfer with a .txt file
const dataTransfer = await page.evaluateHandle((content) => {
const dt = new DataTransfer();
const file = new File([content], "test-file.txt", { type: "text/plain" });
dt.items.add(file);
return dt;
}, testContent);
// Dispatch drag events to simulate file drop
await contentTextarea.dispatchEvent("dragover", { dataTransfer });
await contentTextarea.dispatchEvent("drop", { dataTransfer });
// Wait for the content to be populated
await expect(contentTextarea).toHaveValue(testContent, { timeout: 5000 });
// Verify filename was auto-filled
const fileNameInput = page.locator('[data-testid="new-file-name"]');
await expect(fileNameInput).toHaveValue("test-file.txt");
});
test("should populate content from dropped .md file", async ({ page }) => {
await navigateToContextAndOpenDialog(page);
const contentTextarea = page.locator('[data-testid="new-file-content"]');
const testContent = "# Markdown File\n\nThis is markdown content.";
// Create a data transfer with a .md file
const dataTransfer = await page.evaluateHandle((content) => {
const dt = new DataTransfer();
const file = new File([content], "readme.md", { type: "text/markdown" });
dt.items.add(file);
return dt;
}, testContent);
// Dispatch drag events to simulate file drop
await contentTextarea.dispatchEvent("dragover", { dataTransfer });
await contentTextarea.dispatchEvent("drop", { dataTransfer });
// Wait for the content to be populated
await expect(contentTextarea).toHaveValue(testContent, { timeout: 5000 });
// Verify filename was auto-filled
const fileNameInput = page.locator('[data-testid="new-file-name"]');
await expect(fileNameInput).toHaveValue("readme.md");
});
test("should not auto-fill filename if already provided", async ({
page,
}) => {
await navigateToContextAndOpenDialog(page);
// Fill in the filename first
const fileNameInput = page.locator('[data-testid="new-file-name"]');
await fileNameInput.fill("my-custom-name.md");
const contentTextarea = page.locator('[data-testid="new-file-content"]');
const testContent = "Content from dropped file";
// Create a data transfer with a .txt file
const dataTransfer = await page.evaluateHandle((content) => {
const dt = new DataTransfer();
const file = new File([content], "dropped-file.txt", {
type: "text/plain",
});
dt.items.add(file);
return dt;
}, testContent);
// Dispatch drag events to simulate file drop
await contentTextarea.dispatchEvent("dragover", { dataTransfer });
await contentTextarea.dispatchEvent("drop", { dataTransfer });
// Wait for the content to be populated
await expect(contentTextarea).toHaveValue(testContent, { timeout: 5000 });
// Verify filename was NOT overwritten
await expect(fileNameInput).toHaveValue("my-custom-name.md");
});
});

View File

@@ -0,0 +1,237 @@
import { test, expect } from "@playwright/test";
import {
setupMockMultipleProjects,
waitForElement,
isProjectPickerDropdownOpen,
waitForProjectPickerDropdown,
waitForProjectPickerDropdownHidden,
pressShortcut,
pressNumberKey,
isProjectHotkeyVisible,
getProjectPickerShortcut,
} from "./utils";
test.describe("Project Picker Keyboard Shortcuts", () => {
test("pressing P key opens the project picker dropdown", async ({ page }) => {
// Setup with multiple projects
await setupMockMultipleProjects(page, 3);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar to be visible
await waitForElement(page, "sidebar");
// Dropdown should initially be closed
expect(await isProjectPickerDropdownOpen(page)).toBe(false);
// Press P to open project picker
await pressShortcut(page, "p");
// Dropdown should now be open
await waitForProjectPickerDropdown(page);
expect(await isProjectPickerDropdownOpen(page)).toBe(true);
});
test("project options show hotkey indicators (1-5)", async ({ page }) => {
// Setup with 5 projects
await setupMockMultipleProjects(page, 5);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Open project picker
await pressShortcut(page, "p");
await waitForProjectPickerDropdown(page);
// Check that all 5 hotkey indicators are visible
for (let i = 1; i <= 5; i++) {
expect(await isProjectHotkeyVisible(page, i)).toBe(true);
const hotkey = page.locator(`[data-testid="project-hotkey-${i}"]`);
expect(await hotkey.textContent()).toBe(i.toString());
}
});
test("pressing number key selects the corresponding project", async ({
page,
}) => {
// Setup with 3 projects
await setupMockMultipleProjects(page, 3);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Check initial project (should be Test Project 1)
const projectSelector = page.locator('[data-testid="project-selector"]');
await expect(projectSelector).toContainText("Test Project 1");
// Open project picker
await pressShortcut(page, "p");
await waitForProjectPickerDropdown(page);
// Press 2 to select the second project
await pressNumberKey(page, 2);
// Dropdown should close
await waitForProjectPickerDropdownHidden(page);
// Project should now be Test Project 2
await expect(projectSelector).toContainText("Test Project 2");
});
test("pressing number key for non-existent project does nothing", async ({
page,
}) => {
// Setup with 2 projects
await setupMockMultipleProjects(page, 2);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Check initial project
const projectSelector = page.locator('[data-testid="project-selector"]');
await expect(projectSelector).toContainText("Test Project 1");
// Open project picker
await pressShortcut(page, "p");
await waitForProjectPickerDropdown(page);
// Press 5 (there's no 5th project)
await pressNumberKey(page, 5);
// Dropdown should remain open
expect(await isProjectPickerDropdownOpen(page)).toBe(true);
// Project should still be Test Project 1
await expect(projectSelector).toContainText("Test Project 1");
});
test("pressing Escape closes the project picker dropdown", async ({
page,
}) => {
// Setup with multiple projects
await setupMockMultipleProjects(page, 3);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Open project picker
await pressShortcut(page, "p");
await waitForProjectPickerDropdown(page);
// Press Escape
await page.keyboard.press("Escape");
// Dropdown should close
await waitForProjectPickerDropdownHidden(page);
expect(await isProjectPickerDropdownOpen(page)).toBe(false);
});
test("project selector button shows P shortcut indicator", async ({
page,
}) => {
// Setup with multiple projects
await setupMockMultipleProjects(page, 3);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar and project selector
await waitForElement(page, "sidebar");
await waitForElement(page, "project-selector");
// Check that P shortcut indicator is visible
const shortcutIndicator = await getProjectPickerShortcut(page);
await expect(shortcutIndicator).toBeVisible();
await expect(shortcutIndicator).toHaveText("P");
});
test("only first 5 projects are shown with hotkeys", async ({ page }) => {
// Setup with 7 projects
await setupMockMultipleProjects(page, 7);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Open project picker
await pressShortcut(page, "p");
await waitForProjectPickerDropdown(page);
// Only 5 hotkey indicators should be visible (1-5)
for (let i = 1; i <= 5; i++) {
expect(await isProjectHotkeyVisible(page, i)).toBe(true);
}
// 6th and 7th should not exist
const hotkey6 = page.locator('[data-testid="project-hotkey-6"]');
const hotkey7 = page.locator('[data-testid="project-hotkey-7"]');
await expect(hotkey6).not.toBeVisible();
await expect(hotkey7).not.toBeVisible();
});
test("clicking a project option also works", async ({ page }) => {
// Setup with 3 projects
await setupMockMultipleProjects(page, 3);
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Open project picker by clicking
await page.locator('[data-testid="project-selector"]').click();
await waitForProjectPickerDropdown(page);
// Click on second project option
await page.locator('[data-testid="project-option-test-project-2"]').click();
// Dropdown should close
await waitForProjectPickerDropdownHidden(page);
// Project should now be Test Project 2
const projectSelector = page.locator('[data-testid="project-selector"]');
await expect(projectSelector).toContainText("Test Project 2");
});
test("P shortcut does not work when no projects exist", async ({ page }) => {
// Setup with empty projects
await page.addInitScript(() => {
const mockState = {
state: {
projects: [],
currentProject: null,
currentView: "welcome",
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
});
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for sidebar
await waitForElement(page, "sidebar");
// Press P - should not open any dropdown since there are no projects
await pressShortcut(page, "p");
await page.waitForTimeout(300);
// Dropdown should not be visible
expect(await isProjectPickerDropdownOpen(page)).toBe(false);
});
});

View File

@@ -1701,3 +1701,90 @@ export async function getOutputModalDescription(page: Page): Promise<string | nu
const modalDescription = page.locator('[data-testid="agent-output-modal"] [data-slot="dialog-description"]');
return await modalDescription.textContent().catch(() => null);
}
/**
* Check if the project picker dropdown is open
*/
export async function isProjectPickerDropdownOpen(page: Page): Promise<boolean> {
const dropdown = page.locator('[data-testid="project-picker-dropdown"]');
return await dropdown.isVisible().catch(() => false);
}
/**
* Wait for the project picker dropdown to be visible
*/
export async function waitForProjectPickerDropdown(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "project-picker-dropdown", options);
}
/**
* Wait for the project picker dropdown to be hidden
*/
export async function waitForProjectPickerDropdownHidden(
page: Page,
options?: { timeout?: number }
): Promise<void> {
await waitForElementHidden(page, "project-picker-dropdown", options);
}
/**
* Get a project hotkey indicator element by number (1-5)
*/
export async function getProjectHotkey(page: Page, num: number): Promise<Locator> {
return page.locator(`[data-testid="project-hotkey-${num}"]`);
}
/**
* Check if a project hotkey indicator is visible
*/
export async function isProjectHotkeyVisible(page: Page, num: number): Promise<boolean> {
const hotkey = page.locator(`[data-testid="project-hotkey-${num}"]`);
return await hotkey.isVisible().catch(() => false);
}
/**
* Get the project picker shortcut indicator (P key)
*/
export async function getProjectPickerShortcut(page: Page): Promise<Locator> {
return page.locator('[data-testid="project-picker-shortcut"]');
}
/**
* Set up a mock state with multiple projects
*/
export async function setupMockMultipleProjects(
page: Page,
projectCount: number = 3
): Promise<void> {
await page.addInitScript((count: number) => {
const mockProjects = [];
for (let i = 0; i < count; i++) {
mockProjects.push({
id: `test-project-${i + 1}`,
name: `Test Project ${i + 1}`,
path: `/mock/test-project-${i + 1}`,
lastOpened: new Date(Date.now() - i * 86400000).toISOString(),
});
}
const mockState = {
state: {
projects: mockProjects,
currentProject: mockProjects[0],
currentView: "board",
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
}, projectCount);
}