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 */}
+
);
}
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);
}