Files
fusion/packages/dashboard/app/components/settings/sections/GeneralSection.tsx
2026-06-08 07:53:42 -07:00

388 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Project General section (U9 / KTD-10).
*
* Project-scoped general settings: task prefix, default workflow, ephemeral
* agents, completion-documentation mode, quick-chat FAB, chat-history/mail/log
* retention, chat-room compaction tuning, capacity-risk banner, and GitHub
* tracking defaults. The prefix-validation error and the project tracking-repo
* options are owned by the shell (the prefix error gates Save; the repo options
* are fetched once) and relayed as props. Keys, validation regexes, and the
* cross-field summarizer hint are preserved verbatim from the original inline
* JSX.
*/
import { useEffect, useMemo, useState, type ReactNode } from "react";
import type { WorkflowDefinition } from "@fusion/core";
import { ProjectDefaultWorkflowField } from "../../WorkflowSelector";
import { TrackingRepoSelect, type TrackingRepoOption } from "../../TrackingRepoSelect";
import { fetchWorkflows } from "../../../api";
import type { ToastType } from "../../../hooks/useToast";
import type { SectionBaseProps } from "./context";
export interface GeneralSectionProps extends SectionBaseProps {
scopeBanner: ReactNode;
projectId?: string;
addToast: (message: string, type?: ToastType) => void;
prefixError: string | null;
setPrefixError: (value: string | null) => void;
projectTrackingRepoOptions: TrackingRepoOption[];
projectTrackingRepoLoading: boolean;
projectTrackingRepoError: string | null;
}
export function GeneralSection({
scopeBanner,
form,
setForm,
projectId,
addToast,
prefixError,
setPrefixError,
projectTrackingRepoOptions,
projectTrackingRepoLoading,
projectTrackingRepoError,
}: GeneralSectionProps) {
const [builtinWorkflows, setBuiltinWorkflows] = useState<WorkflowDefinition[]>([]);
useEffect(() => {
let cancelled = false;
fetchWorkflows(projectId, { includeDisabledBuiltins: true })
.then((workflows) => {
if (!cancelled) {
setBuiltinWorkflows(workflows.filter((workflow) => workflow.id.startsWith("builtin:")));
}
})
.catch(() => {
if (!cancelled) setBuiltinWorkflows([]);
});
return () => {
cancelled = true;
};
}, [projectId]);
const enabledBuiltinWorkflowIds = useMemo(() => {
const configured = Array.isArray(form.enabledBuiltinWorkflowIds) ? form.enabledBuiltinWorkflowIds : undefined;
return new Set(configured ?? builtinWorkflows.map((workflow) => workflow.id));
}, [builtinWorkflows, form.enabledBuiltinWorkflowIds]);
const setBuiltinWorkflowEnabled = (workflowId: string, enabled: boolean) => {
setForm((f) => {
const allIds = builtinWorkflows.map((workflow) => workflow.id);
const current = new Set(Array.isArray(f.enabledBuiltinWorkflowIds) ? f.enabledBuiltinWorkflowIds : allIds);
if (enabled) {
current.add(workflowId);
} else {
current.delete(workflowId);
}
const nextIds = allIds.filter((id) => current.has(id));
return {
...f,
enabledBuiltinWorkflowIds: nextIds.length === allIds.length ? undefined : nextIds,
};
});
};
return (
<>
{scopeBanner}
<h4 className="settings-section-heading">General</h4>
<div className="form-group">
<label htmlFor="taskPrefix">Task Prefix</label>
<input
id="taskPrefix"
type="text"
placeholder="FN"
value={form.taskPrefix || ""}
onChange={(e) => {
const val = e.target.value;
setForm((f) => ({ ...f, taskPrefix: val || undefined }));
if (val && !/^[A-Z]{1,10}$/.test(val)) {
setPrefixError("Prefix must be 1–10 uppercase letters");
} else {
setPrefixError(null);
}
}}
/>
{prefixError && <small className="field-error">{prefixError}</small>}
{!prefixError && <small>Prefix for new task IDs (e.g. KB, PROJ)</small>}
</div>
<div className="form-group">
<ProjectDefaultWorkflowField projectId={projectId} addToast={addToast} />
<small>New tasks inherit this custom workflow's steps (overridable per task)</small>
</div>
{builtinWorkflows.length > 0 && (
<div className="form-group">
<label>Built-in workflows</label>
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
{builtinWorkflows.map((workflow) => (
<label key={workflow.id} htmlFor={`builtin-workflow-${workflow.id}`} className="checkbox-label">
<input
id={`builtin-workflow-${workflow.id}`}
type="checkbox"
checked={enabledBuiltinWorkflowIds.has(workflow.id)}
onChange={(e) => setBuiltinWorkflowEnabled(workflow.id, e.target.checked)}
/>
<span>{workflow.name}</span>
</label>
))}
</div>
<small>Disabled built-in workflows are hidden from workflow pickers. Existing tasks that already use one continue to resolve.</small>
</div>
)}
<div className="form-group">
<label htmlFor="ephemeralAgentsEnabled" className="checkbox-label">
<input
id="ephemeralAgentsEnabled"
type="checkbox"
checked={form.ephemeralAgentsEnabled !== false}
onChange={(e) =>
setForm((f) => ({ ...f, ephemeralAgentsEnabled: e.target.checked }))
}
/>
Use ephemeral task-worker agents
</label>
<small>
When enabled (default), Fusion spawns short-lived <code>executor-FN-XXXX</code> agents to run each task. When disabled, only permanent agents execute tasks and the scheduler auto-assigns work using the agent reporting chain. Tasks with no eligible permanent agent stay queued.
</small>
</div>
<div className="form-group">
<label htmlFor="completionDocumentationMode">Completion Documentation Automation</label>
<select
id="completionDocumentationMode"
value={form.completionDocumentationMode || "off"}
onChange={(e) =>
setForm((f) => ({
...f,
completionDocumentationMode: e.target.value as "off" | "changeset" | "changelog",
}))
}
>
<option value="off">Off</option>
<option value="changeset">Require changeset (.changeset/*.md)</option>
<option value="changelog">Require changelog update (existing changelog)</option>
</select>
<small>
Controls how future task specs handle release-note artifacts at completion. Use changeset mode for repositories that follow
<code>.changeset</code> workflows, or changelog mode when contributors should update an existing changelog file.
</small>
</div>
<div className="form-group">
<label htmlFor="showQuickChatFAB" className="checkbox-label">
<input
id="showQuickChatFAB"
type="checkbox"
checked={form.showQuickChatFAB === true}
onChange={(e) =>
setForm((f) => ({ ...f, showQuickChatFAB: e.target.checked }))
}
/>
Show quick chat button
</label>
<small>Show the floating chat button in the dashboard. Chat is still accessible from the Chat tab in the mobile navigation.</small>
</div>
<h4 className="settings-section-heading settings-section-heading--spaced">Chat history</h4>
<div className="form-group">
<label htmlFor="chatAutoCleanupDays">Auto-cleanup old chats</label>
<select
id="chatAutoCleanupDays"
className="select"
value={form.chatAutoCleanupDays ?? 0}
onChange={(e) =>
setForm((f) => ({ ...f, chatAutoCleanupDays: Number(e.target.value) || 0 }))
}
>
<option value={0}>Off</option>
<option value={7}>7 days</option>
<option value={14}>14 days</option>
<option value={30}>30 days</option>
<option value={60}>60 days</option>
<option value={90}>90 days</option>
</select>
<small>Delete chat sessions and rooms that have been idle for this many days. Default: Off.</small>
</div>
<div className="form-group">
<label htmlFor="mailAutoCleanupDays">Auto-prune old mail</label>
<select
id="mailAutoCleanupDays"
className="select"
value={form.mailAutoCleanupDays ?? 0}
onChange={(e) =>
setForm((f) => ({ ...f, mailAutoCleanupDays: Number(e.target.value) || 0 }))
}
>
<option value={0}>Off</option>
<option value={7}>7 days</option>
<option value={14}>14 days</option>
<option value={30}>30 days</option>
<option value={60}>60 days</option>
<option value={90}>90 days</option>
</select>
<small>Delete inbox/outbox messages older than this many days. Default: Off. 7 days is the suggested setting.</small>
</div>
<div className="form-group">
<label htmlFor="operationalLogRetentionDays">Operational log retention</label>
<select
id="operationalLogRetentionDays"
className="select"
value={form.operationalLogRetentionDays ?? 30}
onChange={(e) =>
setForm((f) => ({ ...f, operationalLogRetentionDays: Number(e.target.value) || 0 }))
}
>
<option value={0}>Off</option>
<option value={7}>7 days</option>
<option value={14}>14 days</option>
<option value={30}>30 days</option>
<option value={60}>60 days</option>
<option value={90}>90 days</option>
</select>
<small>
Lowering this window means Reliability metrics/charts and the Activity feed will not show history older
than the selected range. Per-task task detail history is unaffected. Default: 30 days.
</small>
</div>
<h4 className="settings-section-heading settings-section-heading--spaced">Chat Rooms</h4>
<div className="form-group">
<label htmlFor="chatRoomRecentVerbatimMessages">Recent verbatim room messages</label>
<input
id="chatRoomRecentVerbatimMessages"
type="number"
min="1"
className="input"
placeholder="25"
value={form.chatRoomRecentVerbatimMessages ?? ""}
onChange={(e) =>
setForm((f) => ({ ...f, chatRoomRecentVerbatimMessages: Number(e.target.value) || undefined }))
}
/>
<small>Number of most-recent chat-room messages kept verbatim in the responder transcript. Older messages are compacted into a summary block. Default: 25.</small>
</div>
<div className="form-group">
<label htmlFor="chatRoomCompactionFetchLimit">Room compaction fetch limit</label>
<input
id="chatRoomCompactionFetchLimit"
type="number"
min="1"
className="input"
placeholder="200"
value={form.chatRoomCompactionFetchLimit ?? ""}
onChange={(e) =>
setForm((f) => ({ ...f, chatRoomCompactionFetchLimit: Number(e.target.value) || undefined }))
}
/>
<small>Upper bound on messages fetched from the room store for compaction consideration. Default: 200.</small>
</div>
<div className="form-group">
<label htmlFor="chatRoomSummaryMaxChars">Room summary max characters</label>
<input
id="chatRoomSummaryMaxChars"
type="number"
min="200"
className="input"
placeholder="3000"
value={form.chatRoomSummaryMaxChars ?? ""}
onChange={(e) =>
setForm((f) => ({ ...f, chatRoomSummaryMaxChars: Number(e.target.value) || undefined }))
}
/>
<small>Hard cap on the synthesized "Earlier room context" summary block. Default: 3000.</small>
</div>
<h4 className="settings-section-heading settings-section-heading--spaced">Capacity Risk Banner</h4>
<div className="form-group">
<label htmlFor="capacityRiskBannerEnabled" className="checkbox-label">
<input
id="capacityRiskBannerEnabled"
type="checkbox"
checked={form.capacityRiskBannerEnabled === true}
onChange={(e) =>
setForm((f) => ({ ...f, capacityRiskBannerEnabled: e.target.checked }))
}
/>
Show capacity risk banner
</label>
<small>Warn on the board when todo work exceeds the threshold and no idle agents are available.</small>
</div>
<div className="form-group">
<label htmlFor="capacityRiskTodoThresholdGeneral">Todo threshold</label>
<input
id="capacityRiskTodoThresholdGeneral"
type="number"
min={0}
className="input"
value={form.capacityRiskTodoThreshold ?? 20}
onChange={(e) =>
setForm((f) => ({
...f,
capacityRiskTodoThreshold:
e.target.value === ""
? 0
: Math.max(0, Number.parseInt(e.target.value, 10) || 0),
}))
}
/>
<small>Banner fires when todo count is strictly greater than this value (default 20). Applies when the banner is enabled.</small>
</div>
<h4 className="settings-section-heading settings-section-heading--spaced">GitHub Tracking</h4>
<div className="form-group">
<label htmlFor="githubTrackingMode">Default tracking mode for new tasks</label>
<select
id="githubTrackingMode"
className="select"
value={form.githubTrackingEnabledByDefault ? "new-tasks" : "off"}
onChange={(e) =>
setForm((f) => ({
...f,
githubTrackingEnabledByDefault: e.target.value === "new-tasks",
}))
}
>
<option value="off">Off (default)</option>
<option value="new-tasks">On for new tasks</option>
</select>
<small>
Controls whether newly created tasks have GitHub issue tracking enabled by default. Individual tasks can still override this from the task detail modal.
</small>
<small>
Tracking issues use this task&apos;s title. If a task has no title yet, Fusion can summarize its description using the title summarization model in Project Models.
{!form.autoSummarizeTitles && !form.useAiMergeCommitSummary && !form.githubTrackingEnabledByDefault
? " Enable summarization in Project Models to configure that model."
: ""}
</small>
</div>
<div className="form-group">
<label htmlFor="projectGithubTrackingDefaultRepoGeneral">Project default tracking repo</label>
<TrackingRepoSelect
id="projectGithubTrackingDefaultRepoGeneral"
ariaLabel="Project default tracking repo"
value={form.githubTrackingDefaultRepo ?? ""}
options={projectTrackingRepoOptions}
loading={projectTrackingRepoLoading}
error={projectTrackingRepoError ?? undefined}
placeholder="owner/repo"
onChange={(nextValue) =>
setForm((f) => ({ ...f, githubTrackingDefaultRepo: nextValue || undefined }))
}
/>
<small>Default repo used when creating GitHub issues for tracked tasks. Falls back to the global default if blank.</small>
</div>
<div className="form-group">
<label htmlFor="githubTrackingDedupEnabled" className="checkbox-label">
<input
id="githubTrackingDedupEnabled"
type="checkbox"
checked={form.githubTrackingDedupEnabled !== false}
onChange={(e) =>
setForm((f) => ({ ...f, githubTrackingDedupEnabled: e.target.checked }))
}
/>
Search the tracking repo for likely duplicates before opening a new issue
</label>
<small>
When enabled, Fusion checks open and closed issues in the target repo for likely duplicates (using File Scope paths and key symptoms) before creating a new tracking issue. Uncheck to always create a new issue.
</small>
</div>
</>
);
}
export default GeneralSection;