feat(FN-2294): merge fusion/fn-2294
This commit is contained in:
@@ -291,6 +291,8 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
const [heartbeatMultiplier, setHeartbeatMultiplier] = useState<number>(1);
|
||||
/** Whether the heartbeat multiplier is currently being saved */
|
||||
const [isSavingMultiplier, setIsSavingMultiplier] = useState(false);
|
||||
/** Agent IDs with an in-flight state transition (for optimistic update guard) */
|
||||
const [transitioningAgentIds, setTransitioningAgentIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Load heartbeat multiplier from project settings on mount
|
||||
useEffect(() => {
|
||||
@@ -423,11 +425,13 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
}, [loadAgents]);
|
||||
|
||||
const handleStateChange = async (agentId: string, newState: AgentState) => {
|
||||
if (transitioningAgentIds.has(agentId)) return;
|
||||
const previousAgent = agents.find(a => a.id === agentId);
|
||||
setAgents(prev => prev.map(a => a.id === agentId ? { ...a, state: newState } : a));
|
||||
setTransitioningAgentIds(prev => new Set(prev).add(agentId));
|
||||
try {
|
||||
await updateAgentState(agentId, newState, projectId);
|
||||
addToast(`Agent state updated to ${newState}`, "success");
|
||||
|
||||
// When activating an agent, also start a heartbeat run so it shows activity
|
||||
if (newState === "active") {
|
||||
try {
|
||||
await startAgentRun(agentId, projectId);
|
||||
@@ -435,10 +439,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
addToast(`Agent activated, but failed to start run: ${runErr.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
void loadAgents();
|
||||
} catch (err: any) {
|
||||
if (previousAgent) {
|
||||
setAgents(prev => prev.map(a => a.id === agentId ? previousAgent : a));
|
||||
}
|
||||
addToast(`Failed to update state: ${err.message}`, "error");
|
||||
} finally {
|
||||
setTransitioningAgentIds(prev => { const next = new Set(prev); next.delete(agentId); return next; });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1114,6 +1122,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Activate"
|
||||
>
|
||||
<Play size={14} /> Start
|
||||
@@ -1132,6 +1141,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Run Now"
|
||||
aria-label={`Run now for ${agent.name}`}
|
||||
>
|
||||
@@ -1140,6 +1150,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Pause"
|
||||
>
|
||||
<Pause size={14} /> Pause
|
||||
@@ -1150,6 +1161,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Resume"
|
||||
>
|
||||
<Play size={14} /> Resume
|
||||
@@ -1168,6 +1180,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Pause"
|
||||
>
|
||||
<Pause size={14} /> Pause
|
||||
@@ -1178,6 +1191,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Retry"
|
||||
>
|
||||
<Play size={14} /> Retry
|
||||
@@ -1188,6 +1202,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Start"
|
||||
>
|
||||
<Play size={14} /> Start
|
||||
|
||||
@@ -322,7 +322,7 @@ export function MailboxModal({
|
||||
</div>
|
||||
<div className="mailbox-header-actions">
|
||||
<button
|
||||
className="btn-sm btn-primary"
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={handleOpenCompose}
|
||||
title="Compose message"
|
||||
data-testid="mailbox-header-compose"
|
||||
@@ -587,7 +587,7 @@ export function MailboxModal({
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
className="btn-sm btn-secondary mailbox-compose-btn"
|
||||
className="btn btn-sm btn-secondary mailbox-compose-btn"
|
||||
onClick={handleOpenCompose}
|
||||
data-testid="mailbox-compose-btn"
|
||||
>
|
||||
|
||||
@@ -381,7 +381,7 @@ export function MailboxView({
|
||||
</div>
|
||||
<div className="mailbox-header-actions">
|
||||
<button
|
||||
className="btn-sm btn-primary"
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={handleOpenCompose}
|
||||
title="Compose message"
|
||||
data-testid="mailbox-header-compose"
|
||||
@@ -650,7 +650,7 @@ export function MailboxView({
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
className="btn-sm btn-secondary mailbox-compose-btn"
|
||||
className="btn btn-sm btn-secondary mailbox-compose-btn"
|
||||
onClick={handleOpenCompose}
|
||||
data-testid="mailbox-compose-btn"
|
||||
>
|
||||
|
||||
@@ -219,7 +219,7 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager
|
||||
return (
|
||||
<div className="pi-ext-manager">
|
||||
<div className="pi-ext-manager-header">
|
||||
<h3>Pi Extensions</h3>
|
||||
<h4 className="settings-section-heading">Pi Extensions</h4>
|
||||
<div className="pi-ext-manager-actions">
|
||||
<button className="btn-icon" onClick={loadSettings} title="Refresh" disabled={loading}>
|
||||
<RefreshCw size={16} className={loading ? "spin" : ""} />
|
||||
@@ -254,7 +254,7 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager
|
||||
disabled={installing}
|
||||
/>
|
||||
<button
|
||||
className="btn-primary"
|
||||
className="btn btn-primary"
|
||||
onClick={handleInstall}
|
||||
disabled={installing || !newSource.trim()}
|
||||
>
|
||||
@@ -389,7 +389,7 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager
|
||||
<div className="pi-ext-discovered-header">
|
||||
<h4>Discovered Extensions</h4>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
className="btn-icon"
|
||||
onClick={loadExtensions}
|
||||
disabled={extensionsLoading}
|
||||
title="Refresh extensions"
|
||||
|
||||
@@ -349,6 +349,9 @@ describe("MailboxModal", () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-header-compose")).toBeDefined();
|
||||
});
|
||||
|
||||
const headerComposeButton = screen.getByTestId("mailbox-header-compose");
|
||||
expect(headerComposeButton).toHaveClass("btn", "btn-sm", "btn-primary");
|
||||
});
|
||||
|
||||
it("shows compose button in header on agents tab", async () => {
|
||||
@@ -365,6 +368,9 @@ describe("MailboxModal", () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-compose-btn")).toBeDefined();
|
||||
});
|
||||
|
||||
const agentsComposeButton = screen.getByTestId("mailbox-compose-btn");
|
||||
expect(agentsComposeButton).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-compose-btn");
|
||||
});
|
||||
|
||||
it("compose opened from Agents tab without selected agent shows recipient select", async () => {
|
||||
|
||||
@@ -385,6 +385,9 @@ describe("MailboxView", () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-header-compose")).toBeDefined();
|
||||
});
|
||||
|
||||
const headerComposeButton = screen.getByTestId("mailbox-header-compose");
|
||||
expect(headerComposeButton).toHaveClass("btn", "btn-sm", "btn-primary");
|
||||
});
|
||||
|
||||
it("shows compose button in header on agents tab", async () => {
|
||||
@@ -552,6 +555,9 @@ describe("MailboxView", () => {
|
||||
expect(screen.getByTestId("mailbox-agent-subtab-inbox")).toBeDefined();
|
||||
expect(screen.getByTestId("mailbox-agent-subtab-outbox")).toBeDefined();
|
||||
});
|
||||
|
||||
const agentsComposeButton = screen.getByTestId("mailbox-compose-btn");
|
||||
expect(agentsComposeButton).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-compose-btn");
|
||||
});
|
||||
|
||||
it("switches to outbox view when clicking outbox sub-tab", async () => {
|
||||
|
||||
Reference in New Issue
Block a user