FN-5825: add dedicated grouped-task modal popup
Introduce a dedicated modal flow for grouped shared-branch tasks in the dashboard. - Add GroupTaskModal component and styles for grouped task details and actions. - Wire grouped-task modal state through App, modal manager hooks, and modal container components. - Update board/column/task-card interactions to open grouped tasks in the new modal. - Adjust subtask breakdown behavior and add focused tests for grouped task modal/task-card flows. - Document the grouped-task modal behavior and add a changeset for @runfusion/fusion. Files changed: .changeset/fn-5825-group-task-modal.md | 5 + docs/dashboard-guide.md | 5 +- packages/dashboard/app/App.tsx | 6 + packages/dashboard/app/components/AppModals.tsx | 24 ++++ packages/dashboard/app/components/Board.tsx | 4 +- packages/dashboard/app/components/Column.tsx | 4 +- .../dashboard/app/components/GroupTaskModal.css | 79 ++++++++++ .../dashboard/app/components/GroupTaskModal.tsx | 159 +++++++++++++++++++++ .../app/components/SubtaskBreakdownModal.tsx | 16 ++- packages/dashboard/app/components/TaskCard.tsx | 8 ++ .../components/__tests__/GroupTaskModal.test.tsx | 105 ++++++++++++++ .../app/components/__tests__/TaskCard.test.tsx | 18 +++ packages/dashboard/app/hooks/useModalManager.ts | 16 +++ packages/dashboard/vitest.config.ts | 2 +- 14 files changed, 445 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-5825 Fusion-Task-Lineage: d9317a58-05ce-4437-8311-b7e2a186537b
This commit is contained in:
5
.changeset/fn-5825-group-task-modal.md
Normal file
5
.changeset/fn-5825-group-task-modal.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add a dedicated dashboard Group Task Modal for shared branch groups. Grouped badges in task cards and subtask planning now open a modal showing shared branch status, member landed progress, tracked PR state, member-task quick links, and completion-gated promote actions.
|
||||||
@@ -1126,9 +1126,12 @@ UI surfaces:
|
|||||||
|
|
||||||
- Subtask planning interview shows a grouped indicator when `assignmentMode=shared`.
|
- Subtask planning interview shows a grouped indicator when `assignmentMode=shared`.
|
||||||
- Task cards show grouped/shared branch metadata for grouped tasks.
|
- Task cards show grouped/shared branch metadata for grouped tasks.
|
||||||
|
- Clicking either grouped badge opens the dedicated **Group Task Modal** for that branch group.
|
||||||
- Task detail renders a branch-group card with member landed progress.
|
- Task detail renders a branch-group card with member landed progress.
|
||||||
|
|
||||||
The branch-group card is completion-gated: while members are still pending, it shows progress only. PR / merge controls are only revealed after all members are landed into the shared branch. When auto-merge is off, promote/open-PR is explicit user action (no automatic push-to-origin behavior).
|
The Group Task Modal shows shared branch name/status, member list (`taskId`, title, column, landed state), quick links to open each member task detail, completion progress (`X of Y members finished`), and tracked PR state when present. It live-refreshes from the same dashboard task-update stream and ignores stale cross-project events.
|
||||||
|
|
||||||
|
Both the modal and branch-group card are completion-gated: while members are still pending, they show progress only. PR / merge controls are only revealed after all members are landed into the shared branch. When auto-merge is off, promote/open-PR is explicit user action (no automatic push-to-origin behavior).
|
||||||
|
|
||||||
### CLI-onboarding backfill runbook
|
### CLI-onboarding backfill runbook
|
||||||
|
|
||||||
|
|||||||
@@ -1116,6 +1116,11 @@ function AppInner() {
|
|||||||
pushNav({ type: "modal", close: modalManager.closeSubtask });
|
pushNav({ type: "modal", close: modalManager.closeSubtask });
|
||||||
}, [modalManager, pushNav]);
|
}, [modalManager, pushNav]);
|
||||||
|
|
||||||
|
const openGroupModalWithNav = useCallback((groupId: string) => {
|
||||||
|
modalManager.openGroupModal(groupId);
|
||||||
|
pushNav({ type: "modal", close: modalManager.closeGroupModal });
|
||||||
|
}, [modalManager, pushNav]);
|
||||||
|
|
||||||
const openGitHubImportWithNav = useCallback(() => {
|
const openGitHubImportWithNav = useCallback(() => {
|
||||||
modalManager.openGitHubImport();
|
modalManager.openGitHubImport();
|
||||||
pushNav({ type: "modal", close: modalManager.closeGitHubImport });
|
pushNav({ type: "modal", close: modalManager.closeGitHubImport });
|
||||||
@@ -1630,6 +1635,7 @@ function AppInner() {
|
|||||||
onMoveTask={moveTask}
|
onMoveTask={moveTask}
|
||||||
onPauseTask={pauseTask}
|
onPauseTask={pauseTask}
|
||||||
onOpenDetail={openDetailTask}
|
onOpenDetail={openDetailTask}
|
||||||
|
onOpenGroupModal={openGroupModalWithNav}
|
||||||
addToast={addToast}
|
addToast={addToast}
|
||||||
onQuickCreate={handleBoardQuickCreate}
|
onQuickCreate={handleBoardQuickCreate}
|
||||||
onNewTask={openNewTaskWithNav}
|
onNewTask={openNewTaskWithNav}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { WorkflowStepManager } from "./WorkflowStepManager";
|
|||||||
import { AgentListModal } from "./AgentListModal";
|
import { AgentListModal } from "./AgentListModal";
|
||||||
import { ModelOnboardingModal } from "./ModelOnboardingModal";
|
import { ModelOnboardingModal } from "./ModelOnboardingModal";
|
||||||
import { ToastContainer } from "./ToastContainer";
|
import { ToastContainer } from "./ToastContainer";
|
||||||
|
import { GroupTaskModal } from "./GroupTaskModal";
|
||||||
import { useNavigationHistoryContext } from "../hooks/useNavigationHistory";
|
import { useNavigationHistoryContext } from "../hooks/useNavigationHistory";
|
||||||
|
|
||||||
const SetupWizardModal = lazy(() => import("./SetupWizardModal").then((m) => ({ default: m.SetupWizardModal })));
|
const SetupWizardModal = lazy(() => import("./SetupWizardModal").then((m) => ({ default: m.SetupWizardModal })));
|
||||||
@@ -149,6 +150,11 @@ export function AppModals({
|
|||||||
[modalManager, pushNav],
|
[modalManager, pushNav],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openGroupModalWithNav = useCallback((groupId: string) => {
|
||||||
|
modalManager.openGroupModal(groupId);
|
||||||
|
pushNav({ type: "modal", close: modalManager.closeGroupModal });
|
||||||
|
}, [modalManager, pushNav]);
|
||||||
|
|
||||||
const handleOnboardingViewTask = useCallback((task: Task) => {
|
const handleOnboardingViewTask = useCallback((task: Task) => {
|
||||||
setFirstCreatedTask(null);
|
setFirstCreatedTask(null);
|
||||||
modalManager.closeModelOnboarding();
|
modalManager.closeModelOnboarding();
|
||||||
@@ -202,6 +208,23 @@ export function AppModals({
|
|||||||
</ModalErrorBoundary>
|
</ModalErrorBoundary>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{modalManager.groupModalGroupId && (
|
||||||
|
<ModalErrorBoundary>
|
||||||
|
<GroupTaskModal
|
||||||
|
isOpen={Boolean(modalManager.groupModalGroupId)}
|
||||||
|
onClose={modalManager.closeGroupModal}
|
||||||
|
groupId={modalManager.groupModalGroupId}
|
||||||
|
projectId={projectId}
|
||||||
|
onOpenMemberTask={(taskId) => {
|
||||||
|
const memberTask = tasks.find((task) => task.id === taskId);
|
||||||
|
if (memberTask) {
|
||||||
|
openDetailTaskWithNav(memberTask);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalErrorBoundary>
|
||||||
|
)}
|
||||||
|
|
||||||
{modalManager.settingsOpen && (
|
{modalManager.settingsOpen && (
|
||||||
<ModalErrorBoundary>
|
<ModalErrorBoundary>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
@@ -252,6 +275,7 @@ export function AppModals({
|
|||||||
onTasksCreated={taskHandlers.handleSubtaskTasksCreated}
|
onTasksCreated={taskHandlers.handleSubtaskTasksCreated}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
resumeSessionId={modalManager.subtaskResumeSessionId}
|
resumeSessionId={modalManager.subtaskResumeSessionId}
|
||||||
|
onOpenGroupModal={openGroupModalWithNav}
|
||||||
/>
|
/>
|
||||||
</ModalErrorBoundary>
|
</ModalErrorBoundary>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface BoardProps {
|
|||||||
onMoveTask: (id: string, column: ColumnType) => Promise<Task>;
|
onMoveTask: (id: string, column: ColumnType) => Promise<Task>;
|
||||||
onPauseTask?: (id: string) => Promise<Task>;
|
onPauseTask?: (id: string) => Promise<Task>;
|
||||||
onOpenDetail: (task: Task | TaskDetail) => void;
|
onOpenDetail: (task: Task | TaskDetail) => void;
|
||||||
|
onOpenGroupModal?: (groupId: string) => void;
|
||||||
addToast: (message: string, type?: ToastType) => void;
|
addToast: (message: string, type?: ToastType) => void;
|
||||||
onQuickCreate?: (input: TaskCreateInput) => Promise<Task | void>;
|
onQuickCreate?: (input: TaskCreateInput) => Promise<Task | void>;
|
||||||
onNewTask: () => void;
|
onNewTask: () => void;
|
||||||
@@ -80,7 +81,7 @@ function areWorkflowNameLookupsEqual(previous: ReadonlyMap<string, string>, next
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable }: BoardProps) {
|
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable }: BoardProps) {
|
||||||
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
|
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
|
||||||
const archivedLoadedRef = useRef(false);
|
const archivedLoadedRef = useRef(false);
|
||||||
const [workflowStepNameLookup, setWorkflowStepNameLookup] = useState<ReadonlyMap<string, string>>(EMPTY_WORKFLOW_STEP_NAME_LOOKUP);
|
const [workflowStepNameLookup, setWorkflowStepNameLookup] = useState<ReadonlyMap<string, string>>(EMPTY_WORKFLOW_STEP_NAME_LOOKUP);
|
||||||
@@ -280,6 +281,7 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask
|
|||||||
onMoveTask={onMoveTask}
|
onMoveTask={onMoveTask}
|
||||||
onPauseTask={onPauseTask}
|
onPauseTask={onPauseTask}
|
||||||
onOpenDetail={onOpenDetail}
|
onOpenDetail={onOpenDetail}
|
||||||
|
onOpenGroupModal={onOpenGroupModal}
|
||||||
addToast={addToast}
|
addToast={addToast}
|
||||||
globalPaused={globalPaused}
|
globalPaused={globalPaused}
|
||||||
onUpdateTask={onUpdateTask}
|
onUpdateTask={onUpdateTask}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface ColumnProps {
|
|||||||
onMoveTask: (id: string, column: ColumnType, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise<Task>;
|
onMoveTask: (id: string, column: ColumnType, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise<Task>;
|
||||||
onPauseTask?: (id: string) => Promise<Task>;
|
onPauseTask?: (id: string) => Promise<Task>;
|
||||||
onOpenDetail: (task: Task | TaskDetail) => void;
|
onOpenDetail: (task: Task | TaskDetail) => void;
|
||||||
|
onOpenGroupModal?: (groupId: string) => void;
|
||||||
addToast: (message: string, type?: ToastType) => void;
|
addToast: (message: string, type?: ToastType) => void;
|
||||||
onQuickCreate?: (input: TaskCreateInput) => Promise<Task | void>;
|
onQuickCreate?: (input: TaskCreateInput) => Promise<Task | void>;
|
||||||
onNewTask?: () => void;
|
onNewTask?: () => void;
|
||||||
@@ -77,7 +78,7 @@ interface ColumnProps {
|
|||||||
prAuthAvailable?: boolean;
|
prAuthAvailable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, isSearchActive, taskStuckTimeoutMs, onOpenMission, lastFetchTimeMs, workflowStepNameLookup, blockerFanoutMap, prAuthAvailable }: ColumnProps) {
|
function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, onPauseTask, onOpenDetail, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, isSearchActive, taskStuckTimeoutMs, onOpenMission, lastFetchTimeMs, workflowStepNameLookup, blockerFanoutMap, prAuthAvailable }: ColumnProps) {
|
||||||
const [dragOver, setDragOver] = useState(false);
|
const [dragOver, setDragOver] = useState(false);
|
||||||
const [visibleTaskCount, setVisibleTaskCount] = useState(VISIBLE_TASKS_INITIAL);
|
const [visibleTaskCount, setVisibleTaskCount] = useState(VISIBLE_TASKS_INITIAL);
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
@@ -523,6 +524,7 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
|
|||||||
task={task}
|
task={task}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
onOpenDetail={onOpenDetail}
|
onOpenDetail={onOpenDetail}
|
||||||
|
onOpenGroupModal={onOpenGroupModal}
|
||||||
addToast={addToast}
|
addToast={addToast}
|
||||||
globalPaused={globalPaused}
|
globalPaused={globalPaused}
|
||||||
onUpdateTask={onUpdateTask}
|
onUpdateTask={onUpdateTask}
|
||||||
|
|||||||
79
packages/dashboard/app/components/GroupTaskModal.css
Normal file
79
packages/dashboard/app/components/GroupTaskModal.css
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
.group-task-modal {
|
||||||
|
max-width: min(var(--layout-content-max-width), calc(100vw - var(--spacing-2xl)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-error {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-summary,
|
||||||
|
.group-task-modal-members-card,
|
||||||
|
.group-task-modal-pr,
|
||||||
|
.group-task-modal-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-summary-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-label {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-progress-text {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-members {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-member {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto auto auto;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-member-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-member-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.group-task-modal-member {
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-task-modal-member .badge,
|
||||||
|
.group-task-modal-member-status {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
159
packages/dashboard/app/components/GroupTaskModal.tsx
Normal file
159
packages/dashboard/app/components/GroupTaskModal.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import "./GroupTaskModal.css";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { CheckCircle2, CircleDashed, ExternalLink, Loader2, X } from "lucide-react";
|
||||||
|
import { apiGetBranchGroup, apiPromoteBranchGroup, type BranchGroupSummary } from "../api";
|
||||||
|
import { subscribeSse } from "../sse-bus";
|
||||||
|
|
||||||
|
interface GroupTaskModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
groupId: string | null;
|
||||||
|
projectId?: string;
|
||||||
|
onOpenMemberTask: (taskId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GroupTaskModal({ isOpen, onClose, groupId, projectId, onOpenMemberTask }: GroupTaskModalProps) {
|
||||||
|
const [group, setGroup] = useState<BranchGroupSummary | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [promoting, setPromoting] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadGroup = useCallback(async () => {
|
||||||
|
if (!groupId) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await apiGetBranchGroup(groupId, projectId);
|
||||||
|
setGroup(response.group);
|
||||||
|
setError(null);
|
||||||
|
} catch (loadError) {
|
||||||
|
setError(loadError instanceof Error ? loadError.message : "Failed to load branch group");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [groupId, projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !groupId) return;
|
||||||
|
void loadGroup();
|
||||||
|
}, [groupId, isOpen, loadGroup]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen || !groupId) return;
|
||||||
|
const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
|
||||||
|
return subscribeSse(`/api/events${query}`, {
|
||||||
|
events: {
|
||||||
|
"task:updated": (event) => {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(event.data) as { projectId?: string };
|
||||||
|
if (projectId && payload.projectId && payload.projectId !== projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
void loadGroup();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onReconnect: () => {
|
||||||
|
void loadGroup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [groupId, isOpen, loadGroup, projectId]);
|
||||||
|
|
||||||
|
const completionText = useMemo(() => {
|
||||||
|
if (!group) return "";
|
||||||
|
return `${group.completion.landed} of ${group.completion.total} members finished`;
|
||||||
|
}, [group]);
|
||||||
|
|
||||||
|
const completionPercent = useMemo(() => {
|
||||||
|
if (!group || group.completion.total <= 0) return 0;
|
||||||
|
return (group.completion.landed / group.completion.total) * 100;
|
||||||
|
}, [group]);
|
||||||
|
|
||||||
|
const onPromote = useCallback(async () => {
|
||||||
|
if (!groupId) return;
|
||||||
|
setPromoting(true);
|
||||||
|
try {
|
||||||
|
await apiPromoteBranchGroup(groupId, projectId);
|
||||||
|
await loadGroup();
|
||||||
|
} finally {
|
||||||
|
setPromoting(false);
|
||||||
|
}
|
||||||
|
}, [groupId, loadGroup, projectId]);
|
||||||
|
|
||||||
|
if (!isOpen || !groupId) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay open" onClick={onClose}>
|
||||||
|
<div className="modal modal-lg group-task-modal" role="dialog" aria-modal="true" aria-label="Branch group details" onClick={(event) => event.stopPropagation()}>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>Branch Group {groupId}</h2>
|
||||||
|
<button type="button" className="modal-close" onClick={onClose} aria-label="Close group modal">
|
||||||
|
<X />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body group-task-modal-body">
|
||||||
|
{loading && (
|
||||||
|
<div className="card group-task-modal-state"><Loader2 className="spin" /> Loading branch group…</div>
|
||||||
|
)}
|
||||||
|
{!loading && error && <div className="card group-task-modal-state group-task-modal-error">{error}</div>}
|
||||||
|
{!loading && !error && !group && <div className="card group-task-modal-state">Branch group unavailable</div>}
|
||||||
|
{!loading && !error && group && (
|
||||||
|
<>
|
||||||
|
<section className="card group-task-modal-summary">
|
||||||
|
<div className="group-task-modal-summary-row">
|
||||||
|
<span className="group-task-modal-label">Shared branch</span>
|
||||||
|
<strong>{group.branchName}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="group-task-modal-summary-row">
|
||||||
|
<span className="group-task-modal-label">Status</span>
|
||||||
|
<span className="badge">{group.status}</span>
|
||||||
|
</div>
|
||||||
|
<div className="group-task-modal-progress-text">{completionText}</div>
|
||||||
|
<div className="branch-group-card-progress" role="progressbar" aria-valuenow={group.completion.landed} aria-valuemin={0} aria-valuemax={group.completion.total}>
|
||||||
|
<span className="branch-group-card-progress-fill" style={{ width: `${completionPercent}%` }} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="card group-task-modal-members-card">
|
||||||
|
<h3>Members</h3>
|
||||||
|
<ul className="group-task-modal-members">
|
||||||
|
{group.members.map((member) => (
|
||||||
|
<li key={member.taskId} className="group-task-modal-member">
|
||||||
|
<span className={`status-dot ${member.landed ? "status-dot--online" : "status-dot--pending"}`} />
|
||||||
|
<span className="group-task-modal-member-main">{member.taskId} · {member.title}</span>
|
||||||
|
<span className="badge">{member.column}</span>
|
||||||
|
<span className="group-task-modal-member-status">{member.landed ? <CheckCircle2 /> : <CircleDashed />}</span>
|
||||||
|
<button type="button" className="btn btn-sm" onClick={() => onOpenMemberTask(member.taskId)}>Open task</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{group.prUrl && (
|
||||||
|
<section className="card group-task-modal-pr">
|
||||||
|
<a className="btn" href={group.prUrl} target="_blank" rel="noreferrer">
|
||||||
|
PR #{group.prNumber ?? "—"} ({group.prState}) <ExternalLink />
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{group.completion.complete && (
|
||||||
|
<section className="card group-task-modal-actions">
|
||||||
|
{group.autoMerge ? (
|
||||||
|
<span className="badge">Auto-merge enabled</span>
|
||||||
|
) : (
|
||||||
|
<button type="button" className="btn" onClick={() => void onPromote()} disabled={promoting}>
|
||||||
|
{promoting ? <Loader2 className="spin" /> : null}
|
||||||
|
{group.prState === "none" ? "Open PR" : "Merge group into main"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ interface SubtaskBreakdownModalProps {
|
|||||||
parentTaskId?: string;
|
parentTaskId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
resumeSessionId?: string;
|
resumeSessionId?: string;
|
||||||
|
onOpenGroupModal?: (groupId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ViewState =
|
type ViewState =
|
||||||
@@ -74,7 +75,7 @@ function hasDependencyCycle(subtasks: SubtaskItem[]): boolean {
|
|||||||
return subtasks.some((item) => visit(item.id));
|
return subtasks.some((item) => visit(item.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onTasksCreated, parentTaskId, projectId, resumeSessionId }: SubtaskBreakdownModalProps) {
|
export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onTasksCreated, parentTaskId, projectId, resumeSessionId, onOpenGroupModal }: SubtaskBreakdownModalProps) {
|
||||||
const viewportMode = useViewportMode();
|
const viewportMode = useViewportMode();
|
||||||
useMobileScrollLock(isOpen);
|
useMobileScrollLock(isOpen);
|
||||||
const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } = useMobileKeyboard({
|
const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } = useMobileKeyboard({
|
||||||
@@ -735,7 +736,18 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
|
|||||||
<option value="per-task-derived">Per-task branches derived from planning branch</option>
|
<option value="per-task-derived">Per-task branches derived from planning branch</option>
|
||||||
</select>
|
</select>
|
||||||
{branchAssignmentMode === "shared" && branchName.trim() && (
|
{branchAssignmentMode === "shared" && branchName.trim() && (
|
||||||
<p className="text-muted">Grouped on shared branch <strong>{branchName.trim()}</strong></p>
|
<p className="text-muted">
|
||||||
|
Grouped on shared branch <strong>{branchName.trim()}</strong>
|
||||||
|
{onOpenGroupModal && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => onOpenGroupModal(`BG-${branchName.trim()}`)}
|
||||||
|
>
|
||||||
|
Open group modal
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ interface TaskCardProps {
|
|||||||
projectId?: string;
|
projectId?: string;
|
||||||
queued?: boolean;
|
queued?: boolean;
|
||||||
onOpenDetail: (task: Task | TaskDetail) => void;
|
onOpenDetail: (task: Task | TaskDetail) => void;
|
||||||
|
onOpenGroupModal?: (groupId: string) => void;
|
||||||
addToast: (message: string, type?: ToastType) => void;
|
addToast: (message: string, type?: ToastType) => void;
|
||||||
globalPaused?: boolean;
|
globalPaused?: boolean;
|
||||||
onUpdateTask?: (
|
onUpdateTask?: (
|
||||||
@@ -437,6 +438,7 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo
|
|||||||
previous.prAuthAvailable === next.prAuthAvailable &&
|
previous.prAuthAvailable === next.prAuthAvailable &&
|
||||||
previous.autoMergeEnabled === next.autoMergeEnabled &&
|
previous.autoMergeEnabled === next.autoMergeEnabled &&
|
||||||
previous.onOpenDetail === next.onOpenDetail &&
|
previous.onOpenDetail === next.onOpenDetail &&
|
||||||
|
previous.onOpenGroupModal === next.onOpenGroupModal &&
|
||||||
previous.addToast === next.addToast &&
|
previous.addToast === next.addToast &&
|
||||||
previous.onUpdateTask === next.onUpdateTask &&
|
previous.onUpdateTask === next.onUpdateTask &&
|
||||||
previous.onArchiveTask === next.onArchiveTask &&
|
previous.onArchiveTask === next.onArchiveTask &&
|
||||||
@@ -528,6 +530,7 @@ function TaskCardComponent({
|
|||||||
projectId,
|
projectId,
|
||||||
queued,
|
queued,
|
||||||
onOpenDetail,
|
onOpenDetail,
|
||||||
|
onOpenGroupModal,
|
||||||
addToast,
|
addToast,
|
||||||
globalPaused,
|
globalPaused,
|
||||||
onUpdateTask,
|
onUpdateTask,
|
||||||
@@ -1899,6 +1902,11 @@ function TaskCardComponent({
|
|||||||
? `${task.branchContext.groupId} · ${branchMetadata.branch}`
|
? `${task.branchContext.groupId} · ${branchMetadata.branch}`
|
||||||
: task.branchContext.groupId
|
: task.branchContext.groupId
|
||||||
}
|
}
|
||||||
|
onClick={(event) => {
|
||||||
|
if (!onOpenGroupModal) return;
|
||||||
|
event.stopPropagation();
|
||||||
|
onOpenGroupModal(task.branchContext.groupId);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className="card-branch-label">
|
<span className="card-branch-label">
|
||||||
{task.branchContext.assignmentMode === "shared" ? "Shared" : "Group"}
|
{task.branchContext.assignmentMode === "shared" ? "Shared" : "Group"}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { GroupTaskModal } from "../GroupTaskModal";
|
||||||
|
import { apiGetBranchGroup, apiPromoteBranchGroup } from "../../api";
|
||||||
|
|
||||||
|
vi.mock("../../api", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("../../api")>("../../api");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
apiGetBranchGroup: vi.fn(),
|
||||||
|
apiPromoteBranchGroup: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("../../hooks/useNavigationHistory", () => ({
|
||||||
|
useNavigationHistory: () => ({ canGoBack: false, goBack: vi.fn() }),
|
||||||
|
useNavigationHistoryContext: () => ({ pushNav: vi.fn(), replaceCurrent: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockedGet = vi.mocked(apiGetBranchGroup);
|
||||||
|
const mockedPromote = vi.mocked(apiPromoteBranchGroup);
|
||||||
|
|
||||||
|
function makeGroup(overrides: Record<string, unknown> = {}) {
|
||||||
|
return {
|
||||||
|
id: "BG-1",
|
||||||
|
branchName: "feature/shared",
|
||||||
|
status: "open",
|
||||||
|
autoMerge: false,
|
||||||
|
prState: "none",
|
||||||
|
prUrl: null,
|
||||||
|
prNumber: null,
|
||||||
|
completion: { landed: 1, total: 2, complete: false },
|
||||||
|
members: [
|
||||||
|
{ taskId: "FN-1", title: "First", column: "done", landed: true },
|
||||||
|
{ taskId: "FN-2", title: "Second", column: "todo", landed: false },
|
||||||
|
],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("GroupTaskModal", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockedPromote.mockReset();
|
||||||
|
mockedGet.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders group summary and member open action", async () => {
|
||||||
|
mockedGet.mockResolvedValue({ group: makeGroup() } as Awaited<ReturnType<typeof apiGetBranchGroup>>);
|
||||||
|
const onOpenMemberTask = vi.fn();
|
||||||
|
|
||||||
|
render(<GroupTaskModal isOpen onClose={vi.fn()} groupId="BG-1" onOpenMemberTask={onOpenMemberTask} />);
|
||||||
|
|
||||||
|
expect(await screen.findByText("feature/shared")).toBeDefined();
|
||||||
|
expect(screen.getByText("1 of 2 members finished")).toBeDefined();
|
||||||
|
await userEvent.click(screen.getAllByRole("button", { name: "Open task" })[0]);
|
||||||
|
expect(onOpenMemberTask).toHaveBeenCalledWith("FN-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides promote controls until complete", async () => {
|
||||||
|
mockedGet.mockResolvedValue({ group: makeGroup() } as Awaited<ReturnType<typeof apiGetBranchGroup>>);
|
||||||
|
|
||||||
|
render(<GroupTaskModal isOpen onClose={vi.fn()} groupId="BG-1" onOpenMemberTask={vi.fn()} />);
|
||||||
|
|
||||||
|
await screen.findByText("1 of 2 members finished");
|
||||||
|
expect(screen.queryByRole("button", { name: /open pr|merge group into main/i })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("promotes when complete and auto-merge is off", async () => {
|
||||||
|
mockedGet
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
group: makeGroup({ completion: { landed: 2, total: 2, complete: true }, members: [
|
||||||
|
{ taskId: "FN-1", title: "First", column: "done", landed: true },
|
||||||
|
{ taskId: "FN-2", title: "Second", column: "done", landed: true },
|
||||||
|
] }),
|
||||||
|
} as Awaited<ReturnType<typeof apiGetBranchGroup>>)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
group: makeGroup({ completion: { landed: 2, total: 2, complete: true } }),
|
||||||
|
} as Awaited<ReturnType<typeof apiGetBranchGroup>>);
|
||||||
|
|
||||||
|
mockedPromote.mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof apiPromoteBranchGroup>>);
|
||||||
|
|
||||||
|
render(<GroupTaskModal isOpen onClose={vi.fn()} groupId="BG-1" onOpenMemberTask={vi.fn()} />);
|
||||||
|
|
||||||
|
const action = await screen.findByRole("button", { name: /open pr/i });
|
||||||
|
await userEvent.click(action);
|
||||||
|
await waitFor(() => expect(mockedPromote).toHaveBeenCalledWith("BG-1", undefined));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders tracked pr info when present", async () => {
|
||||||
|
mockedGet.mockResolvedValue({
|
||||||
|
group: makeGroup({
|
||||||
|
completion: { landed: 2, total: 2, complete: true },
|
||||||
|
prState: "open",
|
||||||
|
prUrl: "https://github.com/org/repo/pull/1",
|
||||||
|
prNumber: 1,
|
||||||
|
}),
|
||||||
|
} as Awaited<ReturnType<typeof apiGetBranchGroup>>);
|
||||||
|
|
||||||
|
render(<GroupTaskModal isOpen onClose={vi.fn()} groupId="BG-1" onOpenMemberTask={vi.fn()} />);
|
||||||
|
|
||||||
|
const link = await screen.findByRole("link", { name: /PR #1/i });
|
||||||
|
expect(link.getAttribute("href")).toContain("/pull/1");
|
||||||
|
expect(link.textContent).toContain("open");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1409,6 +1409,24 @@ describe("TaskCard", () => {
|
|||||||
expect(screen.getAllByText("feature/shared-branch").length).toBeGreaterThan(0);
|
expect(screen.getAllByText("feature/shared-branch").length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens group modal from shared branch chip", () => {
|
||||||
|
const onOpenGroupModal = vi.fn();
|
||||||
|
render(
|
||||||
|
<TaskCard
|
||||||
|
task={makeTask({
|
||||||
|
branch: "feature/shared-branch",
|
||||||
|
branchContext: { groupId: "BG-22", source: "planning", assignmentMode: "shared" },
|
||||||
|
})}
|
||||||
|
onOpenDetail={noop}
|
||||||
|
onOpenGroupModal={onOpenGroupModal}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Shared"));
|
||||||
|
expect(onOpenGroupModal).toHaveBeenCalledWith("BG-22");
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps long non-default branch names readable via text and title semantics", () => {
|
it("keeps long non-default branch names readable via text and title semantics", () => {
|
||||||
const longBranch = "feature/fn-3423-display-very-long-working-branch-name-for-card-metadata";
|
const longBranch = "feature/fn-3423-display-very-long-working-branch-name-for-card-metadata";
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface ModalManager {
|
|||||||
detailTask: (Task | TaskDetail) | null;
|
detailTask: (Task | TaskDetail) | null;
|
||||||
detailTaskInitialTab: DetailTaskTab;
|
detailTaskInitialTab: DetailTaskTab;
|
||||||
detailTaskOrigin: DetailTaskOrigin | null;
|
detailTaskOrigin: DetailTaskOrigin | null;
|
||||||
|
groupModalGroupId: string | null;
|
||||||
settingsOpen: boolean;
|
settingsOpen: boolean;
|
||||||
settingsInitialSection: SectionId | undefined;
|
settingsInitialSection: SectionId | undefined;
|
||||||
schedulesOpen: boolean;
|
schedulesOpen: boolean;
|
||||||
@@ -81,6 +82,9 @@ export interface ModalManager {
|
|||||||
updateDetailTask: (updated: Partial<TaskDetail>) => void;
|
updateDetailTask: (updated: Partial<TaskDetail>) => void;
|
||||||
closeDetailTask: () => void;
|
closeDetailTask: () => void;
|
||||||
|
|
||||||
|
openGroupModal: (groupId: string) => void;
|
||||||
|
closeGroupModal: () => void;
|
||||||
|
|
||||||
openSettings: (section?: SectionId) => void;
|
openSettings: (section?: SectionId) => void;
|
||||||
closeSettings: () => void;
|
closeSettings: () => void;
|
||||||
|
|
||||||
@@ -152,6 +156,7 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
|
|||||||
const [detailTask, setDetailTask] = useState<(Task | TaskDetail) | null>(null);
|
const [detailTask, setDetailTask] = useState<(Task | TaskDetail) | null>(null);
|
||||||
const [detailTaskInitialTab, setDetailTaskInitialTab] = useState<DetailTaskTab>("definition");
|
const [detailTaskInitialTab, setDetailTaskInitialTab] = useState<DetailTaskTab>("definition");
|
||||||
const [detailTaskOrigin, setDetailTaskOrigin] = useState<DetailTaskOrigin | null>(null);
|
const [detailTaskOrigin, setDetailTaskOrigin] = useState<DetailTaskOrigin | null>(null);
|
||||||
|
const [groupModalGroupId, setGroupModalGroupId] = useState<string | null>(null);
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [settingsInitialSection, setSettingsInitialSection] = useState<SectionId | undefined>(undefined);
|
const [settingsInitialSection, setSettingsInitialSection] = useState<SectionId | undefined>(undefined);
|
||||||
const [schedulesOpen, setSchedulesOpen] = useState(false);
|
const [schedulesOpen, setSchedulesOpen] = useState(false);
|
||||||
@@ -175,6 +180,7 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
|
|||||||
|
|
||||||
const anyModalOpen = Boolean(
|
const anyModalOpen = Boolean(
|
||||||
detailTask ||
|
detailTask ||
|
||||||
|
groupModalGroupId ||
|
||||||
settingsOpen ||
|
settingsOpen ||
|
||||||
newTaskModalOpen ||
|
newTaskModalOpen ||
|
||||||
isPlanningOpen ||
|
isPlanningOpen ||
|
||||||
@@ -261,6 +267,13 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
|
|||||||
setDetailTaskOrigin(null);
|
setDetailTaskOrigin(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const openGroupModal = useCallback((groupId: string) => {
|
||||||
|
setGroupModalGroupId(groupId);
|
||||||
|
}, []);
|
||||||
|
const closeGroupModal = useCallback(() => {
|
||||||
|
setGroupModalGroupId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const openSettings = useCallback((section?: SectionId) => {
|
const openSettings = useCallback((section?: SectionId) => {
|
||||||
setSettingsInitialSection(section);
|
setSettingsInitialSection(section);
|
||||||
setSettingsOpen(true);
|
setSettingsOpen(true);
|
||||||
@@ -376,6 +389,7 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
|
|||||||
detailTask,
|
detailTask,
|
||||||
detailTaskInitialTab,
|
detailTaskInitialTab,
|
||||||
detailTaskOrigin,
|
detailTaskOrigin,
|
||||||
|
groupModalGroupId,
|
||||||
settingsOpen,
|
settingsOpen,
|
||||||
settingsInitialSection,
|
settingsInitialSection,
|
||||||
schedulesOpen,
|
schedulesOpen,
|
||||||
@@ -411,6 +425,8 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
|
|||||||
openDetailWithChangesTab,
|
openDetailWithChangesTab,
|
||||||
updateDetailTask,
|
updateDetailTask,
|
||||||
closeDetailTask,
|
closeDetailTask,
|
||||||
|
openGroupModal,
|
||||||
|
closeGroupModal,
|
||||||
openSettings,
|
openSettings,
|
||||||
closeSettings,
|
closeSettings,
|
||||||
openSchedules,
|
openSchedules,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const qualityAppTests = [
|
|||||||
"app/api/**/*.test.ts",
|
"app/api/**/*.test.ts",
|
||||||
// Representative workflow/component coverage. Exhaustive modal/view suites
|
// Representative workflow/component coverage. Exhaustive modal/view suites
|
||||||
// stay available in the full `dashboard-app` project.
|
// stay available in the full `dashboard-app` project.
|
||||||
"app/components/__tests__/{ActiveAgentsPanel,ActivityLogModal,AgentMentionPopup,AgentMetricsBar,AgentOnboardingModal,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile,board-mobile-view-switch,BranchGroupCard,ChatView,ChatView.autosize,ChatView.chat-input-autosize,ChatView.default-model-icon,ChatView.draft,ChatView.hash-mention,ChatView.rooms,ChatView.scroll-to-top,ChatView.swipe-back,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DevServerView.mobile,DirectoryPicker,DuplicateWarningModal,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,GitHubBadge,InlineCreateCard,LoginInstructions,MemoryView,MergeAdvanceNotice,MessageComposer,MessageComposer.autosize,MobileNavBar,NewTaskModal,NewTaskModal.shared-cache,NodeCard,NodeHealthDot,NodeStatusIndicator,PlanningModeModal.autosize,PrChecksList,PrCreateModal,PrCreateModal.layout,ProjectCard,ProjectSelector,ProviderIcon,PrPanel,PrPanel.merge,PrPanel.reviews,QuickChatFAB,QuickChatFAB.shared-cache,ReliabilityView,ResearchView,SecretsView,SecretsView.mobile,SettingsModal,SettingsModal.testMode,SettingsModal.worktrunk,StashConflictModal,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskCard.badge-wrap,TaskCard.footer-wrap,TaskChangesTab,TaskComments,TaskDetailModal,TaskDetailModal.allow-resurrection,TaskDetailModal.create-pr-e2e,TestModeBanner,TaskDetailModal.create-pr-integration,TaskDetailModal.github-tracking-header,TaskDetailModal.github-tracking-stale,TaskDetailModal.rebind-banner,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,TrackingRepoSelect,WorkflowResultsTab,WorktrunkInstallApprovalDetails}.test.tsx",
|
"app/components/__tests__/{ActiveAgentsPanel,ActivityLogModal,AgentMentionPopup,AgentMetricsBar,AgentOnboardingModal,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile,board-mobile-view-switch,BranchGroupCard,ChatView,ChatView.autosize,ChatView.chat-input-autosize,ChatView.default-model-icon,ChatView.draft,ChatView.hash-mention,ChatView.rooms,ChatView.scroll-to-top,ChatView.swipe-back,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DevServerView.mobile,DirectoryPicker,DuplicateWarningModal,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,GitHubBadge,GroupTaskModal,InlineCreateCard,LoginInstructions,MemoryView,MergeAdvanceNotice,MessageComposer,MessageComposer.autosize,MobileNavBar,NewTaskModal,NewTaskModal.shared-cache,NodeCard,NodeHealthDot,NodeStatusIndicator,PlanningModeModal.autosize,PrChecksList,PrCreateModal,PrCreateModal.layout,ProjectCard,ProjectSelector,ProviderIcon,PrPanel,PrPanel.merge,PrPanel.reviews,QuickChatFAB,QuickChatFAB.shared-cache,ReliabilityView,ResearchView,SecretsView,SecretsView.mobile,SettingsModal,SettingsModal.testMode,SettingsModal.worktrunk,StashConflictModal,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskCard.badge-wrap,TaskCard.footer-wrap,TaskChangesTab,TaskComments,TaskDetailModal,TaskDetailModal.allow-resurrection,TaskDetailModal.create-pr-e2e,TestModeBanner,TaskDetailModal.create-pr-integration,TaskDetailModal.github-tracking-header,TaskDetailModal.github-tracking-stale,TaskDetailModal.rebind-banner,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,TrackingRepoSelect,WorkflowResultsTab,WorktrunkInstallApprovalDetails}.test.tsx",
|
||||||
// Hooks and utilities are fast, user-visible state/formatting behavior.
|
// Hooks and utilities are fast, user-visible state/formatting behavior.
|
||||||
"app/context/**/*.test.tsx",
|
"app/context/**/*.test.tsx",
|
||||||
"app/hooks/__tests__/{useAgents,useAgentLogs,useAgentLogs.resume-instrumentation,useAppSettings,useAuthOnboarding,useConfirm,useCurrentProject,useNodes,useNodes.resume-instrumentation,useNodeSettingsSync,useProjects,useProjects.resume-instrumentation,useMeshState.resume-instrumentation,useManagedDockerNodes.resume-instrumentation,usePrChecksStream.resume-instrumentation,useDevServerLogs.resume-instrumentation,useResearch.resume-instrumentation,useBackgroundSessions.resume-instrumentation,useQuickChat,useTasks,useTasks.resume-instrumentation,useChatRooms.resume-instrumentation,useTerminalSessions,useTheme,useToast,useUsageData,useViewState,useMergeAdvanceNotice}.test.{ts,tsx}",
|
"app/hooks/__tests__/{useAgents,useAgentLogs,useAgentLogs.resume-instrumentation,useAppSettings,useAuthOnboarding,useConfirm,useCurrentProject,useNodes,useNodes.resume-instrumentation,useNodeSettingsSync,useProjects,useProjects.resume-instrumentation,useMeshState.resume-instrumentation,useManagedDockerNodes.resume-instrumentation,usePrChecksStream.resume-instrumentation,useDevServerLogs.resume-instrumentation,useResearch.resume-instrumentation,useBackgroundSessions.resume-instrumentation,useQuickChat,useTasks,useTasks.resume-instrumentation,useChatRooms.resume-instrumentation,useTerminalSessions,useTheme,useToast,useUsageData,useViewState,useMergeAdvanceNotice}.test.{ts,tsx}",
|
||||||
|
|||||||
Reference in New Issue
Block a user