feat(FN-3448): normalize AgentsView button utility classes

The merge normalizes button utility classes in AgentsView, swapping 12 lines of CSS class references for their standardized counterparts for consistency and maintainability.

Fusion-Task-Id: FN-3448
This commit is contained in:
Fusion
2026-05-05 05:28:16 -07:00
committed by gsxdsm
parent 7e33272026
commit b227afa1da
8 changed files with 431 additions and 12 deletions

View File

@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import {
SHARED_MESH_PROTOCOL_ID,
SHARED_MESH_PROTOCOL_VERSION,
classifyReadStaleness,
createFenceToken,
getCoordinationModeForEntity,
getDefaultWriteClassForEntity,
getQuorumRequirement,
isMeshIntentState,
isMeshWriteClass,
isProtocolRef,
isQuorumSatisfied,
} from "../mesh-replication-protocol.js";
describe("mesh-replication-protocol", () => {
it("exposes protocol identity", () => {
expect(SHARED_MESH_PROTOCOL_ID).toBe("fusion.shared-mesh");
expect(SHARED_MESH_PROTOCOL_VERSION).toBe("1.0");
});
it("classifies entity coordination modes", () => {
expect(getCoordinationModeForEntity("task")).toBe("strongly-coordinated");
expect(getCoordinationModeForEntity("audit-event")).toBe("append-only-replicated");
expect(getCoordinationModeForEntity("filesystem-blob")).toBe("queued-for-later");
expect(getCoordinationModeForEntity("agent-runtime")).toBe("node-local-only");
});
it("maps entity coordination mode to default write class", () => {
expect(getDefaultWriteClassForEntity("task")).toBe("strong");
expect(getDefaultWriteClassForEntity("audit-event")).toBe("append-only");
expect(getDefaultWriteClassForEntity("filesystem-blob")).toBe("queued");
expect(getDefaultWriteClassForEntity("agent-runtime")).toBe("local");
});
it("computes quorum requirements and satisfies majority rule", () => {
expect(getQuorumRequirement(1)).toEqual({ eligibleVoters: 1, requiredAcks: 1 });
expect(getQuorumRequirement(2)).toEqual({ eligibleVoters: 2, requiredAcks: 2 });
expect(getQuorumRequirement(3)).toEqual({ eligibleVoters: 3, requiredAcks: 2 });
expect(isQuorumSatisfied(5, 2)).toBe(false);
expect(isQuorumSatisfied(5, 3)).toBe(true);
});
it("validates write class and intent state discriminators", () => {
expect(isMeshWriteClass("strong")).toBe(true);
expect(isMeshWriteClass("invalid")).toBe(false);
expect(isMeshIntentState("committed")).toBe(true);
expect(isMeshIntentState("waiting")).toBe(false);
});
it("creates deterministic fence tokens", () => {
expect(createFenceToken(7, "node_a", 99)).toBe("7:node_a:99");
});
it("validates protocol refs", () => {
expect(isProtocolRef({ protocol: "fusion.shared-mesh", version: "1.0" })).toBe(true);
expect(isProtocolRef({ protocol: "fusion.shared-mesh", version: "2.0" })).toBe(false);
});
it("classifies read staleness from queue depth and lag", () => {
const fresh = classifyReadStaleness({ queueDepth: 0, observedAt: "2026-05-05T00:00:10.000Z", lastGlobalCommitAt: "2026-05-05T00:00:10.000Z" });
expect(fresh.isStale).toBe(false);
expect(fresh.source).toBe("local-committed");
const queued = classifyReadStaleness({ queueDepth: 2, observedAt: "2026-05-05T00:00:10.000Z", lastGlobalCommitAt: "2026-05-05T00:00:09.000Z" });
expect(queued.isStale).toBe(true);
expect(queued.source).toBe("local-queued");
expect(queued.replicationLagMs).toBe(1000);
});
});

View File

