FN-7842: color-code priority icons by urgency across quick add, task form, and task cards

Adds a shared urgency color source to priorityIndicator and wires it into every priority-glyph surface so low/normal/high/urgent read consistently by color, not just icon shape.

- priorityIndicator.tsx: add colorVar (low=info/blue, normal=muted, high=warning/amber, urgent=error/red) and new getPriorityColorVar() export as the single source of truth
- QuickEntryBox: tint the icon-only priority trigger and each option row in the priority picker with the matching urgency color
- TaskForm: tint the New Task inline priority glyph using the same color source
- TaskCard: render a colored priority glyph alongside the existing text label in the card-priority-badge (kept the non-normal visibility gate and badge geometry via a small CSS gap addition)
- Updated/added tests for QuickEntryBox, TaskCard (badge, badge-height, badge-wrap), and priorityIndicator to assert the new colors; added a patch changeset and a dashboard-guide.md doc update

Files changed:
 .changeset/fn-7842-priority-color-coding.md        |  7 +++++
 docs/dashboard-guide.md                            |  7 +++--
 .../dashboard/app/components/QuickEntryBox.tsx     |  7 +++--
 packages/dashboard/app/components/TaskCard.css     |  2 ++
 packages/dashboard/app/components/TaskCard.tsx     |  6 +++-
 packages/dashboard/app/components/TaskForm.tsx     |  7 +++--
 .../components/__tests__/QuickEntryBox.test.tsx    | 17 +++++++++--
 .../__tests__/TaskCard.badge-height.test.tsx       |  5 ++++
 .../__tests__/TaskCard.badge-wrap.test.tsx         |  5 ++++
 .../app/components/__tests__/TaskCard.test.tsx     | 33 +++++++++++++++++++++-
 .../app/utils/__tests__/priorityIndicator.test.tsx | 13 +++++----
 packages/dashboard/app/utils/priorityIndicator.tsx | 16 ++++++++---
 12 files changed, 103 insertions(+), 22 deletions(-)

Fusion-Task-Id: FN-7842

Fusion-Task-Lineage: 0b2d0d15-6e61-45fc-9378-bc09002bef55

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-11 21:54:21 -07:00
parent 3da3f377c9
commit c7c6c5a8c4
12 changed files with 103 additions and 22 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Priority selection in quick add and task cards is now color-coded by urgency (blue low, amber high, red urgent).
category: feature
dev: priorityIndicator gains a getPriorityColorVar single source consumed by QuickEntryBox, TaskForm inline row, and TaskCard's .card-priority-badge; semantic tokens only, no test-id/payload changes.

View File

