feat(FN-2202): reposition and restyle task card agent badge
- Move the assigned agent badge out of the card header into a dedicated metadata row below task details - Add a new .card-agent-row container to control spacing and alignment for the badge block - Update .card-agent-badge styling to use token-aligned pill radius and color-mix backgrounds while removing monospace/fixed-width conventions - Expand TaskCard agent badge tests to verify new DOM placement and enforced badge style rules
This commit is contained in:
5
.changeset/fix-cli-help-lazy-command-load.md
Normal file
5
.changeset/fix-cli-help-lazy-command-load.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gsxdsm/fusion": patch
|
||||
---
|
||||
|
||||
Load CLI command handlers lazily so `fn --help` can exit quickly in bundled binaries without timing out.
|
||||
@@ -59,28 +59,110 @@ function configurePiPackage(): void {
|
||||
|
||||
configurePiPackage();
|
||||
|
||||
// Dynamic imports so the pi-coding-agent config module sees PI_PACKAGE_DIR
|
||||
const { runDashboard } = await import("./commands/dashboard.js");
|
||||
const { runServe } = await import("./commands/serve.js");
|
||||
const { runDaemon } = await import("./commands/daemon.js");
|
||||
const { runDesktop } = await import("./commands/desktop.js");
|
||||
const { runTaskCreate, runTaskList, runTaskMove, runTaskMerge, runTaskUpdate, runTaskLog, runTaskLogs, runTaskShow, runTaskAttach, runTaskPause, runTaskUnpause, runTaskImportFromGitHub, runTaskDuplicate, runTaskArchive, runTaskUnarchive, runTaskRefine, runTaskPlan, runTaskDelete, runTaskRetry, runTaskComment, runTaskComments, runTaskSteer, runTaskPrCreate } = await import("./commands/task.js");
|
||||
const { runSettingsShow, runSettingsSet } = await import("./commands/settings.js");
|
||||
const { runSettingsExport } = await import("./commands/settings-export.js");
|
||||
const { runSettingsImport } = await import("./commands/settings-import.js");
|
||||
const { runGitStatus, runGitFetch, runGitPull, runGitPush } = await import("./commands/git.js");
|
||||
const { runBackupCreate, runBackupList, runBackupRestore, runBackupCleanup } = await import("./commands/backup.js");
|
||||
const { runMissionCreate, runMissionList, runMissionShow, runMissionDelete, runMissionActivateSlice } = await import("./commands/mission.js");
|
||||
const { runProjectList, runProjectAdd, runProjectRemove, runProjectShow, runProjectInfo, runProjectSetDefault, runProjectDetect } = await import("./commands/project.js");
|
||||
const { runNodeList, runNodeConnect, runNodeDisconnect, runNodeShow, runNodeHealth, runMeshStatus } = await import("./commands/node.js");
|
||||
const { runInit } = await import("./commands/init.js");
|
||||
const { runAgentStop, runAgentStart } = await import("./commands/agent.js");
|
||||
const { runAgentImport } = await import("./commands/agent-import.js");
|
||||
const { runAgentExport } = await import("./commands/agent-export.js");
|
||||
const { runMessageInbox, runMessageOutbox, runMessageSend, runMessageRead, runMessageDelete, runAgentMailbox } = await import("./commands/message.js");
|
||||
const { runPluginList, runPluginInstall, runPluginUninstall, runPluginEnable, runPluginDisable } = await import("./commands/plugin.js");
|
||||
const { runPluginCreate } = await import("./commands/plugin-scaffold.js");
|
||||
const { runSkillsSearch, runSkillsInstall } = await import("./commands/skills.js");
|
||||
// Command handlers are loaded lazily so --help can return immediately
|
||||
// without importing the full command graph.
|
||||
async function loadCommandHandlers() {
|
||||
const { runDashboard } = await import("./commands/dashboard.js");
|
||||
const { runServe } = await import("./commands/serve.js");
|
||||
const { runDaemon } = await import("./commands/daemon.js");
|
||||
const { runDesktop } = await import("./commands/desktop.js");
|
||||
const { runTaskCreate, runTaskList, runTaskMove, runTaskMerge, runTaskUpdate, runTaskLog, runTaskLogs, runTaskShow, runTaskAttach, runTaskPause, runTaskUnpause, runTaskImportFromGitHub, runTaskDuplicate, runTaskArchive, runTaskUnarchive, runTaskRefine, runTaskPlan, runTaskDelete, runTaskRetry, runTaskComment, runTaskComments, runTaskSteer, runTaskPrCreate } = await import("./commands/task.js");
|
||||
const { runSettingsShow, runSettingsSet } = await import("./commands/settings.js");
|
||||
const { runSettingsExport } = await import("./commands/settings-export.js");
|
||||
const { runSettingsImport } = await import("./commands/settings-import.js");
|
||||
const { runGitStatus, runGitFetch, runGitPull, runGitPush } = await import("./commands/git.js");
|
||||
const { runBackupCreate, runBackupList, runBackupRestore, runBackupCleanup } = await import("./commands/backup.js");
|
||||
const { runMissionCreate, runMissionList, runMissionShow, runMissionDelete, runMissionActivateSlice } = await import("./commands/mission.js");
|
||||
const { runProjectList, runProjectAdd, runProjectRemove, runProjectShow, runProjectInfo, runProjectSetDefault, runProjectDetect } = await import("./commands/project.js");
|
||||
const { runNodeList, runNodeConnect, runNodeDisconnect, runNodeShow, runNodeHealth, runMeshStatus } = await import("./commands/node.js");
|
||||
const { runInit } = await import("./commands/init.js");
|
||||
const { runAgentStop, runAgentStart } = await import("./commands/agent.js");
|
||||
const { runAgentImport } = await import("./commands/agent-import.js");
|
||||
const { runAgentExport } = await import("./commands/agent-export.js");
|
||||
const { runMessageInbox, runMessageOutbox, runMessageSend, runMessageRead, runMessageDelete, runAgentMailbox } = await import("./commands/message.js");
|
||||
const { runPluginList, runPluginInstall, runPluginUninstall, runPluginEnable, runPluginDisable } = await import("./commands/plugin.js");
|
||||
const { runPluginCreate } = await import("./commands/plugin-scaffold.js");
|
||||
const { runSkillsSearch, runSkillsInstall } = await import("./commands/skills.js");
|
||||
|
||||
return {
|
||||
runDashboard,
|
||||
runServe,
|
||||
runDaemon,
|
||||
runDesktop,
|
||||
runTaskCreate,
|
||||
runTaskList,
|
||||
runTaskMove,
|
||||
runTaskMerge,
|
||||
runTaskUpdate,
|
||||
runTaskLog,
|
||||
runTaskLogs,
|
||||
runTaskShow,
|
||||
runTaskAttach,
|
||||
runTaskPause,
|
||||
runTaskUnpause,
|
||||
runTaskImportFromGitHub,
|
||||
runTaskDuplicate,
|
||||
runTaskArchive,
|
||||
runTaskUnarchive,
|
||||
runTaskRefine,
|
||||
runTaskPlan,
|
||||
runTaskDelete,
|
||||
runTaskRetry,
|
||||
runTaskComment,
|
||||
runTaskComments,
|
||||
runTaskSteer,
|
||||
runTaskPrCreate,
|
||||
runSettingsShow,
|
||||
runSettingsSet,
|
||||
runSettingsExport,
|
||||
runSettingsImport,
|
||||
runGitStatus,
|
||||
runGitFetch,
|
||||
runGitPull,
|
||||
runGitPush,
|
||||
runBackupCreate,
|
||||
runBackupList,
|
||||
runBackupRestore,
|
||||
runBackupCleanup,
|
||||
runMissionCreate,
|
||||
runMissionList,
|
||||
runMissionShow,
|
||||
runMissionDelete,
|
||||
runMissionActivateSlice,
|
||||
runProjectList,
|
||||
runProjectAdd,
|
||||
runProjectRemove,
|
||||
runProjectShow,
|
||||
runProjectInfo,
|
||||
runProjectSetDefault,
|
||||
runProjectDetect,
|
||||
runNodeList,
|
||||
runNodeConnect,
|
||||
runNodeDisconnect,
|
||||
runNodeShow,
|
||||
runNodeHealth,
|
||||
runMeshStatus,
|
||||
runInit,
|
||||
runAgentStop,
|
||||
runAgentStart,
|
||||
runAgentImport,
|
||||
runAgentExport,
|
||||
runMessageInbox,
|
||||
runMessageOutbox,
|
||||
runMessageSend,
|
||||
runMessageRead,
|
||||
runMessageDelete,
|
||||
runAgentMailbox,
|
||||
runPluginList,
|
||||
runPluginInstall,
|
||||
runPluginUninstall,
|
||||
runPluginEnable,
|
||||
runPluginDisable,
|
||||
runPluginCreate,
|
||||
runSkillsSearch,
|
||||
runSkillsInstall,
|
||||
};
|
||||
}
|
||||
|
||||
const HELP = `
|
||||
fn — AI-orchestrated task board
|
||||
@@ -315,6 +397,85 @@ async function main() {
|
||||
await checkAndMigrate();
|
||||
}
|
||||
|
||||
const {
|
||||
runDashboard,
|
||||
runServe,
|
||||
runDaemon,
|
||||
runDesktop,
|
||||
runTaskCreate,
|
||||
runTaskList,
|
||||
runTaskMove,
|
||||
runTaskMerge,
|
||||
runTaskUpdate,
|
||||
runTaskLog,
|
||||
runTaskLogs,
|
||||
runTaskShow,
|
||||
runTaskAttach,
|
||||
runTaskPause,
|
||||
runTaskUnpause,
|
||||
runTaskImportFromGitHub,
|
||||
runTaskDuplicate,
|
||||
runTaskArchive,
|
||||
runTaskUnarchive,
|
||||
runTaskRefine,
|
||||
runTaskPlan,
|
||||
runTaskDelete,
|
||||
runTaskRetry,
|
||||
runTaskComment,
|
||||
runTaskComments,
|
||||
runTaskSteer,
|
||||
runTaskPrCreate,
|
||||
runSettingsShow,
|
||||
runSettingsSet,
|
||||
runSettingsExport,
|
||||
runSettingsImport,
|
||||
runGitStatus,
|
||||
runGitFetch,
|
||||
runGitPull,
|
||||
runGitPush,
|
||||
runBackupCreate,
|
||||
runBackupList,
|
||||
runBackupRestore,
|
||||
runBackupCleanup,
|
||||
runMissionCreate,
|
||||
runMissionList,
|
||||
runMissionShow,
|
||||
runMissionDelete,
|
||||
runMissionActivateSlice,
|
||||
runProjectList,
|
||||
runProjectAdd,
|
||||
runProjectRemove,
|
||||
runProjectShow,
|
||||
runProjectInfo,
|
||||
runProjectSetDefault,
|
||||
runProjectDetect,
|
||||
runNodeList,
|
||||
runNodeConnect,
|
||||
runNodeDisconnect,
|
||||
runNodeShow,
|
||||
runNodeHealth,
|
||||
runMeshStatus,
|
||||
runInit,
|
||||
runAgentStop,
|
||||
runAgentStart,
|
||||
runAgentImport,
|
||||
runAgentExport,
|
||||
runMessageInbox,
|
||||
runMessageOutbox,
|
||||
runMessageSend,
|
||||
runMessageRead,
|
||||
runMessageDelete,
|
||||
runAgentMailbox,
|
||||
runPluginList,
|
||||
runPluginInstall,
|
||||
runPluginUninstall,
|
||||
runPluginEnable,
|
||||
runPluginDisable,
|
||||
runPluginCreate,
|
||||
runSkillsSearch,
|
||||
runSkillsInstall,
|
||||
} = await loadCommandHandlers();
|
||||
|
||||
try {
|
||||
switch (command) {
|
||||
case "init": {
|
||||
|
||||
@@ -813,17 +813,6 @@ function TaskCardComponent({
|
||||
{abbreviateMissionTitle(missionTitle ?? task.missionId)}
|
||||
</span>
|
||||
)}
|
||||
{task.assignedAgentId && (
|
||||
<span
|
||||
className={`card-agent-badge${isAgentNameLoading ? " card-agent-badge--loading" : ""}`}
|
||||
title={`Assigned to ${agentName ?? task.assignedAgentId}`}
|
||||
>
|
||||
<Bot size={11} />
|
||||
<span className="card-agent-badge-text">
|
||||
{abbreviateBadge(agentName ?? task.assignedAgentId, 15)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<div className="card-header-actions">
|
||||
{canEdit && (
|
||||
<button
|
||||
@@ -1065,6 +1054,19 @@ function TaskCardComponent({
|
||||
{(queued || task.status === "queued") && task.column !== "in-progress" && <span className="queued-badge"><Clock size={12} style={{ verticalAlign: "middle" }} /> Queued</span>}
|
||||
</div>
|
||||
)}
|
||||
{task.assignedAgentId && (
|
||||
<div className="card-agent-row">
|
||||
<span
|
||||
className={`card-agent-badge${isAgentNameLoading ? " card-agent-badge--loading" : ""}`}
|
||||
title={`Assigned to ${agentName ?? task.assignedAgentId}`}
|
||||
>
|
||||
<Bot size={11} />
|
||||
<span className="card-agent-badge-text">
|
||||
{abbreviateBadge(agentName ?? task.assignedAgentId, 15)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<PluginSlot slotId="task-card-badge" projectId={projectId} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3610,9 +3610,16 @@ describe("TaskCard agent badge", () => {
|
||||
expect(text).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const badge = screen.getByTitle("Assigned to Autopilot Agent");
|
||||
expect(badge.closest(".card-agent-row")).toBeTruthy();
|
||||
expect(badge.closest(".card-header")).toBeNull();
|
||||
|
||||
const styles = readFileSync(resolve(PACKAGE_ROOT, "app/styles.css"), "utf-8");
|
||||
expect(styles).toMatch(/\.card-agent-badge\s*\{[^}]*flex-shrink:\s*0;/);
|
||||
expect(styles).toMatch(/\.card-agent-badge\s*\{[^}]*max-width:\s*120px;/);
|
||||
expect(styles).toMatch(/\.card-agent-badge\s*\{[^}]*font-size:\s*10px;/);
|
||||
expect(styles).toMatch(/\.card-agent-badge\s*\{[^}]*border-radius:\s*var\(--radius-pill\);/);
|
||||
expect(styles).toMatch(/\.card-agent-badge\s*\{[^}]*background:\s*color-mix\(/);
|
||||
expect(styles).not.toMatch(/\.card-agent-badge\s*\{[^}]*font-family:\s*var\(--font-mono\);/);
|
||||
expect(styles).toMatch(/\.card-agent-row\s*\{/);
|
||||
expect(styles).toMatch(/\.card-agent-badge-text\s*\{[^}]*text-overflow:\s*ellipsis;/);
|
||||
expect(styles).toMatch(/\.card-agent-badge-text\s*\{[^}]*white-space:\s*nowrap;/);
|
||||
expect(styles).toMatch(/\.card-agent-badge-text\s*\{[^}]*overflow:\s*hidden;/);
|
||||
|
||||
@@ -1745,20 +1745,24 @@ body {
|
||||
background: var(--badge-mission-bg-hover);
|
||||
}
|
||||
|
||||
.card-agent-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.card-agent-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: var(--text-xs);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-muted);
|
||||
background: var(--card-agent-badge-bg, rgba(139, 148, 158, 0.15));
|
||||
border: 1px solid var(--card-agent-badge-border, rgba(139, 148, 158, 0.28));
|
||||
background: color-mix(in srgb, var(--text-muted) 18%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--text-muted) 35%, transparent);
|
||||
padding: 2px 6px;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
max-width: 120px;
|
||||
border-radius: var(--radius-pill);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -2841,7 +2841,11 @@ export class TaskExecutor {
|
||||
// The task is already in in-progress, so we need to:
|
||||
// 1. Move to todo (this triggers the guard to clear)
|
||||
// 2. Move back to in-progress (this triggers fresh execution)
|
||||
// moveTask(in-progress → todo) clears `task.worktree`; restore it before
|
||||
// the return trip so the dashboard never renders the task under
|
||||
// "Unassigned" and self-healing can't reclaim the worktree as idle.
|
||||
await this.store.moveTask(task.id, "todo");
|
||||
await this.store.updateTask(task.id, { worktree: worktreePath });
|
||||
await this.store.moveTask(task.id, "in-progress");
|
||||
executorLog.log(`${task.id}: revision rerun scheduled — moved to todo then in-progress`);
|
||||
} catch (err: unknown) {
|
||||
@@ -2970,8 +2974,12 @@ ${feedback}
|
||||
executorLog.log(`${task.id}: scheduling fresh execution after workflow step failure (retry ${retryCount}/${MAX_WORKFLOW_STEP_RETRIES})`);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// Move task to todo briefly, then back to in-progress to trigger fresh execution
|
||||
// Move task to todo briefly, then back to in-progress to trigger fresh execution.
|
||||
// moveTask(in-progress → todo) clears `task.worktree`; restore it before
|
||||
// the return trip so the dashboard never renders the task under
|
||||
// "Unassigned" and self-healing can't reclaim the worktree as idle.
|
||||
await this.store.moveTask(task.id, "todo");
|
||||
await this.store.updateTask(task.id, { worktree: worktreePath });
|
||||
await this.store.moveTask(task.id, "in-progress");
|
||||
executorLog.log(`${task.id}: workflow step retry scheduled — moved to todo then in-progress`);
|
||||
} catch (err: unknown) {
|
||||
@@ -3040,7 +3048,11 @@ ${feedback}
|
||||
// 6. Schedule the move after the guard unwinds (per guard-unwind requirement)
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// moveTask(in-progress → todo) clears `task.worktree`; restore it before
|
||||
// the return trip so the dashboard never renders the task under
|
||||
// "Unassigned" and self-healing can't reclaim the worktree as idle.
|
||||
await this.store.moveTask(taskId, "todo");
|
||||
await this.store.updateTask(taskId, { worktree: worktreePath });
|
||||
await this.store.moveTask(taskId, "in-progress");
|
||||
executorLog.log(`${taskId}: sent back to in-progress for remediation`);
|
||||
} catch (err: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user