@@ -1,6 +1,7 @@
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeConflictStrategy, buildResearchDocumentKey } from "./types.js";
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskSource, SourceType, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
export { AGENT_VALID_TRANSITIONS } from "./types.js";
export * from "./mesh-replication-protocol.js";
export {
BUILTIN_AGENT_PROMPTS,
resolveAgentPrompt,

View File

@@ -0,0 +1,179 @@
import type { NodeMeshState, PeerSyncRequest, PeerSyncResponse, SettingsSyncPayload } from "./types.js";
export const SHARED_MESH_PROTOCOL_ID = "fusion.shared-mesh" as const;
export const SHARED_MESH_PROTOCOL_VERSION = "1.0" as const;
export const MESH_WRITE_CLASSES = ["strong", "append-only", "queued", "local"] as const;
export type MeshWriteClass = (typeof MESH_WRITE_CLASSES)[number];
export const MESH_INTENT_STATES = ["intent", "committed", "rejected", "queued", "reconciled"] as const;
export type MeshIntentState = (typeof MESH_INTENT_STATES)[number];
export const MESH_RECONCILIATION_OUTCOMES = [
"applied",
"noop_already_applied",
"superseded",
"conflict_requires_merge",
"rejected_fenced",
] as const;
export type MeshReconciliationOutcome = (typeof MESH_RECONCILIATION_OUTCOMES)[number];
export type SharedMeshEntityType =
| "task"
| "task-metadata"
| "mission"
| "agent-config"
| "agent-runtime"
| "project-settings"
| "auth-material"
| "execution-run"
| "audit-event"
| "filesystem-blob";
export type SharedMeshCoordinationMode = "strongly-coordinated" | "append-only-replicated" | "queued-for-later" | "node-local-only";
export interface SharedMeshProtocolRef {
protocol: typeof SHARED_MESH_PROTOCOL_ID;
version: typeof SHARED_MESH_PROTOCOL_VERSION;
}
export interface SharedMeshLeaseRef {
leaseEpoch: number;
fenceToken: string;
coordinatorNodeId: string;
}
export interface SharedMeshWritePrecondition {
expectedBaseRevision?: string;
expectedLeaseEpoch?: number;
}
export interface SharedMeshReplicationEnvelope<TPayload = unknown> extends SharedMeshProtocolRef, SharedMeshLeaseRef {
recordId: string;
intentId: string;
entityType: SharedMeshEntityType;
entityId: string;
originNodeId: string;
originSeq: number;
writeClass: MeshWriteClass;
state: MeshIntentState;
createdAt: string;
committedAt?: string;
precondition?: SharedMeshWritePrecondition;
payload: TPayload;
}
export interface SharedMeshQueueEntryMeta {
firstAttemptAt: string;
lastAttemptAt?: string;
nextAttemptAt?: string;
retryCount: number;
}
export interface SharedMeshReconciliationResult {
intentId: string;
outcome: MeshReconciliationOutcome;
detail?: string;
}
export interface SharedMeshReadStaleness {
source: "local-committed" | "local-queued" | "replica";
lastGlobalCommitAt?: string;
replicationLagMs?: number;
queueDepth: number;
isStale: boolean;
}
export interface SharedMeshQuorumRequirement {
eligibleVoters: number;
requiredAcks: number;
}
export type SharedMeshSyncRequestEnvelope<TPayload = unknown> = PeerSyncRequest & {
replication?: SharedMeshReplicationEnvelope<TPayload>[];
};
export type SharedMeshSyncResponseEnvelope<TPayload = unknown> = PeerSyncResponse & {
replication?: SharedMeshReplicationEnvelope<TPayload>[];
};
export interface SharedMeshSettingsRecord {
settings: SettingsSyncPayload;
}
export interface SharedMeshSnapshot {
mesh: NodeMeshState;
staleness: SharedMeshReadStaleness;
}
const COORDINATION_BY_ENTITY: Record<SharedMeshEntityType, SharedMeshCoordinationMode> = {
task: "strongly-coordinated",
"task-metadata": "strongly-coordinated",
mission: "strongly-coordinated",
"agent-config": "strongly-coordinated",
"agent-runtime": "node-local-only",
"project-settings": "strongly-coordinated",
"auth-material": "queued-for-later",
"execution-run": "queued-for-later",
"audit-event": "append-only-replicated",
"filesystem-blob": "queued-for-later",
};
export function isMeshWriteClass(value: string): value is MeshWriteClass {
return MESH_WRITE_CLASSES.includes(value as MeshWriteClass);
}
export function isMeshIntentState(value: string): value is MeshIntentState {
return MESH_INTENT_STATES.includes(value as MeshIntentState);
}
export function getCoordinationModeForEntity(entityType: SharedMeshEntityType): SharedMeshCoordinationMode {
return COORDINATION_BY_ENTITY[entityType];
}
export function getDefaultWriteClassForEntity(entityType: SharedMeshEntityType): MeshWriteClass {
const mode = getCoordinationModeForEntity(entityType);
if (mode === "append-only-replicated") return "append-only";
if (mode === "queued-for-later") return "queued";
if (mode === "node-local-only") return "local";
return "strong";
}
export function getQuorumRequirement(eligibleVoters: number): SharedMeshQuorumRequirement {
const normalized = Math.max(1, Math.floor(eligibleVoters));
return {
eligibleVoters: normalized,
requiredAcks: Math.floor(normalized / 2) + 1,
};
}
export function isQuorumSatisfied(eligibleVoters: number, ackCount: number): boolean {
return ackCount >= getQuorumRequirement(eligibleVoters).requiredAcks;
}
export function createFenceToken(leaseEpoch: number, coordinatorNodeId: string, originSeq: number): string {
return `${leaseEpoch}:${coordinatorNodeId}:${originSeq}`;
}
export function isProtocolRef(value: { protocol?: string; version?: string } | null | undefined): value is SharedMeshProtocolRef {
return value?.protocol === SHARED_MESH_PROTOCOL_ID && value.version === SHARED_MESH_PROTOCOL_VERSION;
}
export function classifyReadStaleness(params: {
queueDepth: number;
lastGlobalCommitAt?: string;
observedAt?: string;
}): SharedMeshReadStaleness {
const observedAt = params.observedAt ? Date.parse(params.observedAt) : Date.now();
const lastGlobal = params.lastGlobalCommitAt ? Date.parse(params.lastGlobalCommitAt) : undefined;
const lag = lastGlobal !== undefined && Number.isFinite(lastGlobal) ? Math.max(0, observedAt - lastGlobal) : undefined;
const queueDepth = Math.max(0, params.queueDepth);
return {
source: queueDepth > 0 ? "local-queued" : "local-committed",
lastGlobalCommitAt: params.lastGlobalCommitAt,
replicationLagMs: lag,
queueDepth,
isStale: queueDepth > 0 || (lag ?? 0) > 0,
};
}

View File

@@ -1251,7 +1251,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
/>
<span className="text-secondary">min</span>
<button
className="btn btn--sm"
className="btn btn-sm"
onClick={() => void handleCustomHeartbeatSave(agent)}
disabled={isUpdatingHeartbeat}
title="Save custom interval"
@@ -1259,7 +1259,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
Save
</button>
<button
className="btn btn--sm"
className="btn btn-sm"
onClick={() => {
setCustomHeartbeatAgentId(null);
setCustomHeartbeatMinutes((prev) => {
@@ -1327,7 +1327,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
<div className="agent-card-actions">
{agent.state === "idle" && (
<button
className="btn btn--sm"
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)}
title="Activate"
@@ -1338,7 +1338,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
{agent.state === "active" && (
<>
<button
className="btn btn--sm"
className="btn btn-sm"
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
disabled={transitioningAgentIds.has(agent.id)}
title="Run Now"
@@ -1347,7 +1347,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
<Activity size={14} /> Run Now
</button>
<button
className="btn btn--sm"
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "paused")}
disabled={transitioningAgentIds.has(agent.id)}
title="Pause"
@@ -1358,7 +1358,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
)}
{agent.state === "paused" && (
<button
className="btn btn--sm"
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)}
title="Resume"
@@ -1369,7 +1369,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
{agent.state === "running" && (
<>
<button
className="btn btn--sm"
className="btn btn-sm"
onClick={() => openAgentDetail(agent.id, { initialTab: "runs", initialRunId: null, preferActiveRun: true })}
title="View live run details"
aria-label={`View live run details for ${agent.name}`}
@@ -1377,7 +1377,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
<Activity size={14} /> Running
</button>
<button
className="btn btn--sm"
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "paused")}
disabled={transitioningAgentIds.has(agent.id)}
title="Pause"
@@ -1388,7 +1388,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
)}
{agent.state === "error" && (
<button
className="btn btn--sm"
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)}
title="Retry"
@@ -1398,7 +1398,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
)}
{agent.state === "terminated" && (
<button
className="btn btn--sm"
className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)}
title="Start"
@@ -1407,7 +1407,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
</button>
)}
<button
className="btn btn--sm agent-card-details-btn"
className="btn btn-sm agent-card-details-btn"
onClick={() => openAgentDetail(agent.id)}
title={`View details for ${agent.name}`}
aria-label={`View details for ${agent.name}`}
@@ -1416,7 +1416,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
</button>
{(agent.state === "idle" || agent.state === "terminated" || agent.state === "paused") && (
<button
className="btn btn--sm btn--danger"
className="btn btn-sm btn-danger"
onClick={() => void handleDelete(agent.id, agent.name)}
title="Delete"
>