@@ -196,7 +196,7 @@ Features:
- PR/issue badges with live updates
- GitLab tracking badges on task cards for linked GitLab project issues, group issues, and merge requests; stale GitLab metadata uses a warning-colored badge while GitHub badges remain unchanged.
- GitHub provenance marker on task cards imported from GitHub (`sourceType: github_import`), shown in the footer with other external-source metadata
- Task card header meta badges group priority and fast mode in the header; agent-created provenance renders in a dedicated bottom-left row ahead of workflow identity so the ID/status/actions header does not wrap on narrow cards. Agent labels prefer `sourceMetadata.agentName` over raw agent IDs.
- Task card header meta badges group priority and fast mode in the header; priority badges include the shared urgency glyph/color language (low blue/info, high amber/warning, urgent red/error) while agent-created provenance renders in a dedicated bottom-left row ahead of workflow identity so the ID/status/actions header does not wrap on narrow cards. Agent labels prefer `sourceMetadata.agentName` over raw agent IDs.
- **Settings → Appearance → Show cost badges on task cards** is default off. When enabled, board cards with recorded positive token usage show a compact derived-cost badge beside the execution-time badge; unpriced models display `—`, and cards with no usage render no badge shell.
<!-- FNXC:TaskCardCostBadge 2026-07-11-12:25: The card spend badge is opt-in because card footers are dense. It must remain guess-free (unpriced `—`, no fabricated `$0`) and absent for tasks without positive token usage. -->
<!-- FNXC:TaskCardLayout 2026-07-10-00:00: FN-7780 moved agent-created provenance out of the header meta-badge cluster into a bottom row. Keep dashboard docs aligned so operators do not expect the agent chip to participate in header wrapping. -->
@@ -471,8 +471,9 @@ Create requests never send an explicit `column`. The task store resolves the lan
Optional workflow steps declared by the active workflow are available from the quick-add action row and the **New Task** dialog's inline quick buttons. For example, the coding workflow's browser verification option appears as a quick drop-down when that workflow is active; each option is seeded from the workflow step's `defaultOn` setting and is sent with the task's `enabledWorkflowSteps` payload at creation time.
<!-- FNXC:QuickAddAttachments 2026-06-30-00:00: Quick Add attachments use a compact icon-only paperclip while keeping the Attach action and pending image count in accessible labels. The same pending preview/upload path accepts image selection, paste, and direct drag/drop onto the Quick Add box. -->
<!-- FNXC:QuickAddPriorityIndicator 2026-07-10-21:45: Quick Add keeps status controls in the bottom action cluster: GitHub tracking sits beside the paperclip attach button, Priority is icon-only with low/down, normal/flag, high/up, urgent/alert glyphs, and Fast is an icon-only lightning button. -->
Quick Add image attachments use the paperclip icon button in the action row. Supported image files (`png`, `jpeg`, `gif`, `webp`) can be selected from that control, pasted into the Quick Add input, or dragged onto the Quick Add box; all three paths show pending previews before task creation and upload the images to the created task afterward. The same bottom action row places the GitHub tracking override beside the paperclip; Priority is an icon-only control whose glyph changes by selected level (down arrow for low, flag for normal, up arrow for high, alert for urgent), and Fast is an icon-only lightning control. These icon-only controls keep accessible labels and the same create-payload behavior as the previous text chips.
<!-- FNXC:QuickAddPriorityIndicator 2026-07-10-21:45: Quick Add keeps status controls in the bottom action cluster: GitHub tracking sits beside the paperclip attach button, Priority is icon-only with low/down, normal/flag, high/up, urgent/alert glyphs, and Fast is an icon-only lightning button.
FNXC:PriorityColorCoding 2026-07-11-00:00: Priority glyphs share urgency colors across Quick Add, the New Task inline row, and task-card badges: low=info/blue, normal=muted, high=warning/amber, urgent=error/red. -->
Quick Add image attachments use the paperclip icon button in the action row. Supported image files (`png`, `jpeg`, `gif`, `webp`) can be selected from that control, pasted into the Quick Add input, or dragged onto the Quick Add box; all three paths show pending previews before task creation and upload the images to the created task afterward. The same bottom action row places the GitHub tracking override beside the paperclip; Priority is an icon-only control whose glyph changes by selected level (down arrow for low, flag for normal, up arrow for high, alert for urgent) and is color-coded by urgency (low blue/info, normal muted, high amber/warning, urgent red/error), and Fast is an icon-only lightning control. These icon-only controls keep accessible labels and the same create-payload behavior as the previous text chips.
Quick entry, inline quick-create, and the full **New Task** dialog all check for similar active tasks before creating. When possible duplicates exist, the warning lists each match by task description (falling back to title, then “No description”) and lets you open an existing task, cancel, or create anyway with the duplicates acknowledged.

View File

