diff --git a/CLA.md b/CLA.md
index d0ca9bb4e7..af6227a959 100644
--- a/CLA.md
+++ b/CLA.md
@@ -29,7 +29,7 @@ Subject to the terms and conditions of this Agreement, You hereby grant to the P
You understand and agree that the Project Owner may, in the future, license the Project, including Your Contributions, under additional licenses beyond the current GNU Affero General Public License version 3.0 (AGPL-3.0). Such additional licenses may include commercial or enterprise licenses.
-This provision ensures the Project has proper licensing flexibility should such licensing options be introduced in the future. The open source version of the Project will continue to be available under AGPL-3.0.
+This provision ensures the Project has proper licensing flexibility should such licensing options be introduced in the future. The open-source version of the Project will continue to be available under AGPL-3.0.
## 5. Representations
diff --git a/apps/backend/runners/github/services/orchestrator_reviewer.py b/apps/backend/runners/github/services/orchestrator_reviewer.py
index ce64bbd0d4..6f517741a6 100644
--- a/apps/backend/runners/github/services/orchestrator_reviewer.py
+++ b/apps/backend/runners/github/services/orchestrator_reviewer.py
@@ -1153,6 +1153,6 @@ def _generate_summary(
lines.append("")
lines.append("---")
- lines.append("_Generated by Auto Claude Orchestrating PR Reviewer (Opus 4.5)_")
+ lines.append("_Generated by Auto Claude Orchestrating PR Reviewer_")
return "\n".join(lines)
diff --git a/apps/frontend/src/renderer/components/KanbanBoard.tsx b/apps/frontend/src/renderer/components/KanbanBoard.tsx
index 861e6da198..8e1291204f 100644
--- a/apps/frontend/src/renderer/components/KanbanBoard.tsx
+++ b/apps/frontend/src/renderer/components/KanbanBoard.tsx
@@ -18,16 +18,19 @@ import {
sortableKeyboardCoordinates,
verticalListSortingStrategy
} from '@dnd-kit/sortable';
-import { Plus, Inbox, Loader2, Eye, CheckCircle2, Archive } from 'lucide-react';
+import { Plus, Inbox, Loader2, Eye, CheckCircle2, Archive, Settings, ListPlus } from 'lucide-react';
import { ScrollArea } from './ui/scroll-area';
import { Button } from './ui/button';
import { Checkbox } from './ui/checkbox';
import { Label } from './ui/label';
import { TaskCard } from './TaskCard';
import { SortableTaskCard } from './SortableTaskCard';
+import { QueueSettingsModal } from './QueueSettingsModal';
import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants';
import { cn } from '../lib/utils';
-import { persistTaskStatus, archiveTasks } from '../stores/task-store';
+import { persistTaskStatus, archiveTasks, useTaskStore } from '../stores/task-store';
+import { updateProjectSettings } from '../stores/project-store';
+import { useProjectStore } from '../stores/project-store';
import type { Task, TaskStatus } from '../../shared/types';
interface KanbanBoardProps {
@@ -43,6 +46,9 @@ interface DroppableColumnProps {
isOver: boolean;
onAddClick?: () => void;
onArchiveAll?: () => void;
+ onQueueSettings?: () => void;
+ onQueueAll?: () => void;
+ maxParallelTasks?: number;
}
// Empty state content for each column
@@ -54,6 +60,12 @@ const getEmptyStateContent = (status: TaskStatus, t: (key: string) => string): {
message: t('kanban.emptyBacklog'),
subtext: t('kanban.emptyBacklogHint')
};
+ case 'queue':
+ return {
+ icon: ,
+ message: t('kanban.emptyQueue'),
+ subtext: t('kanban.emptyQueueHint')
+ };
case 'in_progress':
return {
icon: ,
@@ -86,7 +98,7 @@ const getEmptyStateContent = (status: TaskStatus, t: (key: string) => string): {
}
};
-function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick, onArchiveAll }: DroppableColumnProps) {
+function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick, onArchiveAll, onQueueSettings, onQueueAll, maxParallelTasks }: DroppableColumnProps) {
const { t } = useTranslation('tasks');
const { setNodeRef } = useDroppable({
id: status
@@ -94,10 +106,15 @@ function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick, onArc
const taskIds = tasks.map((t) => t.id);
+ // Check if In Progress column is at capacity
+ const isInProgressFull = status === 'in_progress' && maxParallelTasks && tasks.length >= maxParallelTasks;
+
const getColumnBorderColor = (): string => {
switch (status) {
case 'backlog':
return 'column-backlog';
+ case 'queue':
+ return 'column-queue';
case 'in_progress':
return 'column-in-progress';
case 'ai_review':
@@ -129,19 +146,55 @@ function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick, onArc
{TASK_STATUS_LABELS[status]}
-
- {tasks.length}
-
+ {status === 'in_progress' && maxParallelTasks ? (
+
+ {tasks.length}/{maxParallelTasks}
+
+ ) : (
+
+ {tasks.length}
+
+ )}
- {status === 'backlog' && onAddClick && (
+ {status === 'backlog' && (
+ <>
+ {onQueueAll && tasks.length > 0 && (
+
+
+
+ )}
+ {onAddClick && (
+
+
+
+ )}
+ >
+ )}
+ {status === 'queue' && onQueueSettings && (
-
+
)}
{status === 'done' && onArchiveAll && tasks.length > 0 && (
@@ -216,6 +269,13 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
const [activeTask, setActiveTask] = useState
(null);
const [overColumnId, setOverColumnId] = useState(null);
const [showArchived, setShowArchived] = useState(false);
+ const [showQueueSettings, setShowQueueSettings] = useState(false);
+
+ // Get active project for queue settings
+ const { getActiveProject } = useProjectStore();
+ const activeProject = getActiveProject();
+ const projectId = tasks.length > 0 ? tasks[0].projectId : activeProject?.id || '';
+ const maxParallelTasks = activeProject?.settings.maxParallelTasks ?? 3;
// Count archived tasks for display
const archivedCount = useMemo(() => {
@@ -244,6 +304,7 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
const tasksByStatus = useMemo(() => {
const grouped: Record = {
backlog: [],
+ queue: [],
in_progress: [],
ai_review: [],
human_review: [],
@@ -285,6 +346,36 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
}
};
+ const handleSaveQueueSettings = async (newMaxParallel: number) => {
+ if (!projectId) {
+ console.error('[KanbanBoard] No projectId found');
+ return;
+ }
+
+ const success = await updateProjectSettings(projectId, {
+ maxParallelTasks: newMaxParallel
+ });
+
+ if (!success) {
+ console.error('[KanbanBoard] Failed to save queue settings');
+ }
+ };
+
+ const handleQueueAll = async () => {
+ const backlogTasks = tasksByStatus.backlog;
+ if (backlogTasks.length === 0) return;
+
+ console.log(`[Queue] Moving ${backlogTasks.length} tasks from Planning to Queue`);
+
+ // Move all backlog tasks to queue
+ for (const task of backlogTasks) {
+ await persistTaskStatus(task.id, 'queue');
+ }
+
+ // After all tasks are queued, process the queue to start filling In Progress
+ await processQueue();
+ };
+
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
const task = tasks.find((t) => t.id === active.id);
@@ -316,7 +407,7 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
}
};
- const handleDragEnd = (event: DragEndEvent) => {
+ const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
setActiveTask(null);
setOverColumnId(null);
@@ -326,26 +417,86 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
const activeTaskId = active.id as string;
const overId = over.id as string;
+ // Determine target status
+ let newStatus: TaskStatus | null = null;
+
// Check if dropped on a column
if (TASK_STATUS_COLUMNS.includes(overId as TaskStatus)) {
- const newStatus = overId as TaskStatus;
- const task = tasks.find((t) => t.id === activeTaskId);
+ newStatus = overId as TaskStatus;
+ } else {
+ // Check if dropped on another task - move to that task's column
+ const overTask = tasks.find((t) => t.id === overId);
+ if (overTask) {
+ newStatus = overTask.status;
+ }
+ }
+
+ if (!newStatus) return;
+
+ const task = tasks.find((t) => t.id === activeTaskId);
+ if (!task || task.status === newStatus) return;
- if (task && task.status !== newStatus) {
- // Persist status change to file and update local state
- persistTaskStatus(activeTaskId, newStatus);
+ const oldStatus = task.status;
+
+ // ============================================
+ // QUEUE SYSTEM: Enforce parallel task limit
+ // ============================================
+ if (newStatus === 'in_progress') {
+ // Get CURRENT state from store directly to avoid stale prop/memo issues during rapid dragging
+ const currentTasks = useTaskStore.getState().tasks;
+ const inProgressCount = currentTasks.filter((t) =>
+ t.status === 'in_progress' && !t.metadata?.archivedAt
+ ).length;
+
+ // If limit reached, move to queue instead (unless already coming from queue)
+ if (inProgressCount >= maxParallelTasks && oldStatus !== 'queue') {
+ console.log(`[Queue] In Progress full (${inProgressCount}/${maxParallelTasks}), moving task to Queue`);
+ newStatus = 'queue';
}
- return;
}
- // Check if dropped on another task - move to that task's column
- const overTask = tasks.find((t) => t.id === overId);
- if (overTask) {
- const task = tasks.find((t) => t.id === activeTaskId);
- if (task && task.status !== overTask.status) {
- // Persist status change to file and update local state
- persistTaskStatus(activeTaskId, overTask.status);
+ // Persist status change to file and update local state
+ await persistTaskStatus(activeTaskId, newStatus);
+
+ // ============================================
+ // QUEUE SYSTEM: Auto-process queue when slot opens
+ // ============================================
+ if (oldStatus === 'in_progress' && newStatus !== 'in_progress') {
+ // A task left In Progress - check if we can promote from queue
+ await processQueue();
+ }
+ };
+
+ /**
+ * Automatically move tasks from Queue to In Progress to fill available capacity
+ * Promotes multiple tasks if needed (e.g., after bulk queue)
+ */
+ const processQueue = async () => {
+ // Loop until capacity is full or queue is empty
+ while (true) {
+ // Get CURRENT state from store to ensure accuracy
+ const currentTasks = useTaskStore.getState().tasks;
+ const inProgressCount = currentTasks.filter((t) =>
+ t.status === 'in_progress' && !t.metadata?.archivedAt
+ ).length;
+ const queuedTasks = currentTasks.filter((t) =>
+ t.status === 'queue' && !t.metadata?.archivedAt
+ );
+
+ // Stop if no capacity or no queued tasks
+ if (inProgressCount >= maxParallelTasks || queuedTasks.length === 0) {
+ break;
}
+
+ // Get the oldest task in queue (FIFO ordering)
+ const nextTask = queuedTasks.sort((a, b) => {
+ const dateA = new Date(a.createdAt).getTime();
+ const dateB = new Date(b.createdAt).getTime();
+ return dateA - dateB; // Ascending order (oldest first)
+ })[0];
+
+ console.log(`[Queue] Auto-promoting task ${nextTask.id} from Queue to In Progress (${inProgressCount + 1}/${maxParallelTasks})`);
+ await persistTaskStatus(nextTask.id, 'in_progress');
}
};
@@ -391,7 +542,10 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
onTaskClick={onTaskClick}
isOver={overColumnId === status}
onAddClick={status === 'backlog' ? onNewTaskClick : undefined}
+ onQueueAll={status === 'backlog' ? handleQueueAll : undefined}
+ onQueueSettings={status === 'queue' ? () => setShowQueueSettings(true) : undefined}
onArchiveAll={status === 'done' ? handleArchiveAll : undefined}
+ maxParallelTasks={status === 'in_progress' ? maxParallelTasks : undefined}
/>
))}
@@ -405,6 +559,15 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
) : null}
+
+ {/* Queue Settings Modal */}
+
);
}
diff --git a/apps/frontend/src/renderer/components/QueueSettingsModal.tsx b/apps/frontend/src/renderer/components/QueueSettingsModal.tsx
new file mode 100644
index 0000000000..75ce2e8466
--- /dev/null
+++ b/apps/frontend/src/renderer/components/QueueSettingsModal.tsx
@@ -0,0 +1,109 @@
+import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle
+} from './ui/dialog';
+import { Button } from './ui/button';
+import { Label } from './ui/label';
+import { Input } from './ui/input';
+
+interface QueueSettingsModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ projectId: string;
+ currentMaxParallel?: number;
+ onSave: (maxParallel: number) => void;
+}
+
+export function QueueSettingsModal({
+ open,
+ onOpenChange,
+ projectId,
+ currentMaxParallel = 3,
+ onSave
+}: QueueSettingsModalProps) {
+ const { t } = useTranslation('tasks');
+ const [maxParallel, setMaxParallel] = useState(currentMaxParallel);
+ const [error, setError] = useState(null);
+
+ // Reset to current value when modal opens
+ useEffect(() => {
+ if (open) {
+ setMaxParallel(currentMaxParallel);
+ setError(null);
+ }
+ }, [open, currentMaxParallel]);
+
+ const handleSave = () => {
+ // Validate the input
+ if (maxParallel < 1) {
+ setError('Must be at least 1');
+ return;
+ }
+ if (maxParallel > 10) {
+ setError('Cannot exceed 10');
+ return;
+ }
+
+ onSave(maxParallel);
+ onOpenChange(false);
+ };
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const value = parseInt(e.target.value, 10);
+ if (!isNaN(value)) {
+ setMaxParallel(value);
+ setError(null);
+ }
+ };
+
+ return (
+
+
+
+ Queue Settings
+
+ Configure the maximum number of tasks that can run in parallel in the "In Progress" board
+
+
+
+
+
+
+ Max Parallel Tasks
+
+
+ {error && (
+
{error}
+ )}
+
+ When this limit is reached, new tasks will wait in the queue before moving to "In Progress"
+
+
+
+
+
+ onOpenChange(false)}>
+ Cancel
+
+
+ Save
+
+
+
+
+ );
+}
diff --git a/apps/frontend/src/renderer/stores/download-store.ts b/apps/frontend/src/renderer/stores/download-store.ts
index 251ec78b8a..c9eb4b331c 100644
--- a/apps/frontend/src/renderer/stores/download-store.ts
+++ b/apps/frontend/src/renderer/stores/download-store.ts
@@ -25,18 +25,6 @@ interface DownloadState {
getActiveDownloads: () => DownloadProgress[];
}
-// Progress tracking state for speed calculation
-// Defined before store so cleanup can be called from store actions
-const progressTracker: Record = {};
-
-/**
- * Clean up progress tracker entry to prevent memory leaks.
- * Called when downloads are cleared.
- */
-function cleanupProgressTracker(modelName: string): void {
- delete progressTracker[modelName];
-}
-
export const useDownloadStore = create((set, get) => ({
downloads: {},
@@ -76,9 +64,6 @@ export const useDownloadStore = create((set, get) => ({
const existing = state.downloads[modelName];
if (!existing) return state;
- // Clean up progress tracker when download completes
- cleanupProgressTracker(modelName);
-
return {
downloads: {
...state.downloads,
@@ -96,9 +81,6 @@ export const useDownloadStore = create((set, get) => ({
const existing = state.downloads[modelName];
if (!existing) return state;
- // Clean up progress tracker when download fails
- cleanupProgressTracker(modelName);
-
return {
downloads: {
...state.downloads,
@@ -113,9 +95,6 @@ export const useDownloadStore = create((set, get) => ({
clearDownload: (modelName: string) =>
set((state) => {
- // Clean up progress tracker to prevent memory leaks
- cleanupProgressTracker(modelName);
-
const { [modelName]: _, ...rest } = state.downloads;
return { downloads: rest };
}),
@@ -135,6 +114,9 @@ export const useDownloadStore = create((set, get) => ({
},
}));
+// Progress tracking state for speed calculation
+const progressTracker: Record = {};
+
/**
* Subscribe to download progress events from the main process.
* Call this once when the app starts.
diff --git a/apps/frontend/src/renderer/stores/task-store.ts b/apps/frontend/src/renderer/stores/task-store.ts
index 43ba85a512..4ab563cebc 100644
--- a/apps/frontend/src/renderer/stores/task-store.ts
+++ b/apps/frontend/src/renderer/stores/task-store.ts
@@ -1,5 +1,6 @@
import { create } from 'zustand';
import type { Task, TaskStatus, ImplementationPlan, Subtask, TaskMetadata, ExecutionProgress, ExecutionPhase, ReviewReason, TaskDraft } from '../../shared/types';
+import { useProjectStore } from './project-store';
interface TaskState {
tasks: Task[];
@@ -226,14 +227,96 @@ export async function createTask(
* Start a task
*/
export function startTask(taskId: string, options?: { parallel?: boolean; workers?: number }): void {
+ const store = useTaskStore.getState();
+ const task = store.tasks.find((t) => t.id === taskId || t.specId === taskId);
+
+ if (!task) {
+ console.error('[startTask] Task not found:', taskId);
+ return;
+ }
+
+ // ============================================
+ // QUEUE SYSTEM: Enforce parallel task limit
+ // ============================================
+ // Get project settings to check maxParallelTasks
+ const projectId = task.projectId;
+ if (projectId) {
+ const projectStore = useProjectStore.getState();
+ const project = projectStore.projects.find((p) => p.id === projectId);
+ const maxParallelTasks = project?.settings.maxParallelTasks ?? 3;
+
+ // Count current in-progress tasks (excluding archived)
+ const inProgressCount = store.tasks.filter((t) =>
+ t.status === 'in_progress' && !t.metadata?.archivedAt
+ ).length;
+
+ // If limit reached, move to queue instead of starting immediately
+ if (inProgressCount >= maxParallelTasks) {
+ console.log(`[Queue] In Progress full (${inProgressCount}/${maxParallelTasks}), moving task to Queue`);
+ // Move to queue - it will auto-promote when a slot opens
+ persistTaskStatus(taskId, 'queue');
+ return;
+ }
+ }
+
window.electronAPI.startTask(taskId, options);
}
/**
- * Stop a task
+ * Stop a task and auto-promote from queue if needed
*/
-export function stopTask(taskId: string): void {
+export async function stopTask(taskId: string): Promise {
+ const store = useTaskStore.getState();
+ const task = store.tasks.find((t) => t.id === taskId || t.specId === taskId);
+
+ // Stop the task
window.electronAPI.stopTask(taskId);
+
+ // If the task was in progress, process queue to fill the now-empty slot
+ if (task && task.status === 'in_progress') {
+ // Get project settings for maxParallelTasks
+ const projectId = task.projectId;
+ if (projectId) {
+ const projectStore = useProjectStore.getState();
+ const project = projectStore.projects.find((p) => p.id === projectId);
+ const maxParallelTasks = project?.settings.maxParallelTasks ?? 3;
+
+ // Wait a bit for the backend to update the task status
+ setTimeout(async () => {
+ await processQueueForProject(projectId, maxParallelTasks);
+ }, 500);
+ }
+ }
+}
+
+/**
+ * Process queue for a specific project - auto-promote tasks to fill capacity
+ */
+async function processQueueForProject(projectId: string, maxParallelTasks: number): Promise {
+ // Loop until capacity is full or queue is empty
+ while (true) {
+ // Get CURRENT state from store
+ const currentTasks = useTaskStore.getState().tasks;
+ const projectTasks = currentTasks.filter((t) => t.projectId === projectId && !t.metadata?.archivedAt);
+
+ const inProgressCount = projectTasks.filter((t) => t.status === 'in_progress').length;
+ const queuedTasks = projectTasks.filter((t) => t.status === 'queue');
+
+ // Stop if no capacity or no queued tasks
+ if (inProgressCount >= maxParallelTasks || queuedTasks.length === 0) {
+ break;
+ }
+
+ // Get the oldest task in queue (FIFO ordering)
+ const nextTask = queuedTasks.sort((a, b) => {
+ const dateA = new Date(a.createdAt).getTime();
+ const dateB = new Date(b.createdAt).getTime();
+ return dateA - dateB;
+ })[0];
+
+ console.log(`[Queue] Auto-promoting task ${nextTask.id} from Queue to In Progress (${inProgressCount + 1}/${maxParallelTasks})`);
+ await persistTaskStatus(nextTask.id, 'in_progress');
+ }
}
/**
diff --git a/apps/frontend/src/renderer/styles/globals.css b/apps/frontend/src/renderer/styles/globals.css
index d8ff00d212..36c8a51b60 100644
--- a/apps/frontend/src/renderer/styles/globals.css
+++ b/apps/frontend/src/renderer/styles/globals.css
@@ -1140,6 +1140,10 @@ body {
border-top-color: var(--muted-foreground);
}
+.column-queue {
+ border-top-color: var(--info);
+}
+
.column-in-progress {
border-top-color: var(--info);
}
diff --git a/apps/frontend/src/shared/constants/task.ts b/apps/frontend/src/shared/constants/task.ts
index 88367c9ca4..439c9d9514 100644
--- a/apps/frontend/src/shared/constants/task.ts
+++ b/apps/frontend/src/shared/constants/task.ts
@@ -10,6 +10,7 @@
// Task status columns in Kanban board order
export const TASK_STATUS_COLUMNS = [
'backlog',
+ 'queue',
'in_progress',
'ai_review',
'human_review',
@@ -19,6 +20,7 @@ export const TASK_STATUS_COLUMNS = [
// Human-readable status labels
export const TASK_STATUS_LABELS: Record = {
backlog: 'Planning',
+ queue: 'Queue',
in_progress: 'In Progress',
ai_review: 'AI Review',
human_review: 'Human Review',
@@ -28,6 +30,7 @@ export const TASK_STATUS_LABELS: Record = {
// Status colors for UI
export const TASK_STATUS_COLORS: Record = {
backlog: 'bg-muted text-muted-foreground',
+ queue: 'bg-cyan-500/10 text-cyan-400',
in_progress: 'bg-info/10 text-info',
ai_review: 'bg-warning/10 text-warning',
human_review: 'bg-purple-500/10 text-purple-400',
diff --git a/apps/frontend/src/shared/i18n/locales/en/navigation.json b/apps/frontend/src/shared/i18n/locales/en/navigation.json
index c08e66f2ba..cc3d21180a 100644
--- a/apps/frontend/src/shared/i18n/locales/en/navigation.json
+++ b/apps/frontend/src/shared/i18n/locales/en/navigation.json
@@ -14,9 +14,9 @@
"githubIssues": "GitHub Issues",
"githubPRs": "GitHub PRs",
"gitlabIssues": "GitLab Issues",
- "gitlabMRs": "GitLab MRs",
"worktrees": "Worktrees",
- "agentTools": "MCP Overview"
+ "agentTools": "MCP Overview",
+ "gitlabMRs": "GitLab MRs"
},
"actions": {
"settings": "Settings",
diff --git a/apps/frontend/src/shared/i18n/locales/en/tasks.json b/apps/frontend/src/shared/i18n/locales/en/tasks.json
index 8602b8a2e7..64fe2a497b 100644
--- a/apps/frontend/src/shared/i18n/locales/en/tasks.json
+++ b/apps/frontend/src/shared/i18n/locales/en/tasks.json
@@ -35,7 +35,9 @@
},
"tooltips": {
"archiveTask": "Archive task",
- "archiveAllDone": "Archive all done tasks"
+ "archiveAllDone": "Archive all done tasks",
+ "addTask": "Add new task",
+ "queueAll": "Move all tasks to Queue"
},
"creation": {
"title": "Create New Task",
@@ -49,6 +51,8 @@
"kanban": {
"emptyBacklog": "No tasks planned",
"emptyBacklogHint": "Add a task to get started",
+ "emptyQueue": "Queue is empty",
+ "emptyQueueHint": "Tasks wait here before starting",
"emptyInProgress": "Nothing running",
"emptyInProgressHint": "Start a task from Backlog",
"emptyAiReview": "No tasks in review",
@@ -83,6 +87,18 @@
"qa": "QA"
}
},
+ "queue": {
+ "limitReached": "Task queued. Maximum parallel tasks limit reached ({{count}}). Will start automatically when a slot opens.",
+ "movedToQueue": "Task moved to Queue ({{current}}/{{max}} tasks running)",
+ "autoPromoted": "Task automatically promoted from Queue to In Progress",
+ "capacityAvailable": "{{available}} slot(s) available",
+ "settings": {
+ "title": "Queue Settings",
+ "maxParallel": "Maximum Parallel Tasks",
+ "description": "Set the maximum number of tasks that can run in parallel",
+ "save": "Save Settings"
+ }
+ },
"files": {
"title": "Files",
"tab": "Files",
diff --git a/apps/frontend/src/shared/i18n/locales/fr/navigation.json b/apps/frontend/src/shared/i18n/locales/fr/navigation.json
index e64bf7b982..2ca6061d12 100644
--- a/apps/frontend/src/shared/i18n/locales/fr/navigation.json
+++ b/apps/frontend/src/shared/i18n/locales/fr/navigation.json
@@ -14,9 +14,9 @@
"githubIssues": "Issues GitHub",
"githubPRs": "PRs GitHub",
"gitlabIssues": "Issues GitLab",
- "gitlabMRs": "MRs GitLab",
"worktrees": "Worktrees",
- "agentTools": "Aperçu MCP"
+ "agentTools": "Aperçu MCP",
+ "gitlabMRs": "MRs GitLab"
},
"actions": {
"settings": "Paramètres",
diff --git a/apps/frontend/src/shared/i18n/locales/fr/tasks.json b/apps/frontend/src/shared/i18n/locales/fr/tasks.json
index ea3e5b38fd..3014bbc713 100644
--- a/apps/frontend/src/shared/i18n/locales/fr/tasks.json
+++ b/apps/frontend/src/shared/i18n/locales/fr/tasks.json
@@ -35,7 +35,9 @@
},
"tooltips": {
"archiveTask": "Archiver la tâche",
- "archiveAllDone": "Archiver toutes les tâches terminées"
+ "archiveAllDone": "Archiver toutes les tâches terminées",
+ "addTask": "Ajouter une nouvelle tâche",
+ "queueAll": "Déplacer toutes les tâches vers la file d'attente"
},
"creation": {
"title": "Créer une nouvelle tâche",
@@ -49,6 +51,8 @@
"kanban": {
"emptyBacklog": "Aucune tâche planifiée",
"emptyBacklogHint": "Ajoutez une tâche pour commencer",
+ "emptyQueue": "File d'attente vide",
+ "emptyQueueHint": "Les tâches attendent ici avant de démarrer",
"emptyInProgress": "Rien en cours",
"emptyInProgressHint": "Démarrez une tâche depuis le Backlog",
"emptyAiReview": "Aucune tâche en révision",
@@ -83,6 +87,18 @@
"qa": "QA"
}
},
+ "queue": {
+ "limitReached": "Tâche mise en file d'attente. Limite de tâches parallèles atteinte ({{count}}). Démarrera automatiquement quand un emplacement se libère.",
+ "movedToQueue": "Tâche déplacée vers la file d'attente ({{current}}/{{max}} tâches en cours)",
+ "autoPromoted": "Tâche automatiquement promue de la file d'attente vers En cours",
+ "capacityAvailable": "{{available}} emplacement(s) disponible(s)",
+ "settings": {
+ "title": "Paramètres de la file d'attente",
+ "maxParallel": "Tâches parallèles maximales",
+ "description": "Définir le nombre maximum de tâches pouvant s'exécuter en parallèle",
+ "save": "Enregistrer les paramètres"
+ }
+ },
"files": {
"title": "Fichiers",
"tab": "Fichiers",
diff --git a/apps/frontend/src/shared/types/project.ts b/apps/frontend/src/shared/types/project.ts
index fd34224be2..201239dd71 100644
--- a/apps/frontend/src/shared/types/project.ts
+++ b/apps/frontend/src/shared/types/project.ts
@@ -26,6 +26,8 @@ export interface ProjectSettings {
mainBranch?: string;
/** Include CLAUDE.md instructions in agent system prompt (default: true) */
useClaudeMd?: boolean;
+ /** Maximum number of tasks that can run in parallel in "In Progress" (default: 3) */
+ maxParallelTasks?: number;
}
export interface NotificationSettings {
diff --git a/apps/frontend/src/shared/types/task.ts b/apps/frontend/src/shared/types/task.ts
index 833516bc7a..36d20b8442 100644
--- a/apps/frontend/src/shared/types/task.ts
+++ b/apps/frontend/src/shared/types/task.ts
@@ -5,7 +5,7 @@
import type { ThinkingLevel, PhaseModelConfig, PhaseThinkingConfig } from './settings';
import type { ExecutionPhase as ExecutionPhaseType } from '../constants/phase-protocol';
-export type TaskStatus = 'backlog' | 'in_progress' | 'ai_review' | 'human_review' | 'done';
+export type TaskStatus = 'backlog' | 'queue' | 'in_progress' | 'ai_review' | 'human_review' | 'done';
// Reason why a task is in human_review status
// - 'completed': All subtasks done and QA passed, ready for final approval/merge