From 83fab5321ef2ceba908adc08dd76deebed06f13e Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Tue, 16 Dec 2025 22:36:22 -0500 Subject: [PATCH] feat: add file renaming functionality in ContextView - Implemented a rename dialog for files, allowing users to rename selected context files. - Added state management for the rename dialog and file name input. - Enhanced file handling to check for existing names and update file paths accordingly. - Updated UI to include a pencil icon for triggering the rename action on files. - Improved user experience by ensuring the renamed file is selected after the operation. --- .../board-view/components/kanban-card.tsx | 19 +-- .../board-view/hooks/use-board-actions.ts | 8 +- .../app/src/components/views/context-view.tsx | 142 ++++++++++++++++-- apps/app/tests/feature-lifecycle.spec.ts | 36 +++-- apps/app/tests/utils/features/kanban.ts | 36 ++++- 5 files changed, 197 insertions(+), 44 deletions(-) diff --git a/apps/app/src/components/views/board-view/components/kanban-card.tsx b/apps/app/src/components/views/board-view/components/kanban-card.tsx index 480f7ac8..3ba39a06 100644 --- a/apps/app/src/components/views/board-view/components/kanban-card.tsx +++ b/apps/app/src/components/views/board-view/components/kanban-card.tsx @@ -328,8 +328,8 @@ export const KanbanCard = memo(function KanbanCard({
- {Array.from({ length: 4 - feature.priority }).map((_, i) => ( - - ))} + P{feature.priority}
diff --git a/apps/app/src/components/views/board-view/hooks/use-board-actions.ts b/apps/app/src/components/views/board-view/hooks/use-board-actions.ts index b5b16b16..5a5c24a3 100644 --- a/apps/app/src/components/views/board-view/hooks/use-board-actions.ts +++ b/apps/app/src/components/views/board-view/hooks/use-board-actions.ts @@ -788,8 +788,14 @@ export function useBoardActions({ return; } + // Sort by priority (lower number = higher priority, priority 1 is highest) + // This matches the auto mode service behavior for consistency + const sortedBacklog = [...backlogFeatures].sort( + (a, b) => (a.priority || 999) - (b.priority || 999) + ); + // Start only one feature per keypress (user must press again for next) - const featuresToStart = backlogFeatures.slice(0, 1); + const featuresToStart = sortedBacklog.slice(0, 1); for (const feature of featuresToStart) { // Only create worktrees if the feature is enabled diff --git a/apps/app/src/components/views/context-view.tsx b/apps/app/src/components/views/context-view.tsx index 42457344..126c1afe 100644 --- a/apps/app/src/components/views/context-view.tsx +++ b/apps/app/src/components/views/context-view.tsx @@ -19,6 +19,7 @@ import { BookOpen, EditIcon, Eye, + Pencil, } from "lucide-react"; import { useKeyboardShortcuts, @@ -56,6 +57,8 @@ export function ContextView() { const [editedContent, setEditedContent] = useState(""); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false); + const [renameFileName, setRenameFileName] = useState(""); const [newFileName, setNewFileName] = useState(""); const [newFileType, setNewFileType] = useState<"text" | "image">("text"); const [uploadedImageData, setUploadedImageData] = useState( @@ -240,6 +243,60 @@ export function ContextView() { } }; + // Rename selected file + const handleRenameFile = async () => { + const contextPath = getContextPath(); + if (!selectedFile || !contextPath || !renameFileName.trim()) return; + + const newName = renameFileName.trim(); + if (newName === selectedFile.name) { + setIsRenameDialogOpen(false); + return; + } + + try { + const api = getElectronAPI(); + const newPath = `${contextPath}/${newName}`; + + // Check if file with new name already exists + const exists = await api.exists(newPath); + if (exists) { + console.error("A file with this name already exists"); + return; + } + + // Read current file content + const result = await api.readFile(selectedFile.path); + if (!result.success || result.content === undefined) { + console.error("Failed to read file for rename"); + return; + } + + // Write to new path + await api.writeFile(newPath, result.content); + + // Delete old file + await api.deleteFile(selectedFile.path); + + setIsRenameDialogOpen(false); + setRenameFileName(""); + + // Reload files and select the renamed file + await loadContextFiles(); + + // Update selected file with new name and path + const renamedFile: ContextFile = { + name: newName, + type: isImageFile(newName) ? "image" : "text", + path: newPath, + content: result.content, + }; + setSelectedFile(renamedFile); + } catch (error) { + console.error("Failed to rename file:", error); + } + }; + // Handle image upload const handleImageUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -418,24 +475,40 @@ export function ContextView() { ) : (
{contextFiles.map((file) => ( - + + +
))} )} @@ -730,6 +803,53 @@ export function ContextView() { + + {/* Rename Dialog */} + + + + Rename Context File + + Enter a new name for "{selectedFile?.name}". + + +
+
+ + setRenameFileName(e.target.value)} + placeholder="Enter new filename" + data-testid="rename-file-input" + onKeyDown={(e) => { + if (e.key === "Enter" && renameFileName.trim()) { + handleRenameFile(); + } + }} + /> +
+
+ + + + +
+
); } diff --git a/apps/app/tests/feature-lifecycle.spec.ts b/apps/app/tests/feature-lifecycle.spec.ts index 0e3ad3e4..e55e2957 100644 --- a/apps/app/tests/feature-lifecycle.spec.ts +++ b/apps/app/tests/feature-lifecycle.spec.ts @@ -139,8 +139,15 @@ test.describe("Feature Lifecycle Tests", () => { // Perform the drag and drop using dnd-kit compatible method await dragAndDropWithDndKit(page, dragHandle, inProgressColumn); - // Wait for the feature to move to in_progress - await page.waitForTimeout(500); + // First verify that the drag succeeded by checking for in_progress status + // This helps diagnose if the drag-drop is working or not + await expect(async () => { + const featureData = JSON.parse( + fs.readFileSync(path.join(featuresDir, featureId, "feature.json"), "utf-8") + ); + // Feature should be either in_progress (agent running) or waiting_approval (agent done) + expect(["in_progress", "waiting_approval"]).toContain(featureData.status); + }).toPass({ timeout: 15000 }); // The mock agent should complete quickly (about 1.3 seconds based on the sleep times) // Wait for the feature to move to waiting_approval (manual review) @@ -349,15 +356,16 @@ test.describe("Feature Lifecycle Tests", () => { await dragAndDropWithDndKit(page, dragHandle, inProgressColumn); - // Wait for the feature to be in in_progress - await page.waitForTimeout(500); - // Verify feature file still exists and is readable const featureFilePath = path.join(featuresDir, testFeatureId, "feature.json"); expect(fs.existsSync(featureFilePath)).toBe(true); - // Wait a bit for the agent to start - await page.waitForTimeout(1000); + // First verify that the drag succeeded by checking for in_progress status + await expect(async () => { + const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8")); + // Feature should be either in_progress (agent running) or waiting_approval (agent done) + expect(["in_progress", "waiting_approval"]).toContain(featureData.status); + }).toPass({ timeout: 15000 }); // ========================================================================== // Step 3: Wait for the mock agent to complete (it's fast in mock mode) @@ -421,8 +429,15 @@ test.describe("Feature Lifecycle Tests", () => { // Drag to in_progress to restart await dragAndDropWithDndKit(page, restartDragHandle, inProgressColumnRestart); - // Wait for the feature to be processed - await page.waitForTimeout(2000); + // Verify the feature file still exists + expect(fs.existsSync(featureFilePath)).toBe(true); + + // First verify that the restart drag succeeded by checking for in_progress status + await expect(async () => { + const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8")); + // Feature should be either in_progress (agent running) or waiting_approval (agent done) + expect(["in_progress", "waiting_approval"]).toContain(data.status); + }).toPass({ timeout: 15000 }); // Verify no "Feature not found" errors in console const featureNotFoundErrors = consoleErrors.filter( @@ -430,9 +445,6 @@ test.describe("Feature Lifecycle Tests", () => { ); expect(featureNotFoundErrors).toEqual([]); - // Verify the feature file still exists - expect(fs.existsSync(featureFilePath)).toBe(true); - // Wait for the mock agent to complete and move to waiting_approval await expect(async () => { const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8")); diff --git a/apps/app/tests/utils/features/kanban.ts b/apps/app/tests/utils/features/kanban.ts index 6eb1bb77..4b6d2a8a 100644 --- a/apps/app/tests/utils/features/kanban.ts +++ b/apps/app/tests/utils/features/kanban.ts @@ -3,12 +3,22 @@ import { Page, Locator } from "@playwright/test"; /** * Perform a drag and drop operation that works with @dnd-kit * This uses explicit mouse movements with pointer events + * + * NOTE: dnd-kit requires careful timing for drag activation. In CI environments, + * we need longer delays and more movement steps for reliable detection. */ export async function dragAndDropWithDndKit( page: Page, sourceLocator: Locator, targetLocator: Locator ): Promise { + // Ensure elements are visible and stable before getting bounding boxes + await sourceLocator.waitFor({ state: "visible", timeout: 5000 }); + await targetLocator.waitFor({ state: "visible", timeout: 5000 }); + + // Small delay to ensure layout is stable + await page.waitForTimeout(100); + const sourceBox = await sourceLocator.boundingBox(); const targetBox = await targetLocator.boundingBox(); @@ -24,11 +34,29 @@ export async function dragAndDropWithDndKit( const endX = targetBox.x + targetBox.width / 2; const endY = targetBox.y + targetBox.height / 2; - // Perform the drag and drop with pointer events + // Move to source element first await page.mouse.move(startX, startY); + await page.waitForTimeout(50); + + // Press and hold - dnd-kit needs time to activate the drag sensor await page.mouse.down(); - await page.waitForTimeout(150); // Give dnd-kit time to recognize the drag - await page.mouse.move(endX, endY, { steps: 15 }); - await page.waitForTimeout(100); // Allow time for drop detection + await page.waitForTimeout(300); // Longer delay for CI - dnd-kit activation threshold + + // Move slightly first to trigger drag detection (dnd-kit has a distance threshold) + const smallMoveX = startX + 10; + const smallMoveY = startY + 10; + await page.mouse.move(smallMoveX, smallMoveY, { steps: 3 }); + await page.waitForTimeout(100); + + // Now move to target with slower, more deliberate movement + await page.mouse.move(endX, endY, { steps: 25 }); + + // Pause over target for drop detection + await page.waitForTimeout(200); + + // Release await page.mouse.up(); + + // Allow time for the drop handler to process + await page.waitForTimeout(100); }