@@ -17,7 +17,7 @@ import { NodeHealthDot } from "./NodeHealthDot";
import { ProviderIcon } from "./ProviderIcon";
import { WorkflowOptionalStepsDropdown } from "./WorkflowOptionalStepsDropdown";
import { WorkflowIcon } from "./WorkflowIcon";
import { getPriorityIcon, getPriorityLabel } from "../utils/priorityIndicator";
import { getPriorityColorVar, getPriorityIcon, getPriorityLabel } from "../utils/priorityIndicator";
const STORAGE_KEY = "kb-quick-entry-text";
const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
@@ -2225,7 +2225,8 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
});
}}
>
<PriorityIcon size={12} aria-hidden="true" />
{/* FNXC:PriorityColorCoding 2026-07-11-00:00: The quick-add icon-only priority trigger must preview urgency color from priorityIndicator without changing its label, test id, or picker behavior. */}
<PriorityIcon size={12} aria-hidden="true" style={{ color: getPriorityColorVar(priority) }} />
</button>
</div>
@@ -2246,6 +2247,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<div className="dep-dropdown-search-header">{t("tasks.selectPriority", "Select priority")}</div>
{TASK_PRIORITIES.map((taskPriority) => {
const label = getPriorityLabel(taskPriority);
const OptionPriorityIcon = getPriorityIcon(taskPriority);
return (
<div
key={taskPriority}
@@ -2258,6 +2260,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
setPriorityPickerPosition(null);
}}
>
<OptionPriorityIcon size={12} aria-hidden="true" style={{ color: getPriorityColorVar(taskPriority) }} />
<span className="dep-dropdown-title">{label}</span>
</div>
);

View File

@@ -512,6 +512,8 @@ FN-6774 removes the saturated stuck-task edge stripe from board cards. Keep the
}
.card-priority-badge {
/* FNXC:PriorityColorCoding 2026-07-11-00:00: Priority badges add a glyph beside the label; keep inline-flex geometry and token spacing so badge height/wrap invariants do not change. */
gap: var(--space-xs);
letter-spacing: 0.4px;
}

View File

