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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Spaces: allow empty Spaces (no last-node warning/auto-close), add pane context menu action to create an empty Space, and allow archiving a Space without saving its history. (#171)

### 🐞 Fixed
- Task: keep priority menus above the Task Creator and Task Editor dialog backdrops. (#313)
- Notifications: clicking an Agent notification from another Project now lands on the corresponding Agent instead of only switching Projects. (#310)
- Agent: derive directly launched Agent titles from their first bound session and keep short-turn completion notifications aligned with the resolved title. (#309)
- Sidebar: distinguish expandable Spaces from empty Spaces in the collapsed rail, keeping disclosure arrows only for Spaces with agents and using a subtle leaf marker for empty Spaces. (#308)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export function TaskCreatorWindow({
<CoveSelect
id="workspace-task-priority"
testId="workspace-task-priority"
menuLayer="dialog-popover"
value={taskCreator.priority}
disabled={taskCreator.isCreating || taskCreator.isGeneratingTitle}
options={TASK_PRIORITY_OPTIONS.map(option => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export function TaskEditorWindow({
<CoveSelect
id="workspace-task-editor-priority"
testId="workspace-task-editor-priority"
menuLayer="dialog-popover"
value={taskEditor.priority}
disabled={taskEditor.isSaving || taskEditor.isGeneratingTitle}
options={TASK_PRIORITY_OPTIONS.map(option => ({
Expand Down
55 changes: 54 additions & 1 deletion tests/e2e/workspace-canvas.tasks.edit-delete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ import {
} from './workspace-canvas.helpers'

test.describe('Workspace Canvas - Tasks (Edit & Delete)', () => {
test('supports task edit, resize, and delete confirmation', async () => {
test('supports task edit, resize, and delete confirmation', async ({
browserName: _browserName,
}, testInfo) => {
const { electronApp, window } = await launchApp()

try {
const initialViewport = await window.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
}))

await window.setViewportSize({ width: 720, height: 480 })

await clearAndSeedWorkspace(window, [
{
id: 'task-edit-node',
Expand All @@ -22,6 +31,7 @@ test.describe('Workspace Canvas - Tasks (Edit & Delete)', () => {
task: {
requirement: 'Initial requirement text',
status: 'todo',
priority: 'medium',
linkedAgentNodeId: null,
lastRunAt: null,
autoGeneratedTitle: false,
Expand Down Expand Up @@ -54,10 +64,51 @@ test.describe('Workspace Canvas - Tasks (Edit & Delete)', () => {
.locator('[data-testid="workspace-task-editor-requirement"]')
.fill('Implement retry with capped exponential backoff and jitter')

const priorityMenu = window.locator('[data-testid="workspace-task-editor-priority-menu"]')
await window.locator('[data-testid="workspace-task-editor-priority-trigger"]').click()
await expect(priorityMenu).toBeVisible()
await expect(priorityMenu).toHaveClass(/cove-select__menu--within-dialog/)

const editorBackdrop = window.locator('.cove-window-backdrop').filter({ has: editor })
const menuZIndex = await priorityMenu.evaluate(element =>
Number(window.getComputedStyle(element).zIndex),
)
const backdropZIndex = await editorBackdrop.evaluate(element =>
Number(window.getComputedStyle(element).zIndex),
)

expect(menuZIndex).toBe(175)
expect(backdropZIndex).toBe(160)
expect(menuZIndex).toBeGreaterThan(backdropZIndex)

const priorityMenuBox = await priorityMenu.boundingBox()
const viewport = window.viewportSize()
if (!priorityMenuBox || !viewport) {
throw new Error('task priority menu or viewport bounding box unavailable')
}

expect(priorityMenuBox.x).toBeGreaterThanOrEqual(0)
expect(priorityMenuBox.y).toBeGreaterThanOrEqual(0)
expect(priorityMenuBox.x + priorityMenuBox.width).toBeLessThanOrEqual(viewport.width)
expect(priorityMenuBox.y + priorityMenuBox.height).toBeLessThanOrEqual(viewport.height)

const urgentOption = priorityMenu.locator('[data-cove-select-option-value="urgent"]')
await expect(urgentOption).toBeVisible()

const screenshotPath = testInfo.outputPath('task-editor-priority-menu-layer.png')
await window.screenshot({ path: screenshotPath })
await testInfo.attach('task-editor-priority-menu-layer', {
path: screenshotPath,
contentType: 'image/png',
})

await urgentOption.click()
await window.setViewportSize(initialViewport)
await window.locator('[data-testid="workspace-task-edit-submit"]').click()

await expect(taskNode.locator('[data-testid="task-node-inline-title-input"]')).toHaveCount(0)
await expect(taskNode.locator('.task-node__header')).toContainText('Retry login workflow')
await expect(taskNode.locator('.task-node__priority')).toHaveText('URGENT')
await expect(inlineRequirementInput).toHaveValue(/capped exponential/)
await expect(taskNode.locator('.task-node__inline-hint')).toHaveCount(0)

Expand Down Expand Up @@ -116,6 +167,7 @@ test.describe('Workspace Canvas - Tasks (Edit & Delete)', () => {
title: string
task?: {
requirement?: string
priority?: string
}
}>
}>
Expand All @@ -129,6 +181,7 @@ test.describe('Workspace Canvas - Tasks (Edit & Delete)', () => {
expect(resizedTask?.height ?? 0).toBeGreaterThan(280)
expect(resizedTask?.title).toBe('Retry login workflow final')
expect(resizedTask?.task?.requirement).toContain('capped exponential')
expect(resizedTask?.task?.priority).toBe('urgent')

await taskNode.locator('[data-testid="task-node-close"]').click()

Expand Down
8 changes: 8 additions & 0 deletions tests/unit/contexts/coveSelect.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ describe('CoveSelect', () => {
expect(screen.queryByRole('option', { name: 'Light' })).not.toBeInTheDocument()
})

it('keeps a standalone select on the default popover layer', () => {
renderHarness()

fireEvent.click(screen.getByTestId('theme-select-trigger'))

expect(screen.getByRole('listbox')).not.toHaveClass('cove-select__menu--within-dialog')
})

it('raises a portaled menu above its containing dialog', () => {
render(
<Dialog open aria-label="Preferences" onDismiss={() => {}}>
Expand Down
89 changes: 89 additions & 0 deletions tests/unit/contexts/taskPrioritySelectLayer.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { useState } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import type {
TaskCreatorState,
TaskEditorState,
} from '../../../src/contexts/workspace/presentation/renderer/components/workspaceCanvas/types'
import { TaskCreatorWindow } from '../../../src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskCreatorWindow'
import { TaskEditorWindow } from '../../../src/contexts/workspace/presentation/renderer/components/workspaceCanvas/windows/TaskEditorWindow'

function TaskEditorHarness(): React.JSX.Element {
const [taskEditor, setTaskEditor] = useState<TaskEditorState | null>({
nodeId: 'task-edit-node',
initialTitle: 'Initial Task Title',
initialAutoGeneratedTitle: false,
titleGeneratedInEditor: false,
title: 'Initial Task Title',
requirement: 'Initial requirement text',
priority: 'medium',
selectedTags: [],
autoGenerateTitle: false,
isGeneratingTitle: false,
isSaving: false,
error: null,
})

return (
<TaskEditorWindow
taskEditor={taskEditor}
taskTitleProviderLabel="Codex"
taskTitleModelLabel="gpt-5.2"
taskTagOptions={[]}
setTaskEditor={setTaskEditor}
closeTaskEditor={() => {}}
generateTaskEditorTitle={async () => {}}
saveTaskEdits={async () => {}}
/>
)
}

function TaskCreatorHarness(): React.JSX.Element {
const [taskCreator, setTaskCreator] = useState<TaskCreatorState | null>({
anchor: { x: 120, y: 80 },
title: '',
requirement: '',
priority: 'medium',
selectedTags: [],
autoGenerateTitle: false,
isGeneratingTitle: false,
isCreating: false,
error: null,
})

return (
<TaskCreatorWindow
taskCreator={taskCreator}
taskTitleProviderLabel="Codex"
taskTitleModelLabel="gpt-5.2"
taskTagOptions={[]}
setTaskCreator={setTaskCreator}
closeTaskCreator={() => {}}
generateTaskTitle={async () => {}}
createTask={async () => {}}
/>
)
}

describe('task priority select dialog layer', () => {
it('raises the Task Editor priority menu above its modal backdrop', () => {
render(<TaskEditorHarness />)

fireEvent.click(screen.getByTestId('workspace-task-editor-priority-trigger'))

const menu = screen.getByTestId('workspace-task-editor-priority-menu')
expect(menu.parentElement).toBe(document.body)
expect(menu).toHaveClass('cove-select__menu--within-dialog')
})

it('raises the Task Creator priority menu above its modal backdrop', () => {
render(<TaskCreatorHarness />)

fireEvent.click(screen.getByTestId('workspace-task-advanced-toggle'))
fireEvent.click(screen.getByTestId('workspace-task-priority-trigger'))

const menu = screen.getByTestId('workspace-task-priority-menu')
expect(menu.parentElement).toBe(document.body)
expect(menu).toHaveClass('cove-select__menu--within-dialog')
})
})
Loading