From 065dad8cdb58e3fb86af567bf43a5399ec760398 Mon Sep 17 00:00:00 2001 From: Fusion Date: Wed, 29 Apr 2026 21:50:38 -0700 Subject: [PATCH] feat(FN-3004): merge fusion/fn-3004 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **New `fn custom-provider` CLI commands** (`register`, `unregister`, `list`) for managing custom AI providers; `dashboard.ts` and `serve.ts` now initialize the registry, and `packages/engine/src/custom-providers.ts` exposes the engine integration - **Custom provider registration API** in `packages/dashboard/src/routes/register-agent-core-routes.ts` with `POST /api/agent-core/register-custom-provider`; providers are persisted in `store.ts` and exposed via `GET /api/agent-core/custom-providers` - **Agent heartbeat improvements** (`packages/engine/src/agent-heartbeat.ts`): clearer conflict semantics when task is checked out by another agent, refactored `WakeContext` handling, and budget enforcement at heartbeat time - **Inline quick chat composer** (FN-3004): `QuickChatFAB` reworked with inline attachment row, improved prompt template logic, and full TypeScript coverage in `QuickChatFAB.test.tsx` - **Settings and onboarding**: `SettingsModal.tsx` surfaces custom provider management; `ModelOnboardingModal.tsx` updated to use the new registry - **Agent instructions expanded** (`packages/engine/src/agent-instructions.ts`): richer system prompt injection for spawned agents including agent personality/soul, ratings context, and parent assignment details - **`agent-store.ts`** now tracks `reportsTo` for spawned agent hierarchy and exposes `list_agents` and `delegate_task` tools via the pi extension - **Database layer**: `db.ts` uses `prepareSelect()` helpers for query consistency; WAL mode enforced; foreign-key checks enabled in test suite - **New tests**: `custom-provider-registry.test.ts`, `pi-create-fn-agent.test.ts`, `agent-heartbeat.test.ts`, `notifier.test.ts`, `routes.test.ts` for dashboard core routes, plus coverage for `QuickChatFAB`, `TaskCard`, and `UpdateAvailableBanner` - **TaskCard**: status badge now uses `--card-status-badge--{status}` CSS classes instead of inline style helpers; `TaskCard.test.tsx` updated to match Commits merged: - fix(FN-3004): restore dashboard typecheck for custom provider state - test(FN-3004): complete Step 2 — cover inline composer attachment row - feat(FN-3004): complete Step 1 — inline quick chat composer row - feat(FN-3003): merge fusion/fn-3003 - feat(FN-2962): merge fusion/fn-2962 - fix(FN-XXX): update release notes link in update notification Files changed: .changeset/fix-update-release-notes-link.md | 5 + .changeset/register-custom-providers.md | 5 + docs/architecture.md | 2 + .../__tests__/custom-provider-registry.test.ts | 167 +++++++++++++++++++++ .../cli/src/commands/__tests__/dashboard.test.ts | 4 + packages/cli/src/commands/__tests__/serve.test.ts | 3 + .../cli/src/commands/custom-provider-registry.ts | 96 ++++++++++++ packages/cli/src/commands/dashboard.ts | 28 ++++ packages/cli/src/commands/serve.ts | 28 ++++ packages/core/src/__tests__/db.test.ts | 26 ++-- packages/core/src/__tests__/insight-store.test.ts | 8 +- packages/core/src/__tests__/mission-store.test.ts | 2 +- packages/core/src/__tests__/roadmap-store.test.ts | 2 +- packages/core/src/__tests__/run-audit.test.ts | 2 +- packages/core/src/__tests__/task-documents.test.ts | 2 +- packages/core/src/agent-store.ts | 16 +- packages/core/src/db.ts | 13 +- packages/core/src/index.ts | 2 +- packages/core/src/store.ts | 35 ++++- packages/core/src/types.ts | 33 ++++ packages/dashboard/app/api/legacy.ts | 11 ++ .../dashboard/app/components/AgentDetailView.tsx | 92 +++++++++++- .../app/components/ModelOnboardingModal.tsx | 15 +- packages/dashboard/app/components/QuickChatFAB.css | 19 ++- packages/dashboard/app/components/QuickChatFAB.tsx | 95 ++++++------ .../dashboard/app/components/SettingsModal.tsx | 39 ++++- packages/dashboard/app/components/TaskCard.tsx | 46 ++++-- .../app/components/UpdateAvailableBanner.tsx | 2 +- .../app/components/__tests__/QuickChatFAB.test.tsx | 23 +++ .../app/components/__tests__/TaskCard.test.tsx | 2 +- .../__tests__/UpdateAvailableBanner.test.tsx | 2 +- .../src/__tests__/routes-agent-skills.test.ts | 1 + packages/dashboard/src/__tests__/routes.test.ts | 34 +++++ .../src/routes/register-agent-core-routes.ts | 78 ++++++++++ .../dashboard/src/routes/register-git-github.ts | 16 +- .../engine/src/__tests__/agent-heartbeat.test.ts | 59 ++++++++ packages/engine/src/__tests__/notifier.test.ts | 33 ++++ .../src/__tests__/pi-create-fn-agent.test.ts | 36 +++++ .../engine/src/__tests__/project-engine.test.ts | 53 ++++++- packages/engine/src/agent-heartbeat.ts | 86 ++++++++++- packages/engine/src/agent-instructions.ts | 103 ++++++++++++- packages/engine/src/custom-providers.ts | 15 ++ packages/engine/src/index.ts | 3 + packages/engine/src/notifier.ts | 3 +- packages/engine/src/pi.ts | 30 ++++ packages/engine/src/project-engine.ts | 12 +- 46 files changed, 1254 insertions(+), 133 deletions(-) Fusion-Task-Id: FN-3004 --- .../app/components/ModelOnboardingModal.tsx | 2 +- .../dashboard/app/components/QuickChatFAB.css | 19 ++-- .../dashboard/app/components/QuickChatFAB.tsx | 95 ++++++++++--------- .../app/components/SettingsModal.tsx | 22 ++++- .../__tests__/QuickChatFAB.test.tsx | 23 +++++ 5 files changed, 105 insertions(+), 56 deletions(-) diff --git a/packages/dashboard/app/components/ModelOnboardingModal.tsx b/packages/dashboard/app/components/ModelOnboardingModal.tsx index 3ddeeff0e..886b0ae37 100644 --- a/packages/dashboard/app/components/ModelOnboardingModal.tsx +++ b/packages/dashboard/app/components/ModelOnboardingModal.tsx @@ -605,7 +605,7 @@ export function ModelOnboardingModal({ const [apiKeyInputs, setApiKeyInputs] = useState>({}); const [apiKeyErrors, setApiKeyErrors] = useState>({}); const [apiKeySuccess, setApiKeySuccess] = useState>({}); - const [customProviders, setCustomProviders] = useState([]); + const [customProviders, setCustomProviders] = useState([]); const [showCustomProviderForm, setShowCustomProviderForm] = useState(false); const [customProviderSaving, setCustomProviderSaving] = useState(false); const [customProviderError, setCustomProviderError] = useState(); diff --git a/packages/dashboard/app/components/QuickChatFAB.css b/packages/dashboard/app/components/QuickChatFAB.css index e4122e3d3..1b94cfce7 100644 --- a/packages/dashboard/app/components/QuickChatFAB.css +++ b/packages/dashboard/app/components/QuickChatFAB.css @@ -471,9 +471,8 @@ .quick-chat-input-wrapper { position: relative; display: flex; - align-items: center; - gap: var(--space-sm); flex: 1; + flex-direction: column; min-width: 0; border: 1px solid var(--border); border-radius: var(--radius-sm); @@ -481,6 +480,13 @@ padding: var(--space-xs); } +.quick-chat-input-row { + display: flex; + align-items: center; + gap: var(--space-sm); + min-width: 0; +} + .quick-chat-input-wrapper--dragover { border-color: var(--todo); background: color-mix(in srgb, var(--todo) 5%, var(--bg)); @@ -510,8 +516,8 @@ flex-shrink: 0; } -/* Only the outer send/stop action keeps the larger touch target. */ -.quick-chat-panel-input > button { +/* The send/stop action stays in the same inline composer row as attach + input. */ +.quick-chat-send-btn { min-width: calc(var(--space-lg) + var(--space-xl)); min-height: calc(var(--space-lg) + var(--space-xl)); width: calc(var(--space-lg) + var(--space-xl)); @@ -523,9 +529,10 @@ display: inline-flex; align-items: center; justify-content: center; + flex-shrink: 0; } -.quick-chat-panel-input > button:disabled { +.quick-chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; } @@ -643,7 +650,7 @@ height: calc(var(--space-lg) * 2 + var(--space-sm)); } - .quick-chat-panel-input > button { + .quick-chat-send-btn { min-width: calc(var(--space-lg) + var(--space-xl)); min-height: calc(var(--space-lg) + var(--space-xl)); } diff --git a/packages/dashboard/app/components/QuickChatFAB.tsx b/packages/dashboard/app/components/QuickChatFAB.tsx index bd4984bf1..48cc7cb04 100644 --- a/packages/dashboard/app/components/QuickChatFAB.tsx +++ b/packages/dashboard/app/components/QuickChatFAB.tsx @@ -1756,30 +1756,55 @@ export function QuickChatFAB({ event.target.value = ""; }} /> - - +
+ + + {isStreaming ? ( + + ) : ( + + )} +
)} - {isStreaming ? ( - - ) : ( - - )} )} diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index c865df287..e13909ff2 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -415,7 +415,7 @@ export function SettingsModal({ const [loginInstructions, setLoginInstructions] = useState>({}); const [apiKeyInputs, setApiKeyInputs] = useState>({}); const [apiKeyErrors, setApiKeyErrors] = useState>({}); - const [customProviders, setCustomProviders] = useState([]); + const [customProviders, setCustomProviders] = useState([]); const [customProviderEditing, setCustomProviderEditing] = useState(null); const [showCustomProviderForm, setShowCustomProviderForm] = useState(false); const [customProviderSaving, setCustomProviderSaving] = useState(false); @@ -833,7 +833,7 @@ export function SettingsModal({ } }, [customProviderEditing, loadCustomProviders]); - const handleDeleteCustomProvider = useCallback(async (provider: CustomProviderConfig) => { + const handleDeleteCustomProvider = useCallback(async (provider: CustomProvider) => { const ok = await confirm({ title: "Delete custom provider", message: `Delete custom provider '${provider.id}'?`, @@ -4711,7 +4711,23 @@ export function SettingsModal({ {provider.id}
- +
diff --git a/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx b/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx index 3ddf91219..93c88c905 100644 --- a/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx +++ b/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx @@ -2309,6 +2309,29 @@ describe("QuickChatFAB", () => { clickSpy.mockRestore(); }); + it("keeps paperclip trigger and message input in the same composer row", async () => { + render(); + fireEvent.click(screen.getByTestId("quick-chat-fab")); + + const composerRow = await screen.findByTestId("quick-chat-input-row"); + const input = screen.getByTestId("quick-chat-input"); + const attachBtn = screen.getByTestId("quick-chat-attach-btn"); + const sendBtn = screen.getByTestId("quick-chat-send"); + + expect(composerRow).toContainElement(attachBtn); + expect(composerRow).toContainElement(input); + expect(composerRow).toContainElement(sendBtn); + + const fileInput = document.querySelector(".quick-chat-attachment-input") as HTMLInputElement; + const imageFile = new File(["img"], "photo.png", { type: "image/png" }); + fireEvent.change(fileInput, { target: { files: [imageFile] } }); + + await waitFor(() => { + expect(screen.getByTestId("quick-chat-attachment-preview-0")).toBeInTheDocument(); + expect(screen.getByTestId("quick-chat-send")).not.toBeDisabled(); + }); + }); + it("adds/removes previews for selected attachments and filters unsupported files", async () => { render(); fireEvent.click(screen.getByTestId("quick-chat-fab"));