feat(FN-3004): merge fusion/fn-3004
- **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
This commit is contained in:
@@ -605,7 +605,7 @@ export function ModelOnboardingModal({
|
||||
const [apiKeyInputs, setApiKeyInputs] = useState<Record<string, string>>({});
|
||||
const [apiKeyErrors, setApiKeyErrors] = useState<Record<string, string>>({});
|
||||
const [apiKeySuccess, setApiKeySuccess] = useState<Record<string, string | null>>({});
|
||||
const [customProviders, setCustomProviders] = useState<CustomProviderConfig[]>([]);
|
||||
const [customProviders, setCustomProviders] = useState<CustomProvider[]>([]);
|
||||
const [showCustomProviderForm, setShowCustomProviderForm] = useState(false);
|
||||
const [customProviderSaving, setCustomProviderSaving] = useState(false);
|
||||
const [customProviderError, setCustomProviderError] = useState<string | undefined>();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -1756,30 +1756,55 @@ export function QuickChatFAB({
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon quick-chat-attach-btn"
|
||||
data-testid="quick-chat-attach-btn"
|
||||
aria-label="Attach files"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Paperclip size={16} />
|
||||
</button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={messageInput}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onKeyUp={handleInputKeyUp}
|
||||
onClick={handleInputSelectionChange}
|
||||
onBlur={handleInputBlur}
|
||||
onFocus={handleInputFocus}
|
||||
onPaste={handlePaste}
|
||||
placeholder={inputPlaceholder}
|
||||
disabled={inputDisabled}
|
||||
data-testid="quick-chat-input"
|
||||
/>
|
||||
<div className="quick-chat-input-row" data-testid="quick-chat-input-row">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon quick-chat-attach-btn"
|
||||
data-testid="quick-chat-attach-btn"
|
||||
aria-label="Attach files"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Paperclip size={16} />
|
||||
</button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={messageInput}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onKeyUp={handleInputKeyUp}
|
||||
onClick={handleInputSelectionChange}
|
||||
onBlur={handleInputBlur}
|
||||
onFocus={handleInputFocus}
|
||||
onPaste={handlePaste}
|
||||
placeholder={inputPlaceholder}
|
||||
disabled={inputDisabled}
|
||||
data-testid="quick-chat-input"
|
||||
/>
|
||||
{isStreaming ? (
|
||||
<button
|
||||
type="button"
|
||||
className="chat-input-stop quick-chat-send-btn"
|
||||
onClick={stopStreaming}
|
||||
aria-label="Stop generation"
|
||||
data-testid="quick-chat-stop"
|
||||
>
|
||||
<Square size={14} />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="quick-chat-send-btn"
|
||||
onClick={() => {
|
||||
void handleSendMessage();
|
||||
}}
|
||||
disabled={inputDisabled || (messageInput.trim().length === 0 && pendingAttachments.length === 0)}
|
||||
data-testid="quick-chat-send"
|
||||
>
|
||||
<Send size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<AgentMentionPopup
|
||||
agents={agents}
|
||||
filter={mentionFilter}
|
||||
@@ -1817,28 +1842,6 @@ export function QuickChatFAB({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isStreaming ? (
|
||||
<button
|
||||
type="button"
|
||||
className="chat-input-stop"
|
||||
onClick={stopStreaming}
|
||||
aria-label="Stop generation"
|
||||
data-testid="quick-chat-stop"
|
||||
>
|
||||
<Square size={14} />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void handleSendMessage();
|
||||
}}
|
||||
disabled={inputDisabled || (messageInput.trim().length === 0 && pendingAttachments.length === 0)}
|
||||
data-testid="quick-chat-send"
|
||||
>
|
||||
<Send size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -415,7 +415,7 @@ export function SettingsModal({
|
||||
const [loginInstructions, setLoginInstructions] = useState<Record<string, string>>({});
|
||||
const [apiKeyInputs, setApiKeyInputs] = useState<Record<string, string>>({});
|
||||
const [apiKeyErrors, setApiKeyErrors] = useState<Record<string, string>>({});
|
||||
const [customProviders, setCustomProviders] = useState<CustomProviderConfig[]>([]);
|
||||
const [customProviders, setCustomProviders] = useState<CustomProvider[]>([]);
|
||||
const [customProviderEditing, setCustomProviderEditing] = useState<CustomProviderConfig | null>(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({
|
||||
<small className="settings-muted">{provider.id}</small>
|
||||
</div>
|
||||
<div className="auth-custom-provider-actions">
|
||||
<button type="button" className="btn btn-sm" onClick={() => { setCustomProviderEditing(provider); setShowCustomProviderForm(true); }}>Edit</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={() => {
|
||||
setCustomProviderEditing({
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
baseUrl: provider.baseUrl,
|
||||
api: provider.apiType === "anthropic-compatible" ? "anthropic-messages" : "openai-completions",
|
||||
apiKey: provider.apiKey,
|
||||
models: provider.models?.map((model) => ({ id: model.id, name: model.name })) ?? [],
|
||||
});
|
||||
setShowCustomProviderForm(true);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button type="button" className="btn btn-sm" onClick={() => { void handleDeleteCustomProvider(provider); }}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2309,6 +2309,29 @@ describe("QuickChatFAB", () => {
|
||||
clickSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("keeps paperclip trigger and message input in the same composer row", async () => {
|
||||
render(<QuickChatFAB addToast={addToast} />);
|
||||
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(<QuickChatFAB addToast={addToast} />);
|
||||
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||
|
||||
Reference in New Issue
Block a user