## Summary Wave 11 of package code organization (continues #2328 / wave 10), after syncing main into the wave10 stack. ### Peels | New module | Parent | |---|---| | `app/api/event-source.ts` | resilient `EventSource` reconnect + pagehide cleanup | | `app/api/chat.ts` | chat sessions / rooms / streaming | | `app/api/projects.ts` | multi-project management client | | `app/api/agents.ts` | agent CRUD / soul / memory | | `app/api/workflows.ts` | workflow definition client | | `app/api/scheduling.ts` | automations + routines | | `app/api/ai-text.ts` | text refine / import translate / subtasks | | `app/api/ai-sessions.ts` | AI / planning session client + `startKeepAlive` | | `app/api/research.ts` | research + evals client | | `task-store/branch-and-pr-entities.ts` | rename of `remaining-ops-6` | Public paths stay stable via `legacy.ts` re-exports (`app/api.ts` → `legacy`). ### LOC - `legacy.ts` ~8921 → ~5670 ### Shims - `legacy.ts` → peels above (delete-when: dashboard imports domain modules) - `remaining-ops-6` → `branch-and-pr-entities` (rename complete) ## Test plan - [x] eslint on peeled API modules + legacy - [x] dashboard app typecheck (`app/api/*` clean) - [x] core typecheck - [x] `plugin-setup-api` tests - [ ] CI merge gate <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Expanded dashboard APIs for agents, AI sessions, chat rooms/sessions, research runs, projects, workflows, routines/automations, and scheduling. * Added AI text refinement/translation, planning title regeneration, and streamed subtask breakdown with task creation. * Introduced resilient server-sent-events streaming for chat and other live updates. * Added workflow export/import and workflow design support, plus scripts and workflow configuration controls. * Added research export and ability to attach research outputs to tasks. * **Enhancements** * Missions support clearing an auto-merge override back to project defaults. * Subtasks can now include an optional priority when creating tasks. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
397 lines
16 KiB
TypeScript
397 lines
16 KiB
TypeScript
/**
|
|
* FNXC:CodeOrganization 2026-07-19-12:00:
|
|
* Workflow definition client API peeled from legacy.ts.
|
|
*/
|
|
import { api } from "./client.js";
|
|
import type { FetchOptions } from "./client.js";
|
|
import { withProjectId } from "./health.js";
|
|
import { dedupe } from "./dedupe.js";
|
|
import { notifyWorkflowSettingValuesUpdated } from "../utils/workflowSettingValuesEvents.js";
|
|
|
|
// ── Workflow definitions (graph-authored custom workflows) ───────────────
|
|
|
|
export type {
|
|
WorkflowDefinition,
|
|
WorkflowDefinitionInput,
|
|
WorkflowDefinitionUpdate,
|
|
WorkflowIr,
|
|
} from "@fusion/core";
|
|
|
|
/** List all workflow definitions for the project. */
|
|
export function fetchWorkflows(projectId?: string, options?: { includeDisabledBuiltins?: boolean } & FetchOptions): Promise<import("@fusion/core").WorkflowDefinition[]> {
|
|
const query = options?.includeDisabledBuiltins ? "?includeDisabledBuiltins=true" : "";
|
|
const path = withProjectId(`/workflows${query}`, projectId);
|
|
return dedupe(path, () => api<import("@fusion/core").WorkflowDefinition[]>(path), options);
|
|
}
|
|
|
|
/** A trait catalog entry as returned by GET /api/traits (U10). Mirrors the
|
|
* registry's TraitDefinition projection (flags + hook descriptors + schema). */
|
|
export interface TraitCatalogEntry {
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
builtin: boolean;
|
|
flags: import("@fusion/core").TraitFlags;
|
|
hooks?: import("@fusion/core").TraitHookDescriptors;
|
|
configSchema?: import("@fusion/core").TraitConfigSchema;
|
|
}
|
|
|
|
/** Fetch the trait catalog (built-ins + registered plugin traits) for the
|
|
* workflow editor's trait picker. Registry-backed, read-only, session-scoped. */
|
|
export function fetchTraits(projectId?: string): Promise<TraitCatalogEntry[]> {
|
|
const path = withProjectId("/traits", projectId);
|
|
return dedupe(path, () =>
|
|
api<{ traits: TraitCatalogEntry[] }>(path).then((res) => res.traits),
|
|
);
|
|
}
|
|
|
|
/** Fetch the step-parser id catalog (built-ins + registered plugin parsers) for
|
|
* the parse-steps node inspector (KTD-12). Registry-backed, read-only,
|
|
* session-scoped. Mirrors fetchTraits. */
|
|
export function fetchStepParsers(projectId?: string): Promise<string[]> {
|
|
const path = withProjectId("/step-parsers", projectId);
|
|
return dedupe(path, () =>
|
|
api<{ parsers: Array<{ id: string }> }>(path).then((res) => res.parsers.map((p) => p.id)),
|
|
);
|
|
}
|
|
|
|
/** Fetch a single workflow definition. */
|
|
export function fetchWorkflow(id: string, projectId?: string): Promise<import("@fusion/core").WorkflowDefinition> {
|
|
return api<import("@fusion/core").WorkflowDefinition>(withProjectId(`/workflows/${encodeURIComponent(id)}`, projectId));
|
|
}
|
|
|
|
/** Fetch resolved optional step declarations for a workflow. */
|
|
export function fetchWorkflowOptionalSteps(
|
|
workflowId: string,
|
|
projectId?: string,
|
|
): Promise<import("@fusion/core").ResolvedWorkflowOptionalStep[]> {
|
|
return api<import("@fusion/core").ResolvedWorkflowOptionalStep[]>(
|
|
withProjectId(`/workflows/${encodeURIComponent(workflowId)}/optional-steps`, projectId),
|
|
);
|
|
}
|
|
|
|
/** Create a workflow definition. */
|
|
export function createWorkflow(
|
|
input: import("@fusion/core").WorkflowDefinitionInput,
|
|
projectId?: string,
|
|
): Promise<import("@fusion/core").WorkflowDefinition> {
|
|
return api<import("@fusion/core").WorkflowDefinition>(withProjectId("/workflows", projectId), {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/** Update a workflow definition (partial). */
|
|
export function updateWorkflow(
|
|
id: string,
|
|
updates: import("@fusion/core").WorkflowDefinitionUpdate,
|
|
projectId?: string,
|
|
): Promise<import("@fusion/core").WorkflowDefinition> {
|
|
return api<import("@fusion/core").WorkflowDefinition>(withProjectId(`/workflows/${encodeURIComponent(id)}`, projectId), {
|
|
method: "PATCH",
|
|
body: JSON.stringify(updates),
|
|
});
|
|
}
|
|
|
|
/** Delete a workflow definition. */
|
|
export function deleteWorkflow(id: string, projectId?: string): Promise<void> {
|
|
return api<void>(withProjectId(`/workflows/${encodeURIComponent(id)}`, projectId), { method: "DELETE" });
|
|
}
|
|
|
|
/** The per-`(workflowId, project)` setting-value payload returned by the
|
|
* workflow setting-value endpoints (U6/R5): the raw `stored` map, the
|
|
* `effective` map (stored ?? declaration default, drop-on-orphan), and the
|
|
* `orphaned` stored entries that no longer validate against the declarations. */
|
|
export interface WorkflowSettingValuesPayload {
|
|
stored: Record<string, unknown>;
|
|
effective: Record<string, unknown>;
|
|
orphaned: Array<{ id: string; value: unknown }>;
|
|
}
|
|
|
|
/** Per-project workflow prompt override payload. `defaults` is the shipped prompt
|
|
* by node id, `stored` is the persisted override map, and `effective` is the
|
|
* prompt text the editor/executor sees after stored-over-default resolution. */
|
|
export interface WorkflowPromptOverridesPayload {
|
|
stored: Record<string, string>;
|
|
effective: Record<string, string>;
|
|
defaults: Record<string, string>;
|
|
}
|
|
|
|
/** Read the setting VALUES (stored/effective/orphaned) for a workflow in the
|
|
* current project context (U6). The project is bound server-side to the
|
|
* scoped store. */
|
|
export function fetchWorkflowSettingValues(
|
|
id: string,
|
|
projectId?: string,
|
|
): Promise<WorkflowSettingValuesPayload> {
|
|
return api<WorkflowSettingValuesPayload>(
|
|
withProjectId(`/workflows/${encodeURIComponent(id)}/setting-values`, projectId),
|
|
);
|
|
}
|
|
|
|
/** Write setting VALUES for a workflow in the current project context (U6). The
|
|
* `values` map is validated against the named workflow's declarations; a `null`
|
|
* value deletes that key. A typed rejection surfaces as an ApiRequestError with
|
|
* `status: 400` and `details.rejections: WorkflowSettingRejection[]`. */
|
|
export async function updateWorkflowSettingValues(
|
|
id: string,
|
|
values: Record<string, unknown>,
|
|
projectId?: string,
|
|
): Promise<WorkflowSettingValuesPayload> {
|
|
const payload = await api<WorkflowSettingValuesPayload>(
|
|
withProjectId(`/workflows/${encodeURIComponent(id)}/setting-values`, projectId),
|
|
{
|
|
method: "PATCH",
|
|
body: JSON.stringify({ values }),
|
|
},
|
|
);
|
|
if (Object.prototype.hasOwnProperty.call(values, "plannerOversightLevel")) {
|
|
notifyWorkflowSettingValuesUpdated(id, projectId);
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
/** Read per-node prompt overrides for a workflow in the current project context. */
|
|
export function fetchWorkflowPromptOverrides(
|
|
id: string,
|
|
projectId?: string,
|
|
): Promise<WorkflowPromptOverridesPayload> {
|
|
return api<WorkflowPromptOverridesPayload>(
|
|
withProjectId(`/workflows/${encodeURIComponent(id)}/prompt-overrides`, projectId),
|
|
);
|
|
}
|
|
|
|
/** Patch per-node prompt overrides. Null, empty, and whitespace values reset to the shipped default. */
|
|
export function updateWorkflowPromptOverrides(
|
|
id: string,
|
|
overrides: Record<string, string | null>,
|
|
projectId?: string,
|
|
): Promise<WorkflowPromptOverridesPayload> {
|
|
return api<WorkflowPromptOverridesPayload>(
|
|
withProjectId(`/workflows/${encodeURIComponent(id)}/prompt-overrides`, projectId),
|
|
{
|
|
method: "PATCH",
|
|
body: JSON.stringify({ overrides }),
|
|
},
|
|
);
|
|
}
|
|
|
|
/** A workflow export envelope (U5/R9/KTD-5). `schemaVersion` is the SERVER's
|
|
* schema version at export time — the import route version-gates against it
|
|
* (the app build aliases @fusion/core to types-only, so the value can only come
|
|
* from the server, never an app-side core import).
|
|
*
|
|
* FNXC:WorkflowPortability 2026-06-30-00:00:
|
|
* Dashboard downloads must carry project-scoped setting values and prompt overrides with the workflow graph so the same shared API path supports portable desktop and mobile Workflow Editor imports.
|
|
*/
|
|
export interface WorkflowExportEnvelope {
|
|
fusionWorkflowExport: 1;
|
|
schemaVersion: number;
|
|
kind: import("@fusion/core").WorkflowDefinition["kind"];
|
|
name: string;
|
|
description: string;
|
|
ir: import("@fusion/core").WorkflowIr;
|
|
layout: import("@fusion/core").WorkflowDefinition["layout"];
|
|
settingValues: Record<string, unknown>;
|
|
promptOverrides: Record<string, string>;
|
|
}
|
|
|
|
/** Fetch a workflow's export envelope and trigger a browser download as
|
|
* `<name>.workflow.json` (U5/R9). Built-ins are exportable too. Mirrors the
|
|
* SettingsModal export pattern (Blob + createObjectURL + a.download). */
|
|
export async function exportWorkflow(id: string, projectId?: string): Promise<WorkflowExportEnvelope> {
|
|
const envelope = await api<WorkflowExportEnvelope>(
|
|
withProjectId(`/workflows/${encodeURIComponent(id)}/export`, projectId),
|
|
);
|
|
const safeName = (envelope.name || "workflow").replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "") || "workflow";
|
|
const blob = new Blob([JSON.stringify(envelope, null, 2)], { type: "application/json" });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = `${safeName}.workflow.json`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
return envelope;
|
|
}
|
|
|
|
/** Result of POST /api/workflows/import (U5/R10). `strippedApprovalFlags` is set
|
|
* when `cliSkipApproval`/`autoApprove` were removed from any node config at the
|
|
* trust boundary; `warnings` lists non-blocking issues (e.g. unknown scriptName). */
|
|
export interface ImportWorkflowResult {
|
|
workflow: import("@fusion/core").WorkflowDefinition;
|
|
strippedApprovalFlags: boolean;
|
|
warnings: string[];
|
|
settingValues: Record<string, unknown>;
|
|
promptOverrides: Record<string, string>;
|
|
}
|
|
|
|
/** Import a workflow export envelope (U5/R10). The server is the sole validator;
|
|
* validation failures reject with an ApiError carrying the server message. */
|
|
export function importWorkflow(
|
|
envelope: unknown,
|
|
projectId?: string,
|
|
): Promise<ImportWorkflowResult> {
|
|
return api<ImportWorkflowResult>(withProjectId("/workflows/import", projectId), {
|
|
method: "POST",
|
|
body: JSON.stringify(envelope),
|
|
});
|
|
}
|
|
|
|
// FNXC:WorkflowStepCRUD 2026-06-26-14:00: U7c removed migrateLegacyWorkflowSteps and
|
|
// MigrateLegacyStepsResult along with the legacy workflow_steps table and its route.
|
|
|
|
/** Result of POST /api/workflows/design (U10/R11). The server validates the
|
|
* AI-produced IR (parseWorkflowIr) and strips trust-escalating flags
|
|
* (`strippedApprovalFlags`). Persists nothing — the client decides what to do
|
|
* with the returned graph. */
|
|
export interface DesignWorkflowResult {
|
|
ir: import("@fusion/core").WorkflowIr;
|
|
layout: import("@fusion/core").WorkflowDefinition["layout"];
|
|
strippedApprovalFlags: boolean;
|
|
}
|
|
|
|
/** Design a workflow from a natural-language prompt (U10/R11). When `workflowId`
|
|
* is supplied the route reads that workflow's persisted IR server-side and folds
|
|
* it into the prompt as the base graph (the client never posts IR). An optional
|
|
* AbortSignal cancels the in-flight request. Validation failures reject with an
|
|
* ApiError carrying the server message; 429 on rate limit. */
|
|
export function designWorkflow(
|
|
input: { prompt: string; workflowId?: string },
|
|
projectId?: string,
|
|
signal?: AbortSignal,
|
|
): Promise<DesignWorkflowResult> {
|
|
return api<DesignWorkflowResult>(withProjectId("/workflows/design", projectId), {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
signal,
|
|
});
|
|
}
|
|
|
|
/** Read the workflow currently selected for a task. */
|
|
export function fetchTaskWorkflow(taskId: string, projectId?: string): Promise<{ workflowId: string | null; enabledWorkflowSteps?: string[] | null }> {
|
|
return api<{ workflowId: string | null; enabledWorkflowSteps?: string[] | null }>(
|
|
withProjectId(`/tasks/${encodeURIComponent(taskId)}/workflow`, projectId),
|
|
);
|
|
}
|
|
|
|
/** Select (or clear, with null) a workflow for a task. Returns the resulting
|
|
* enabled step ids so callers can reflect the change without a refetch. */
|
|
export function selectTaskWorkflow(
|
|
taskId: string,
|
|
workflowId: string | null,
|
|
projectId?: string,
|
|
): Promise<{
|
|
workflowId: string | null;
|
|
enabledWorkflowSteps: string[];
|
|
// U5 (R20): present (flag ON) when the switch re-homed the card; `preserved`
|
|
// false means the card moved columns and the board needs a refresh.
|
|
reconciliation?: { preserved: boolean; fromColumn: string; toColumn: string };
|
|
}> {
|
|
return api<{
|
|
workflowId: string | null;
|
|
enabledWorkflowSteps: string[];
|
|
reconciliation?: { preserved: boolean; fromColumn: string; toColumn: string };
|
|
}>(
|
|
withProjectId(`/tasks/${encodeURIComponent(taskId)}/workflow`, projectId),
|
|
{
|
|
method: "PUT",
|
|
body: JSON.stringify({ workflowId }),
|
|
},
|
|
);
|
|
}
|
|
|
|
/** Approve the raw CLI command a task is paused on, and resume it. */
|
|
export function approveTaskWorkflowCli(taskId: string, projectId?: string): Promise<{ approved: string }> {
|
|
return api<{ approved: string }>(withProjectId(`/tasks/${encodeURIComponent(taskId)}/workflow/approve-cli`, projectId), {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
/** Submit the user's answer to an await-input node and resume the task. */
|
|
export function submitTaskWorkflowInput(taskId: string, text: string, projectId?: string): Promise<{ ok: true }> {
|
|
return api<{ ok: true }>(withProjectId(`/tasks/${encodeURIComponent(taskId)}/workflow/input`, projectId), {
|
|
method: "POST",
|
|
body: JSON.stringify({ text }),
|
|
});
|
|
}
|
|
|
|
/** Read the project default workflow. */
|
|
export function fetchProjectDefaultWorkflow(projectId?: string): Promise<{ workflowId: string | null }> {
|
|
return api<{ workflowId: string | null }>(withProjectId("/project/default-workflow", projectId));
|
|
}
|
|
|
|
/** Set (or clear, with null) the project default workflow. */
|
|
export function setProjectDefaultWorkflow(
|
|
workflowId: string | null,
|
|
projectId?: string,
|
|
): Promise<{ workflowId: string | null }> {
|
|
return api<{ workflowId: string | null }>(withProjectId("/project/default-workflow", projectId), {
|
|
method: "PUT",
|
|
body: JSON.stringify({ workflowId }),
|
|
});
|
|
}
|
|
|
|
// ── Workflow Step Templates ──────────────────────────────────────────────
|
|
|
|
/** Re-export WorkflowStepTemplate type from core */
|
|
export type { WorkflowStepTemplate } from "@fusion/core";
|
|
|
|
/** Fetch the workflow step templates that feed the editor palette. The built-in
|
|
* built-in step-template catalog was deleted in U6, so this now returns only
|
|
* plugin-contributed templates. */
|
|
export function fetchWorkflowStepTemplates(): Promise<{ templates: import("@fusion/core").WorkflowStepTemplate[] }> {
|
|
return api<{ templates: import("@fusion/core").WorkflowStepTemplate[] }>("/workflow-step-templates");
|
|
}
|
|
|
|
/** Fetch plugin-contributed workflow step templates */
|
|
export function fetchPluginWorkflowStepTemplates(): Promise<{
|
|
templates: Array<{ pluginId: string; template: import("@fusion/core").WorkflowStepTemplate }>;
|
|
}> {
|
|
return api<{
|
|
templates: Array<{ pluginId: string; template: import("@fusion/core").WorkflowStepTemplate }>;
|
|
}>("/plugin-workflow-step-templates");
|
|
}
|
|
|
|
// ── Scripts API ────────────────────────────────────────────────────────
|
|
|
|
/** Script entry returned from the API */
|
|
export interface ScriptEntry {
|
|
name: string;
|
|
command: string;
|
|
}
|
|
|
|
/** Result of running a script via POST /api/scripts/:name/run */
|
|
export interface ScriptRunResult {
|
|
sessionId: string;
|
|
command: string;
|
|
}
|
|
|
|
/** Fetch all saved scripts from project settings */
|
|
export function fetchScripts(projectId?: string): Promise<Record<string, string>> {
|
|
return api<Record<string, string>>(withProjectId("/scripts", projectId));
|
|
}
|
|
|
|
/** Add or update a script */
|
|
export function addScript(name: string, command: string, projectId?: string): Promise<ScriptEntry> {
|
|
return api<ScriptEntry>(withProjectId("/scripts", projectId), {
|
|
method: "POST",
|
|
body: JSON.stringify({ name, command }),
|
|
});
|
|
}
|
|
|
|
/** Remove a script by name */
|
|
export function removeScript(name: string, projectId?: string): Promise<void> {
|
|
return api<void>(withProjectId(`/scripts/${encodeURIComponent(name)}`, projectId), { method: "DELETE" });
|
|
}
|
|
|
|
/** Run a saved script by name */
|
|
export function runScript(name: string, args?: string[], projectId?: string): Promise<ScriptRunResult> {
|
|
return api<ScriptRunResult>(withProjectId(`/scripts/${encodeURIComponent(name)}/run`, projectId), {
|
|
method: "POST",
|
|
body: JSON.stringify({ args }),
|
|
});
|
|
}
|
|
|