diff --git a/.changeset/fn-7153-stop-engine-label.md b/.changeset/fn-7153-stop-engine-label.md new file mode 100644 index 0000000000..813e9e0e77 --- /dev/null +++ b/.changeset/fn-7153-stop-engine-label.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Project Dashboard cards now say "Stop engine"/"Start engine" instead of "Pause"/"Resume". +category: feature +dev: Relabels ProjectCard pause/resume controls; pauseProject already stops the engine, so behavior is unchanged. i18n projectCard.* keys updated (en) with empty-string fallback for other locales. diff --git a/packages/dashboard/app/components/ProjectCard.tsx b/packages/dashboard/app/components/ProjectCard.tsx index 9f9b821eb2..8727f6015a 100644 --- a/packages/dashboard/app/components/ProjectCard.tsx +++ b/packages/dashboard/app/components/ProjectCard.tsx @@ -1,7 +1,7 @@ import { memo, useCallback, useState } from "react"; import type { TFunction } from "i18next"; import { useTranslation } from "react-i18next"; -import { Play, Pause, Trash2, Folder, ArrowRight } from "lucide-react"; +import { Play, Square, Trash2, Folder, ArrowRight } from "lucide-react"; import "./ProjectCard.css"; import type { RegisteredProject, ProjectHealth } from "@fusion/core"; import type { ProjectNodeAvailability } from "../api"; @@ -219,27 +219,31 @@ function ProjectCardInner({
+ {/* + * FNXC:ProjectCardEngineControls 2026-06-27-00:00: + * ProjectEngineManager.pauseProject already calls engine.stop(), so the card action names the engine-lifecycle action as Stop engine/Start engine while preserving the active/paused project status model and pauseProject/resumeProject wiring. + */} {isPaused ? ( ) : ( )} diff --git a/packages/dashboard/app/components/__tests__/ProjectCard.test.tsx b/packages/dashboard/app/components/__tests__/ProjectCard.test.tsx index 32654a3e09..9d17703852 100644 --- a/packages/dashboard/app/components/__tests__/ProjectCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/ProjectCard.test.tsx @@ -6,7 +6,8 @@ import type { RegisteredProject, ProjectHealth, ProjectStatus } from "@fusion/co // Mock lucide-react to avoid SVG rendering issues in test env vi.mock("lucide-react", () => ({ Play: () => ▶, - Pause: () => ⏸, + Pause: () => ⏸, + Square: () => ■, AlertCircle: () => ⚠, Loader2: () => ⟳, MoreHorizontal: () => null, @@ -366,7 +367,7 @@ describe("ProjectCard", () => { expect(onSelect).toHaveBeenCalledWith(project); }); - it("calls onPause when pause button is clicked", () => { + it("calls onPause when stop-engine button is clicked", () => { const onPause = vi.fn(); const project = makeProject({ status: "active" }); @@ -381,11 +382,11 @@ describe("ProjectCard", () => { /> ); - fireEvent.click(screen.getByLabelText("Pause project")); + fireEvent.click(screen.getByLabelText("Stop engine")); expect(onPause).toHaveBeenCalledWith(project); }); - it("calls onResume when resume button is clicked", () => { + it("calls onResume when start-engine button is clicked", () => { const onResume = vi.fn(); const project = makeProject({ status: "paused" }); @@ -400,10 +401,88 @@ describe("ProjectCard", () => { /> ); - fireEvent.click(screen.getByLabelText("Resume project")); + fireEvent.click(screen.getByLabelText("Start engine")); expect(onResume).toHaveBeenCalledWith(project); }); + it("labels engine lifecycle controls across active, errored, paused, and initializing states", () => { + const activePause = vi.fn(); + const activeProject = makeProject({ status: "active" }); + const activeRender = render( + + ); + + const activeStop = screen.getByRole("button", { name: "Stop engine" }); + expect(activeStop).toHaveAttribute("title", "Stop engine"); + expect(screen.getByTestId("stop-icon")).toBeDefined(); + const stalePauseLabel = ["Pause", "project"].join(" "); + const staleResumeLabel = ["Resume", "project"].join(" "); + expect(screen.queryByLabelText(stalePauseLabel)).toBeNull(); + expect(screen.queryByLabelText(staleResumeLabel)).toBeNull(); + fireEvent.click(activeStop); + expect(activePause).toHaveBeenCalledWith(activeProject); + activeRender.unmount(); + + const erroredPause = vi.fn(); + const erroredProject = makeProject({ status: "errored" }); + const erroredRender = render( + + ); + + fireEvent.click(screen.getByRole("button", { name: "Stop engine" })); + expect(erroredPause).toHaveBeenCalledWith(erroredProject); + erroredRender.unmount(); + + const pausedResume = vi.fn(); + const pausedProject = makeProject({ status: "paused" }); + const pausedRender = render( + + ); + + const pausedStart = screen.getByRole("button", { name: "Start engine" }); + expect(pausedStart).toHaveAttribute("title", "Start engine"); + expect(screen.getByTestId("play-icon")).toBeDefined(); + fireEvent.click(pausedStart); + expect(pausedResume).toHaveBeenCalledWith(pausedProject); + pausedRender.unmount(); + + render( + + ); + + const initializingStop = screen.getByRole("button", { name: "Stop engine" }); + expect(initializingStop).toBeDisabled(); + expect(initializingStop).toHaveAttribute("title", "Cannot stop engine while initializing"); + }); + it("calls onRemove when remove button is clicked", () => { const onRemove = vi.fn(); const project = makeProject(); @@ -454,7 +533,7 @@ describe("ProjectCard", () => { expect(container.querySelector(".is-armed")).not.toBeNull(); }); - it("disables pause button when initializing", () => { + it("disables stop-engine button when initializing", () => { const { container } = render( { /> ); - // Find the pause button by its title attribute - const pauseButton = container.querySelector('button[title="Cannot pause while initializing"]'); - expect(pauseButton).not.toBeNull(); - expect(pauseButton).toBeDisabled(); + // Find the stop-engine button by its title attribute + const stopEngineButton = container.querySelector('button[title="Cannot stop engine while initializing"]'); + expect(stopEngineButton).not.toBeNull(); + expect(stopEngineButton).toBeDisabled(); }); it("disables all buttons when isLoading is true", () => { @@ -485,7 +564,7 @@ describe("ProjectCard", () => { /> ); - expect(screen.getByLabelText("Pause project")).toBeDisabled(); + expect(screen.getByLabelText("Stop engine")).toBeDisabled(); expect(screen.getByLabelText("Open project")).toBeDisabled(); expect(screen.getByLabelText("Remove project")).toBeDisabled(); }); @@ -536,7 +615,7 @@ describe("ProjectCard", () => { /> ); - fireEvent.click(screen.getByLabelText("Pause project")); + fireEvent.click(screen.getByLabelText("Stop engine")); expect(onPause).toHaveBeenCalled(); expect(onSelect).not.toHaveBeenCalled(); }); diff --git a/packages/dashboard/app/components/__tests__/ProjectThemeTokens.test.tsx b/packages/dashboard/app/components/__tests__/ProjectThemeTokens.test.tsx index 5a7e9282d0..0c3020c98a 100644 --- a/packages/dashboard/app/components/__tests__/ProjectThemeTokens.test.tsx +++ b/packages/dashboard/app/components/__tests__/ProjectThemeTokens.test.tsx @@ -23,6 +23,7 @@ import type { ProjectInfo } from "../../../app/api"; vi.mock("lucide-react", () => ({ Play: () => Play, Pause: () => Pause, + Square: () => Square, AlertCircle: () => AlertCircle, Loader2: () => Loader2, ChevronDown: () => ChevronDown, diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index 22e04d6dc3..0689d6cba8 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -4837,7 +4837,7 @@ "projectCard": { "activeTasks": "Active Tasks", "agents": "Agents", - "cannotPauseWhileInitializing": "Cannot pause while initializing", + "cannotPauseWhileInitializing": "Cannot stop engine while initializing", "completed": "Completed", "confirm": "Confirm", "confirmRemove": "Confirm remove", @@ -4857,11 +4857,11 @@ "noHealthData": "No health data available", "open": "Open", "openProject": "Open project", - "pause": "Pause", - "pauseProject": "Pause project", + "pause": "Stop engine", + "pauseProject": "Stop engine", "removeProject": "Remove project", - "resume": "Resume", - "resumeProject": "Resume project" + "resume": "Start engine", + "resumeProject": "Start engine" }, "projectDetection": { "editName": "Edit name", diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index f38c880d96..e50126c125 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -4827,7 +4827,7 @@ "projectCard": { "activeTasks": "Tareas activas", "agents": "Agentes", - "cannotPauseWhileInitializing": "No se puede pausar mientras se inicializa", + "cannotPauseWhileInitializing": "", "completed": "Completado", "confirm": "Confirmar", "confirmRemove": "Confirmar eliminación", @@ -4847,11 +4847,11 @@ "noHealthData": "No hay datos de salud disponibles", "open": "Abrir", "openProject": "Abrir proyecto", - "pause": "Pausar", - "pauseProject": "Pausar proyecto", + "pause": "", + "pauseProject": "", "removeProject": "Eliminar proyecto", - "resume": "Reanudar", - "resumeProject": "Reanudar proyecto" + "resume": "", + "resumeProject": "" }, "projectDetection": { "editName": "Editar nombre", diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index 42500483d6..045d62938e 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -4827,7 +4827,7 @@ "projectCard": { "activeTasks": "Tâches actives", "agents": "Agents", - "cannotPauseWhileInitializing": "Impossible de mettre en pause lors de l'initialisation", + "cannotPauseWhileInitializing": "", "completed": "Complétée", "confirm": "Confirmer", "confirmRemove": "Confirmer la suppression", @@ -4847,11 +4847,11 @@ "noHealthData": "Aucune donnée de santé disponible", "open": "Ouvrir", "openProject": "Ouvrir le projet", - "pause": "Pause", - "pauseProject": "Mettre le projet en pause", + "pause": "", + "pauseProject": "", "removeProject": "Supprimer le projet", - "resume": "Reprendre", - "resumeProject": "Reprendre le projet" + "resume": "", + "resumeProject": "" }, "projectDetection": { "editName": "Modifier le nom", diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json index 4716e64a03..74ee548a08 100644 --- a/packages/i18n/locales/ko/app.json +++ b/packages/i18n/locales/ko/app.json @@ -4827,7 +4827,7 @@ "projectCard": { "activeTasks": "활성 작업", "agents": "에이전트", - "cannotPauseWhileInitializing": "초기화 중에는 일시 중지할 수 없습니다", + "cannotPauseWhileInitializing": "", "completed": "완료됨", "confirm": "확인", "confirmRemove": "제거 확인", @@ -4847,11 +4847,11 @@ "noHealthData": "사용 가능한 상태 데이터가 없습니다", "open": "열기", "openProject": "프로젝트 열기", - "pause": "일시 중지", - "pauseProject": "프로젝트 일시 중지", + "pause": "", + "pauseProject": "", "removeProject": "프로젝트 제거", - "resume": "재개", - "resumeProject": "프로젝트 재개" + "resume": "", + "resumeProject": "" }, "projectDetection": { "editName": "이름 편집", diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index 8863a214a9..da9572f82a 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -4827,7 +4827,7 @@ "projectCard": { "activeTasks": "活跃任务", "agents": "代理", - "cannotPauseWhileInitializing": "初始化时无法暂停", + "cannotPauseWhileInitializing": "", "completed": "已完成", "confirm": "确认", "confirmRemove": "确认删除", @@ -4847,11 +4847,11 @@ "noHealthData": "没有可用的健康数据", "open": "打开", "openProject": "打开项目", - "pause": "暂停", - "pauseProject": "暂停项目", + "pause": "", + "pauseProject": "", "removeProject": "删除项目", - "resume": "恢复", - "resumeProject": "恢复项目" + "resume": "", + "resumeProject": "" }, "projectDetection": { "editName": "编辑名称", diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index 5fd4e292ca..08f443131d 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -4827,7 +4827,7 @@ "projectCard": { "activeTasks": "活躍任務", "agents": "代理", - "cannotPauseWhileInitializing": "初始化時無法暫停", + "cannotPauseWhileInitializing": "", "completed": "已完成", "confirm": "確認", "confirmRemove": "確認移除", @@ -4847,11 +4847,11 @@ "noHealthData": "沒有可用的健康資料", "open": "打開", "openProject": "打開專案", - "pause": "暫停", - "pauseProject": "暫停專案", + "pause": "", + "pauseProject": "", "removeProject": "移除專案", - "resume": "恢復", - "resumeProject": "恢復專案" + "resume": "", + "resumeProject": "" }, "projectDetection": { "editName": "編輯名稱", diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts index 79127d10fd..09ebb40b05 100644 --- a/packages/i18n/src/resources.d.ts +++ b/packages/i18n/src/resources.d.ts @@ -1230,6 +1230,7 @@ export default interface Resources { "cancelButton": "Cancel", "clearConversationFailed": "Failed to clear conversation", "closeQuickChat": "Close quick chat", + "contextWindowAria": "Estimated {{used}} of {{total}} context tokens", "conversationArchived": "Conversation archived", "conversationDeleted": "Conversation deleted", "conversationName": "Conversation name", @@ -1533,6 +1534,8 @@ export default interface Resources { "maxConcurrent": "Max concurrent tasks", "maxTriageConcurrent": "Max triage concurrent", "maxWorktrees": "Max worktrees", + "runningGlobal": "{{count}} running (all projects)", + "runningProject": "{{count}} running (this project)", "title": "Concurrency" }, "engine": { @@ -1761,7 +1764,8 @@ export default interface Resources { "system": "System", "team": "Team", "tokens": "Tokens", - "tools": "Tools" + "tools": "Tools", + "workflows": "Workflows" }, "team": { "agent": "Agent", @@ -1819,6 +1823,26 @@ export default interface Resources { "sessions": "Sessions", "summaryTitle": "Summary", "toolCalls": "Tool calls" + }, + "workflows": { + "completedByWorkflow": "Tasks done by workflow", + "cost": "Cost", + "done": "Tasks done", + "empty": "No workflow analytics have been recorded for this range yet.", + "files": "Files changed", + "filesChanged": "Files changed", + "inProgress": "In progress", + "inReview": "In review", + "noChartData": "No non-zero values for this chart yet.", + "tableTitle": "Per-workflow breakdown", + "tasksCompleted": "Tasks done", + "tokens": "Tokens", + "tokensByWorkflow": "Tokens by workflow", + "totalCost": "Estimated cost", + "totalTokens": "Total tokens", + "totalsTitle": "Workflow totals", + "unknownWorkflow": "(unknown workflow)", + "workflow": "Workflow" } }, "comments": { @@ -2318,6 +2342,7 @@ export default interface Resources { "daysAgo_one": "{{count}}d ago", "daysAgo_other": "{{count}}d ago", "engineControls": "Engine controls", + "engineControlsClose": "Close engine controls", "escalated": "Escalated", "escalatedSuffix": " (escalated)", "hideProjectDir": "Hide project directory", @@ -4814,7 +4839,7 @@ export default interface Resources { "projectCard": { "activeTasks": "Active Tasks", "agents": "Agents", - "cannotPauseWhileInitializing": "Cannot pause while initializing", + "cannotPauseWhileInitializing": "Cannot stop engine while initializing", "completed": "Completed", "confirm": "Confirm", "confirmRemove": "Confirm remove", @@ -4834,11 +4859,11 @@ export default interface Resources { "nodeAvailability": "Project node availability", "open": "Open", "openProject": "Open project", - "pause": "Pause", - "pauseProject": "Pause project", + "pause": "Stop engine", + "pauseProject": "Stop engine", "removeProject": "Remove project", - "resume": "Resume", - "resumeProject": "Resume project" + "resume": "Start engine", + "resumeProject": "Start engine" }, "projectDetection": { "editName": "Edit name", @@ -5936,6 +5961,12 @@ export default interface Resources { "keepLocal": "Keep Local", "keepRemote": "Keep Remote", "loading": "Loading…", + "mcp": { + "globalDescription": "Configure MCP servers shared by all projects. Project settings may override or disable these servers by name.", + "globalTitle": "Global MCP servers", + "projectDescription": "Configure project-specific MCP servers, overrides, and disabled inherited servers.", + "projectTitle": "Project MCP servers" + }, "memory": { "03": "0 3 * * *", "agentsGetMemorySearchMemoryGetAndMemory": "Agents get memory_search, memory_get, and memory_append tools. Search defaults to qmd with a local file fallback.", @@ -6070,12 +6101,12 @@ export default interface Resources { "origin": "origin", "personalAccessToken": "Personal access token", "pickALocalBranchFromTheDropdownCommon": "). Pick a local branch from the dropdown — common integration names like ", - "positiveIntegerRetryCapForAutoMergeConflict": "Positive integer retry cap for auto-merge conflict resolution before a task parks for human recovery. Default 3.", "planApprovalMode": "Plan approval mode", "planApprovalModeAutoApproveAll": "Auto-approve all tasks", "planApprovalModeHelp": "Project-wide override for the planning approval gate. Leave on workflow to use each workflow's Require plan approval setting, or force all approved specs to bypass or wait for manual approval.", "planApprovalModeRequireAll": "Require approval for all tasks", "planApprovalModeWorkflow": "Use workflow setting", + "positiveIntegerRetryCapForAutoMergeConflict": "Positive integer retry cap for auto-merge conflict resolution before a task parks for human recovery. Default 3.", "postMergeAuditMode": "Post-merge audit mode", "pushRemote": "Push Remote", "pushToRemoteAfterMerge": " Push to remote after merge ", @@ -6143,6 +6174,8 @@ export default interface Resources { "global": "Global setting", "project": "Project setting" }, + "globalMcp": "MCP Servers", + "mcp": "MCP Servers", "prompts": "Prompts", "secrets": "Secrets", "tooltip": { @@ -7567,6 +7600,32 @@ export default interface Resources { "summary": { "heading": "Summary" }, + "summaryTab": { + "agentWorkHeading": "Work done by agents", + "cachedTokens": "Cached", + "changedHeading": "What changed", + "commit": "Commit", + "completedSteps": "Completed steps", + "completionHeading": "Completion summary", + "cost": "Cost", + "costUnavailable": "No pricing for this model", + "deletions": "Removed", + "filesChanged": "Files", + "inputTokens": "Input", + "insertions": "Added", + "model": "Model", + "noAgentWork": "No completed steps or workflow results are available for this task.", + "noChangedFiles": "No changed-file list is available for this task.", + "noCompletionSummary": "No completion summary was recorded for this task.", + "noTokenUsage": "No token usage recorded for this task yet.", + "outputTokens": "Output", + "retries": "Agents retried this task {{count}} time{{plural}}.", + "tokenCostHeading": "Token usage & cost", + "totalCost": "Total cost", + "totalTokens": "Total", + "unknownModel": "(unknown)", + "workflowResults": "Workflow results" + }, "tabs": { "changes": "Changes", "chat": "Chat", @@ -7579,6 +7638,7 @@ export default interface Resources { "review": "Review", "routing": "Routing", "stats": "Stats", + "summary": "Summary", "terminal": "Terminal", "workflow": "Workflow" },