@@ -54,6 +54,7 @@ import { WorkspaceWorktreesSummary, isWorkspaceTask } from "./WorkspaceWorktrees
import { WorkflowIcon } from "./WorkflowIcon";
import { TaskContextMenu, buildTaskActionMenuModel, getTaskPrAutomationLabel, type TaskContextMenuColumnFlags, type TaskContextMenuColumnMetadata, type TaskMenuActionDescriptor } from "./TaskContextMenu";
import { formatCost, hasTaskCost, taskTotalCost } from "../utils/taskTokenCost";
import { getPriorityColorVar, getPriorityIcon } from "../utils/priorityIndicator";
/** Per-branch progress snapshot (U13). Surfaced as an optional additive field
* on the task payload for the parallel-window badge (U9). */
@@ -1284,6 +1285,7 @@ function TaskCardComponent({
const pausedByAgent = Boolean(!isDoneColumn && task.paused && task.pausedByAgentId);
const normalizedPriority = normalizeTaskPriorityValue(task.priority);
const showPriorityBadge = normalizedPriority !== DEFAULT_TASK_PRIORITY;
const PriorityBadgeIcon = getPriorityIcon(normalizedPriority);
const isStuck = isTaskStuck(task, taskStuckTimeoutMs, lastFetchTimeMs);
const stalledReview = getStalledReviewSignal(task);
const showStalledReview = Boolean(stalledReview && task.column === "in-review" && !isPaused);
@@ -3085,7 +3087,9 @@ function TaskCardComponent({
<div className="card-meta-badges" data-testid="card-meta-badges">
{showPriorityBadge && (
<span className={`card-priority-badge card-priority-badge--${normalizedPriority}`}>
{normalizedPriority}
{/* FNXC:PriorityColorCoding 2026-07-11-00:00: Cards render the shared priority glyph with the priorityIndicator urgency color while preserving the existing badge text and non-normal visibility gate. */}
<PriorityBadgeIcon size={10} aria-hidden="true" style={{ color: getPriorityColorVar(normalizedPriority) }} />
<span>{normalizedPriority}</span>
</span>
)}
{task.executionMode === "fast" && (

View File

@@ -10,7 +10,7 @@ import { NodeHealthDot } from "./NodeHealthDot";
import { LoadingSpinner } from "./LoadingSpinner";
import { Sparkles, ChevronUp, ChevronDown, Maximize2, Minimize2, Paperclip, Zap, Brain, Server } from "lucide-react";
import { REPO_OVERRIDE_RE, resolveEffectiveGithubRepoDefault } from "./githubTracking";
import { getPriorityIcon, getPriorityLabel } from "../utils/priorityIndicator";
import { getPriorityColorVar, getPriorityIcon, getPriorityLabel } from "../utils/priorityIndicator";
import { ProviderIcon } from "./ProviderIcon";
import { WorkflowIcon } from "./WorkflowIcon";
@@ -1146,7 +1146,8 @@ export function TaskForm({
</button>
)}
{/* FNXC:NewTask 2026-06-23-00:10: Priority — cycles TASK_PRIORITIES via onPriorityChange (shared icon-only glyph language, same accessible label shape as QuickEntryBox). */}
{/* FNXC:NewTask 2026-06-23-00:10: Priority — cycles TASK_PRIORITIES via onPriorityChange (shared icon-only glyph language, same accessible label shape as QuickEntryBox).
FNXC:PriorityColorCoding 2026-07-11-00:00: Tint the inline priority glyph from priorityIndicator so the New Task row shares quick-add/card urgency colors without changing button semantics. */}
{onPriorityChange && (
<button
type="button"
@@ -1161,7 +1162,7 @@ export function TaskForm({
data-testid="task-form-inline-priority"
title={inlinePriorityButtonLabel}
>
<InlinePriorityIcon size={12} className="task-form-action-icon" aria-hidden="true" />
<InlinePriorityIcon size={12} className="task-form-action-icon" aria-hidden="true" style={{ color: getPriorityColorVar(inlinePriority) }} />
</button>
)}
</div>

View File

@@ -6,6 +6,7 @@ import { TASK_PRIORITIES, type Task, type TaskPriority } from "@fusion/core";
import { checkDuplicateTasks, fetchSettings, fetchAgents, uploadAttachment, fetchWorkflowOptionalSteps } from "../../api";
import { useNodes } from "../../hooks/useNodes";
import { scopedKey } from "../../utils/projectStorage";
import { getPriorityColorVar } from "../../utils/priorityIndicator";
import { loadAllAppCss } from "../../test/cssFixture";
const MOCK_MODELS = [
@@ -441,7 +442,16 @@ function expectQuickEntryPriorityButton(priority: TaskPriority) {
expect(priorityButton).toHaveAttribute("title", `Priority: ${label}`);
expect(priorityButton).toHaveAttribute("aria-label", `Priority: ${label}`);
expect(priorityButton).not.toHaveTextContent(label);
expect(priorityButton.querySelector("svg")?.classList.contains(QUICK_ENTRY_PRIORITY_ICON_CLASS[priority])).toBe(true);
const icon = priorityButton.querySelector("svg");
expect(icon?.classList.contains(QUICK_ENTRY_PRIORITY_ICON_CLASS[priority])).toBe(true);
expect(icon?.getAttribute("style")).toContain(`color: ${getPriorityColorVar(priority)}`);
}
function expectPriorityOptionColor(priority: TaskPriority) {
const option = screen.getByTestId(`quick-entry-priority-option-${priority}`);
const icon = option.querySelector("svg");
expect(icon?.classList.contains(QUICK_ENTRY_PRIORITY_ICON_CLASS[priority])).toBe(true);
expect(icon?.getAttribute("style")).toContain(`color: ${getPriorityColorVar(priority)}`);
}
describe("QuickEntryBox", () => {
@@ -2150,12 +2160,15 @@ describe("QuickEntryBox", () => {
expectQuickEntryPriorityButton("normal");
});
it("renders the icon-only priority glyph and accessible label for every level", () => {
it("renders urgency-colored priority glyphs in the trigger and picker for every level", () => {
renderQuickEntryBox({});
expandQuickEntry();
for (const taskPriority of TASK_PRIORITIES) {
openPriorityMenu();
for (const optionPriority of TASK_PRIORITIES) {
expectPriorityOptionColor(optionPriority);
}
fireEvent.click(screen.getByTestId(`quick-entry-priority-option-${taskPriority}`));
expectQuickEntryPriorityButton(taskPriority);
}

View File

@@ -1,3 +1,4 @@
import React from "react";
import { describe, it, expect, vi } from "vitest";
import { render } from "@testing-library/react";
import { TaskCard } from "../TaskCard";
@@ -20,6 +21,10 @@ vi.mock("lucide-react", () => ({
RotateCw: () => null,
Zap: () => null,
AlertTriangle: () => null,
ArrowDown: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-arrow-down" style={style} />,
Flag: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-flag" style={style} />,
ArrowUp: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-arrow-up" style={style} />,
TriangleAlert: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-triangle-alert" style={style} />,
}));
vi.mock("../ProviderIcon", () => ({

View File

@@ -1,3 +1,4 @@
import React from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { render } from "@testing-library/react";
import { TaskCard } from "../TaskCard";
@@ -22,6 +23,10 @@ vi.mock("lucide-react", () => ({
RotateCw: () => null,
Zap: () => null,
AlertTriangle: () => null,
ArrowDown: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-arrow-down" style={style} />,
Flag: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-flag" style={style} />,
ArrowUp: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-arrow-up" style={style} />,
TriangleAlert: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-triangle-alert" style={style} />,
Eye: () => null,
MoreHorizontal: () => null,
}));

View File

@@ -20,7 +20,8 @@ vi.mock("../../hooks/useToast", () => ({
import { NavigationHistoryProvider, useNavigationHistory } from "../../hooks/useNavigationHistory";
import { useOverlayDismiss } from "../../hooks/useOverlayDismiss";
import type { ConfirmOptions } from "../../hooks/useConfirm";
import type { Task } from "@fusion/core";
import { TASK_PRIORITIES, type Task, type TaskPriority } from "@fusion/core";
import { getPriorityColorVar } from "../../utils/priorityIndicator";
// Mock lucide-react to avoid SVG rendering issues in test env
vi.mock("lucide-react", () => ({
@@ -41,6 +42,10 @@ vi.mock("lucide-react", () => ({
RotateCw: () => null,
Zap: () => <svg data-testid="icon-zap" />,
AlertTriangle: () => null,
ArrowDown: ({ style }: { style?: React.CSSProperties }) => <svg data-testid="priority-icon-low" className="lucide-arrow-down" style={style} />,
Flag: ({ style }: { style?: React.CSSProperties }) => <svg data-testid="priority-icon-normal" className="lucide-flag" style={style} />,
ArrowUp: ({ style }: { style?: React.CSSProperties }) => <svg data-testid="priority-icon-high" className="lucide-arrow-up" style={style} />,
TriangleAlert: ({ style }: { style?: React.CSSProperties }) => <svg data-testid="priority-icon-urgent" className="lucide-triangle-alert" style={style} />,
ArrowUpRight: () => null,
// FN-7592: the overseer badge now renders an icon child instead of a text label,
// so tests must see a real SVG (like Zap) rather than a no-op render.
@@ -2785,6 +2790,32 @@ describe("TaskCard", () => {
]);
});
it("renders shared urgency-colored glyphs for visible priority badges while normal stays hidden", () => {
for (const priority of TASK_PRIORITIES) {
const { container, unmount } = render(
<TaskCard
task={makeTask({ priority: priority as TaskPriority })}
onOpenDetail={noop}
addToast={noop}
/>,
);
const badge = container.querySelector(".card-priority-badge");
if (priority === "normal") {
expect(badge).toBeNull();
unmount();
continue;
}
expect(badge).not.toBeNull();
expect(badge).toHaveTextContent(priority);
const icon = badge?.querySelector("svg");
expect(icon).not.toBeNull();
expect(icon?.getAttribute("style")).toContain(`color: ${getPriorityColorVar(priority)}`);
unmount();
}
});
it("renders partial card meta groups without empty wrappers when time is absent", () => {
const { container } = render(
<TaskCard

View File

@@ -1,13 +1,13 @@
import { describe, expect, it } from "vitest";
import { ArrowDown, ArrowUp, Flag, TriangleAlert } from "lucide-react";
import { TASK_PRIORITIES, type TaskPriority } from "@fusion/core";
import { getPriorityIcon, getPriorityLabel, priorityIndicator } from "../priorityIndicator";
import { getPriorityColorVar, getPriorityIcon, getPriorityLabel, priorityIndicator } from "../priorityIndicator";
const expectedIndicators: Record<TaskPriority, { icon: unknown; label: string }> = {
low: { icon: ArrowDown, label: "Low" },
normal: { icon: Flag, label: "Normal" },
high: { icon: ArrowUp, label: "High" },
urgent: { icon: TriangleAlert, label: "Urgent" },
const expectedIndicators: Record<TaskPriority, { icon: unknown; label: string; colorVar: string }> = {
low: { icon: ArrowDown, label: "Low", colorVar: "var(--color-info)" },
normal: { icon: Flag, label: "Normal", colorVar: "var(--text-muted)" },
high: { icon: ArrowUp, label: "High", colorVar: "var(--color-warning)" },
urgent: { icon: TriangleAlert, label: "Urgent", colorVar: "var(--color-error)" },
};
describe("priorityIndicator", () => {
@@ -16,6 +16,7 @@ describe("priorityIndicator", () => {
expect(priorityIndicator(priority)).toEqual(expectedIndicators[priority]);
expect(getPriorityIcon(priority)).toBe(expectedIndicators[priority].icon);
expect(getPriorityLabel(priority)).toBe(expectedIndicators[priority].label);
expect(getPriorityColorVar(priority)).toBe(expectedIndicators[priority].colorVar);
}
});
});

View File

@@ -4,17 +4,21 @@ import type { TaskPriority } from "@fusion/core";
export interface PriorityIndicator {
icon: LucideIcon;
label: string;
colorVar: string;
}
/*
FNXC:QuickAddPriorityIndicator 2026-07-10-12:00:
Quick-add and New Task priority affordances must share one glyph language: ArrowUp means high, ArrowDown means low, Flag means normal, and TriangleAlert means urgent. Keep this helper as the single source so icon-only priority buttons do not drift across composer surfaces.
FNXC:PriorityColorCoding 2026-07-11-00:00:
Priority affordances in quick add, the New Task inline row, and task cards must be color-coded by urgency from this single source: low=info, normal=muted, high=warning, urgent=error. Use semantic dashboard tokens only so composer glyphs and card badges cannot drift.
*/
const PRIORITY_INDICATORS: Record<TaskPriority, PriorityIndicator> = {
low: { icon: ArrowDown, label: "Low" },
normal: { icon: Flag, label: "Normal" },
high: { icon: ArrowUp, label: "High" },
urgent: { icon: TriangleAlert, label: "Urgent" },
low: { icon: ArrowDown, label: "Low", colorVar: "var(--color-info)" },
normal: { icon: Flag, label: "Normal", colorVar: "var(--text-muted)" },
high: { icon: ArrowUp, label: "High", colorVar: "var(--color-warning)" },
urgent: { icon: TriangleAlert, label: "Urgent", colorVar: "var(--color-error)" },
};
export function priorityIndicator(priority: TaskPriority): PriorityIndicator {
@@ -28,3 +32,7 @@ export function getPriorityIcon(priority: TaskPriority): LucideIcon {
export function getPriorityLabel(priority: TaskPriority): string {
return priorityIndicator(priority).label;
}
export function getPriorityColorVar(priority: TaskPriority): string {
return priorityIndicator(priority).colorVar;
}