diff --git a/.changeset/fix-cli-help-lazy-command-load.md b/.changeset/fix-cli-help-lazy-command-load.md new file mode 100644 index 000000000..3e19c8650 --- /dev/null +++ b/.changeset/fix-cli-help-lazy-command-load.md @@ -0,0 +1,5 @@ +--- +"@gsxdsm/fusion": patch +--- + +Load CLI command handlers lazily so `fn --help` can exit quickly in bundled binaries without timing out. \ No newline at end of file diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 1724e5c41..45485e0f1 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -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": { diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index b2fcd450f..65c5fba89 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -813,17 +813,6 @@ function TaskCardComponent({ {abbreviateMissionTitle(missionTitle ?? task.missionId)} )} - {task.assignedAgentId && ( - - - - {abbreviateBadge(agentName ?? task.assignedAgentId, 15)} - - - )}
{canEdit && (
)} + {task.assignedAgentId && ( +
+ + + + {abbreviateBadge(agentName ?? task.assignedAgentId, 15)} + + +
+ )} ); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 68626257f..bb0382700 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -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;/); diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index 210a0a8e7..257f20798 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -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; diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index c116fdb18..4a14b8040 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -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) {