Skip to content
Open
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
41 changes: 40 additions & 1 deletion frontend/src/components/ProjectSessionList.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useState, useEffect, useMemo, useCallback, useRef, useId } from 'react';
import { ChevronDown, ChevronRight, Plus, FolderPlus, GitBranch, MoreHorizontal, Home, Archive, ArchiveRestore, Trash2, GitPullRequest, Pin, Monitor, MessageSquare } from 'lucide-react';
import { ChevronDown, ChevronRight, Plus, FolderPlus, GitBranch, MoreHorizontal, Home, Archive, ArchiveRestore, Trash2, GitPullRequest, Pin, Monitor, MessageSquare, Settings } from 'lucide-react';
import { SessionDetailTooltip } from './SessionDetailTooltip';
import { useSessionStore } from '../stores/sessionStore';
import { useNavigationStore } from '../stores/navigationStore';
import { SETTINGS_PREFERENCE_KEYS, normalizeSidebarPaneRowLayout, type SidebarPaneRowLayout } from '../types/settings';
import { CreateSessionDialog } from './CreateSessionDialog';
import { AddProjectDialog } from './AddProjectDialog';
import ProjectSettings from './ProjectSettings';
import { Dropdown } from './ui/Dropdown';
import { Tooltip } from './ui/Tooltip';
import type { DropdownItem } from './ui/Dropdown';
Expand Down Expand Up @@ -52,6 +53,8 @@ export function ProjectSessionList({
const [projects, setProjects] = useState<Project[]>([]);
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [createForProject, setCreateForProject] = useState<Project | null>(null);
const [settingsProject, setSettingsProject] = useState<Project | null>(null);
const [showProjectSettings, setShowProjectSettings] = useState(false);
const [sidebarPaneRowLayout, setSidebarPaneRowLayout] = useState<SidebarPaneRowLayout>('single');
const knownSessionIdsRef = useRef<Set<string> | null>(null);

Expand Down Expand Up @@ -195,6 +198,23 @@ export function ProjectSessionList({
setShowCreateDialog(true);
};

const handleOpenProjectSettings = (project: Project) => {
setSettingsProject(project);
setShowProjectSettings(true);
};

const handleProjectUpdated = () => {
loadProjects();
window.dispatchEvent(new Event('project-changed'));
};

const handleProjectSettingsDeleted = () => {
setShowProjectSettings(false);
setSettingsProject(null);
loadProjects();
window.dispatchEvent(new Event('project-changed'));
};

// Session operations
const handleArchiveSession = async (sessionId: string) => {
try {
Expand Down Expand Up @@ -416,6 +436,12 @@ export function ProjectSessionList({
icon: GitBranch,
onClick: () => navigateToProject(project.id),
},
{
id: 'project-settings',
label: 'Project Settings',
icon: Settings,
onClick: () => handleOpenProjectSettings(project),
},
{
id: 'delete',
label: 'Delete Project',
Expand Down Expand Up @@ -539,6 +565,19 @@ export function ProjectSessionList({
isOpen={showAddProjectDialog}
onClose={() => setShowAddProjectDialog(false)}
/>

{settingsProject && (
<ProjectSettings
project={settingsProject}
isOpen={showProjectSettings}
onClose={() => {
setShowProjectSettings(false);
setSettingsProject(null);
}}
onUpdate={handleProjectUpdated}
onDelete={handleProjectSettingsDeleted}
/>
)}
</>
);
}
Expand Down
20 changes: 20 additions & 0 deletions tests/electronApiMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export async function installElectronApiMock(page: Page, options: ElectronApiMoc
let mockActiveProjectId = mockOptions.activeProjectId === undefined
? (mockProjects.find((project) => project.active === true)?.id as number | undefined) ?? null
: mockOptions.activeProjectId;
let lastProjectUpdate: { projectId: string; updates: Record<string, unknown> } | null = null;
let cloudDisconnectError: string | null = null;
let configGetCount = 0;
let nextConfigUpdateError: string | null = null;
Expand Down Expand Up @@ -372,6 +373,12 @@ export async function installElectronApiMock(page: Page, options: ElectronApiMoc
folders: namespace({
getByProject: () => success([]),
}),
git: namespace({
detectBranch: () => success('main'),
}),
dialog: namespace({
openDirectory: () => success('/tmp/pane-worktrees'),
}),
onboarding: namespace({
detectEnvironment: () => success({}),
getGitHubAuthCommand: () => success({ command: '', reason: 'ready' }),
Expand Down Expand Up @@ -431,6 +438,16 @@ export async function installElectronApiMock(page: Page, options: ElectronApiMoc
{ name: 'origin/main', isCurrent: false, hasWorktree: false, isRemote: true },
{ name: 'main', isCurrent: true, hasWorktree: false, isRemote: false },
]),
update: (projectId: string, updates: Record<string, unknown>) => {
lastProjectUpdate = { projectId, updates: clone(updates) };
mockProjects = mockProjects.map((project) => (
String(project.id) === projectId
? { ...project, ...clone(updates), updated_at: new Date().toISOString() }
: project
));
return success(mockProjects.find((project) => String(project.id) === projectId) ?? null);
},
detectConfig: () => success(null),
refreshGitStatus: () => success(),
}),
prompts: namespace({
Expand Down Expand Up @@ -742,6 +759,9 @@ export async function installElectronApiMock(page: Page, options: ElectronApiMoc
getSessionFavoriteToggleCalls() {
return clone(sessionFavoriteToggleCalls);
},
getProjectUpdates() {
return lastProjectUpdate ? [clone(lastProjectUpdate)] : [];
},
},
});
}, options);
Expand Down
58 changes: 57 additions & 1 deletion tests/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,25 @@ import { test, expect, Page } from '@playwright/test';
import { installElectronApiMock } from './electronApiMock';

test.beforeEach(async ({ page }) => {
await installElectronApiMock(page);
await installElectronApiMock(page, {
initialProjects: [
{
id: 1,
name: 'Mock Repo',
path: '/tmp/mock-repo',
system_prompt: null,
run_script: null,
build_script: null,
archive_script: null,
active: true,
created_at: new Date(0).toISOString(),
updated_at: new Date(0).toISOString(),
open_ide_command: null,
displayOrder: 0,
worktree_folder: null,
},
],
});
});

async function dismissStartupDialogs(page: Page) {
Expand Down Expand Up @@ -128,6 +146,44 @@ test.describe('Smoke Tests', () => {
await expect(page.getByText('Something went wrong')).toHaveCount(0);
});

test('Repository menu opens Project Settings with editable Worktree Folder', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });

await dismissStartupDialogs(page);

const repoActionsButton = page.getByRole('button', { name: 'Repository actions for Mock Repo' });
await expect(repoActionsButton).toBeVisible({ timeout: 5000 });
await clickDomNode(repoActionsButton);

await clickDomNode(page.getByRole('menuitem', { name: 'Project Settings' }));

await expect(page.getByText('Project Settings')).toBeVisible({ timeout: 5000 });
await expect(page.getByText('Worktree Folder')).toBeVisible();

const worktreeFolderInput = page.getByPlaceholder('worktrees');
await setInputValue(worktreeFolderInput, '/tmp/pane-worktrees');
await clickDomNode(page.getByRole('button', { name: 'Save Changes' }).first());

const projectUpdates = await page.evaluate(() => {
const mock = (window as typeof window & {
__paneTestElectronMock?: {
getProjectUpdates: () => Array<{ projectId: string; updates: Record<string, unknown> }>;
};
}).__paneTestElectronMock;

return mock?.getProjectUpdates() ?? [];
});

expect(projectUpdates).toHaveLength(1);
expect(projectUpdates[0]).toMatchObject({
projectId: '1',
updates: {
worktree_folder: '/tmp/pane-worktrees',
},
});
await expect(page.getByText('Something went wrong')).toHaveCount(0);
});

test('Remote daemon settings can create a paired profile and switch modes', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });

Expand Down
Loading