Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 26 additions & 97 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6838,103 +6838,32 @@ export interface MigrationResult {

// ── Messaging Types ──────────────────────────────────────────────────────────

/** Participant types for message routing */
export type ParticipantType = "agent" | "user" | "system";

/** Canonical recipient ID for dashboard user mailbox routing. */
export const DASHBOARD_USER_ID = "dashboard";

const DASHBOARD_USER_ALIASES = new Set([DASHBOARD_USER_ID, "user", "user:dashboard", "User: user:dashboard"]);

/** Normalize participant identity for durable mailbox routing. */
export function normalizeMessageParticipant(id: string, type: ParticipantType): { id: string; type: ParticipantType } {
if (type !== "user") {
return { id, type };
}

if (DASHBOARD_USER_ALIASES.has(id)) {
return { id: DASHBOARD_USER_ID, type };
}

return { id, type };
}

/** Message types/categories */
export type MessageType = "agent-to-agent" | "agent-to-user" | "user-to-agent" | "system";

/** Stable metadata contract for linking a reply to an earlier message. */
export interface MessageReplyReference {
/** ID of the message this one is replying to. */
messageId: string;
}

/** Optional metadata attached to mailbox messages. */
export interface MessageMetadata extends Record<string, unknown> {
/** Optional link to the original message when this message is a reply. */
replyTo?: MessageReplyReference;
/**
* If true, the recipient agent is woken immediately on receipt regardless
* of their own `messageResponseMode` setting. Sender-initiated override —
* use sparingly for urgent messages. Ignored when recipient is a user.
*/
wakeRecipient?: boolean;
}

/** Message record stored in the system */
export interface Message {
/** Unique identifier */
id: string;
/** Sender identifier */
fromId: string;
/** Sender type */
fromType: ParticipantType;
/** Recipient identifier */
toId: string;
/** Recipient type */
toType: ParticipantType;
/** Message body */
content: string;
/** Message category */
type: MessageType;
/** Whether the recipient has read this message */
read: boolean;
/** Optional extra data */
metadata?: MessageMetadata;
/** ISO-8601 timestamp of creation */
createdAt: string;
/** ISO-8601 timestamp of last update */
updatedAt: string;
}

/** Input for creating a new message */
export interface MessageCreateInput {
/** Sender identifier (auto-filled by the transport layer if omitted) */
fromId?: string;
/** Sender type (auto-filled by the transport layer if omitted) */
fromType?: ParticipantType;
/** Recipient identifier */
toId: string;
/** Recipient type */
toType: ParticipantType;
/** Message body */
content: string;
/** Message category */
type: MessageType;
/** Optional extra data */
metadata?: MessageMetadata;
}

/** Filter options for querying messages */
export interface MessageFilter {
/** Filter by message type */
type?: MessageType;
/** Filter by read status */
read?: boolean;
/** Maximum number of messages to return */
limit?: number;
/** Number of messages to skip (for pagination) */
offset?: number;
}
import {
DASHBOARD_USER_ID,
normalizeMessageParticipant,
} from "./types/messages.js";
export {
DASHBOARD_USER_ID,
normalizeMessageParticipant,
};
import type {
ParticipantType,
MessageType,
MessageReplyReference,
MessageMetadata,
Message,
MessageCreateInput,
MessageFilter,
} from "./types/messages.js";
export type {
ParticipantType,
MessageType,
MessageReplyReference,
MessageMetadata,
Message,
MessageCreateInput,
MessageFilter,
};

/** Validate mailbox metadata, including reply-link contract when present. */
export function validateMessageMetadata(metadata: MessageMetadata | undefined): void {
Expand Down
101 changes: 101 additions & 0 deletions packages/core/src/types/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* FNXC:CodeOrganization 2026-07-17-12:00:
* Messaging domain types peeled from types.ts.
*/

export type ParticipantType = "agent" | "user" | "system";

/** Canonical recipient ID for dashboard user mailbox routing. */
export const DASHBOARD_USER_ID = "dashboard";

const DASHBOARD_USER_ALIASES = new Set([DASHBOARD_USER_ID, "user", "user:dashboard", "User: user:dashboard"]);

/** Normalize participant identity for durable mailbox routing. */
export function normalizeMessageParticipant(id: string, type: ParticipantType): { id: string; type: ParticipantType } {
if (type !== "user") {
return { id, type };
}

if (DASHBOARD_USER_ALIASES.has(id)) {
return { id: DASHBOARD_USER_ID, type };
}

return { id, type };
}

/** Message types/categories */
export type MessageType = "agent-to-agent" | "agent-to-user" | "user-to-agent" | "system";

/** Stable metadata contract for linking a reply to an earlier message. */
export interface MessageReplyReference {
/** ID of the message this one is replying to. */
messageId: string;
}

/** Optional metadata attached to mailbox messages. */
export interface MessageMetadata extends Record<string, unknown> {
/** Optional link to the original message when this message is a reply. */
replyTo?: MessageReplyReference;
/**
* If true, the recipient agent is woken immediately on receipt regardless
* of their own `messageResponseMode` setting. Sender-initiated override —
* use sparingly for urgent messages. Ignored when recipient is a user.
*/
wakeRecipient?: boolean;
}

/** Message record stored in the system */
export interface Message {
/** Unique identifier */
id: string;
/** Sender identifier */
fromId: string;
/** Sender type */
fromType: ParticipantType;
/** Recipient identifier */
toId: string;
/** Recipient type */
toType: ParticipantType;
/** Message body */
content: string;
/** Message category */
type: MessageType;
/** Whether the recipient has read this message */
read: boolean;
/** Optional extra data */
metadata?: MessageMetadata;
/** ISO-8601 timestamp of creation */
createdAt: string;
/** ISO-8601 timestamp of last update */
updatedAt: string;
}

/** Input for creating a new message */
export interface MessageCreateInput {
/** Sender identifier (auto-filled by the transport layer if omitted) */
fromId?: string;
/** Sender type (auto-filled by the transport layer if omitted) */
fromType?: ParticipantType;
/** Recipient identifier */
toId: string;
/** Recipient type */
toType: ParticipantType;
/** Message body */
content: string;
/** Message category */
type: MessageType;
/** Optional extra data */
metadata?: MessageMetadata;
}

/** Filter options for querying messages */
export interface MessageFilter {
/** Filter by message type */
type?: MessageType;
/** Filter by read status */
read?: boolean;
/** Maximum number of messages to return */
limit?: number;
/** Number of messages to skip (for pagination) */
offset?: number;
}
117 changes: 117 additions & 0 deletions packages/dashboard/app/api/global-and-pi-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* FNXC:CodeOrganization 2026-07-17-12:00:
* Global settings and pi-extension/package client API peeled from legacy.ts.
*/
import type { GlobalSettings, ProjectSettings, Settings } from "@fusion/core";
import { api } from "./client.js";
import type { FetchOptions } from "./client.js";
import { withProjectId } from "./health.js";
import { dedupe } from "./dedupe.js";

export function fetchGlobalSettings(options?: FetchOptions): Promise<GlobalSettings> {
return dedupe("/settings/global", () => api<GlobalSettings>("/settings/global"), options);
}

/** Update global (user-level) settings. These persist across all fn projects. */
export function updateGlobalSettings(settings: Partial<GlobalSettings>): Promise<Settings> {
return api<Settings>("/settings/global", {
method: "PUT",
body: JSON.stringify(settings),
});
}

/** Fetch settings separated by scope: { global, project } */
export function fetchSettingsByScope(projectId?: string): Promise<{ global: GlobalSettings; project: Partial<ProjectSettings> }> {
return api<{ global: GlobalSettings; project: Partial<ProjectSettings> }>(withProjectId("/settings/scopes", projectId));
}

export interface PiExtensionEntry {
id: string;
name: string;
path: string;
source: "fusion-global" | "pi-global" | "fusion-project" | "pi-project" | "package";
enabled: boolean;
}

export interface PiExtensionSettings {
extensions: PiExtensionEntry[];
disabledIds: string[];
settingsPath: string;
}

export function fetchPiExtensions(projectId?: string): Promise<PiExtensionSettings> {
return api<PiExtensionSettings>(withProjectId("/settings/pi-extensions", projectId));
}

export function updatePiExtensions(disabledIds: string[], projectId?: string): Promise<PiExtensionSettings> {
return api<PiExtensionSettings>(withProjectId("/settings/pi-extensions", projectId), {
method: "PUT",
body: JSON.stringify({ disabledIds }),
});
}

/**
* Test a notification provider by sending a test notification.
* Supports "ntfy" and "webhook" provider IDs.
*/
export function testNotification(providerId: string, config?: Record<string, unknown>, projectId?: string): Promise<{ success: boolean }> {
return api<{ success: boolean }>(withProjectId("/settings/test-notification", projectId), {
method: "POST",
// Pin providerId last so config.providerId cannot override the selected provider.
body: JSON.stringify({ ...(config ?? {}), providerId }),
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Backward-compatible ntfy test helper.
* Wraps testNotification() while preserving the legacy function signature.
*/
export function testNtfyNotification(
config?: {
ntfyEnabled?: boolean;
ntfyTopic?: string;
ntfyBaseUrl?: string;
ntfyAccessToken?: string;
},
projectId?: string,
): Promise<{ success: boolean }> {
return testNotification("ntfy", config as Record<string, unknown> | undefined, projectId);
}

/** Pi extension settings from ~/.pi/agent/settings.json (global scope) */
export interface PiSettings {
packages: Array<string | { source: string; extensions?: string[]; skills?: string[]; prompts?: string[]; themes?: string[] }>;
extensions: string[];
skills: string[];
prompts: string[];
themes: string[];
}

/** Fetch pi extension settings (global scope from ~/.pi/agent/settings.json) */
export function fetchPiSettings(): Promise<PiSettings> {
return api<PiSettings>("/pi-settings");
}

/** Update pi extension settings (partial update, global scope) */
export async function updatePiSettings(settings: Partial<PiSettings>): Promise<{ success: boolean }> {
return api<{ success: boolean }>("/pi-settings", {
method: "PUT",
body: JSON.stringify(settings),
});
}

/** Install a new pi package source (adds to ~/.pi/agent/settings.json) */
export async function installPiPackage(source: string): Promise<{ success: boolean }> {
return api<{ success: boolean }>("/pi-settings/packages", {
method: "POST",
body: JSON.stringify({ source }),
});
}

/** Reinstall Fusion's bundled pi package and ensure it remains in global Pi settings. */
export async function reinstallFusionPiPackage(projectId?: string): Promise<{ success: boolean; source: string }> {
return api<{ success: boolean; source: string }>(withProjectId("/pi-settings/reinstall-fusion", projectId), {
method: "POST",
});
}

Loading
Loading