feat(FN-4190): complete Step 7 — tracking UX and summarization discoverability
Fusion-Task-Id: FN-4190 Fusion-Task-Lineage: 5d94a4ad-32fb-4d20-8d04-5d757619f104
This commit is contained in:
@@ -2046,6 +2046,12 @@ export function SettingsModal({
|
||||
<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's title. If a task has no title yet, Fusion can summarize its description using the title summarization model configured above.
|
||||
{!form.autoSummarizeTitles && !form.useAiMergeCommitSummary && !form.githubTrackingEnabledByDefault
|
||||
? " Enable summarization above to configure that model."
|
||||
: ""}
|
||||
</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="projectGithubTrackingDefaultRepoGeneral">Project default tracking repo</label>
|
||||
@@ -2837,7 +2843,8 @@ export function SettingsModal({
|
||||
When enabled, tasks created without a title but with descriptions over 200 characters
|
||||
will automatically get an AI-generated title (max 60 characters). The same model is
|
||||
also used to generate fallback merge commit message bodies when the branch's commit
|
||||
log is empty (e.g. squash merges with no unique commits).
|
||||
log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue
|
||||
titles when a tracked task has no title yet.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -2856,10 +2863,10 @@ export function SettingsModal({
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{(form.autoSummarizeTitles || form.useAiMergeCommitSummary || false) && (
|
||||
{(form.autoSummarizeTitles || form.useAiMergeCommitSummary || form.githubTrackingEnabledByDefault || false) && (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label>Title and commit message summarization model</label>
|
||||
<label>Title, commit message, and GitHub tracking issue summarization model</label>
|
||||
{modelsLoading ? (
|
||||
<small>Loading available models...</small>
|
||||
) : availableModels.length === 0 ? (
|
||||
@@ -2867,7 +2874,7 @@ export function SettingsModal({
|
||||
) : (
|
||||
<CustomModelDropdown
|
||||
id="titleSummarizerModel"
|
||||
label="Title and commit message summarization model"
|
||||
label="Title, commit message, and GitHub tracking issue summarization model"
|
||||
models={availableModels}
|
||||
value={
|
||||
form.titleSummarizerProvider && form.titleSummarizerModelId
|
||||
@@ -2897,6 +2904,9 @@ export function SettingsModal({
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
/>
|
||||
)}
|
||||
<small>
|
||||
Also used to summarize task descriptions into GitHub tracking issue titles when a task has no title yet.
|
||||
</small>
|
||||
<small>
|
||||
{form.titleSummarizerProvider && form.titleSummarizerModelId
|
||||
? "Using explicitly configured model"
|
||||
|
||||
@@ -95,6 +95,19 @@ function extractReviewerModelFromLog(entries: AgentLogEntry[]): { provider: stri
|
||||
return result;
|
||||
}
|
||||
|
||||
function hasUsableTrackingTitle(task: { title?: string | null; description?: string | null }): boolean {
|
||||
if ((task.title ?? "").trim().length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const firstMeaningfulLine = (task.description ?? "")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.find((line) => line.length > 0);
|
||||
|
||||
return Boolean(firstMeaningfulLine);
|
||||
}
|
||||
|
||||
function extractAssignedRuntimeModel(agent: Agent | null | undefined): ModelSelection {
|
||||
const runtimeConfig = (agent?.runtimeConfig ?? undefined) as Record<string, unknown> | undefined;
|
||||
const model = typeof runtimeConfig?.model === "string" ? runtimeConfig.model.trim() : "";
|
||||
@@ -813,6 +826,7 @@ export function TaskDetailContent({
|
||||
const githubTrackingEnabled = githubTrackingEnabledDraft ?? (workingTask.githubTracking?.enabled === true);
|
||||
const githubTrackedIssue = workingTask.githubTracking?.issue;
|
||||
const githubTrackingDetailPending = detailLoading && typeof task.githubTracking === "undefined";
|
||||
const canCreateTrackingIssue = hasUsableTrackingTitle(task);
|
||||
const showInlineGithubTrackingEnableButton =
|
||||
canEditGithubTracking
|
||||
&& !githubTrackedIssue
|
||||
@@ -874,6 +888,10 @@ export function TaskDetailContent({
|
||||
|
||||
const handleRetryGithubTrackingIssueCreate = useCallback(async () => {
|
||||
if (!githubTrackingEnabled || githubTrackedIssue || isSavingGithubTracking) return;
|
||||
if (!hasUsableTrackingTitle(task)) {
|
||||
addToast("Add a title before creating a tracking issue", "info");
|
||||
return;
|
||||
}
|
||||
setIsSavingGithubTracking(true);
|
||||
try {
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
@@ -888,7 +906,7 @@ export function TaskDetailContent({
|
||||
} finally {
|
||||
if (mountedRef.current) setIsSavingGithubTracking(false);
|
||||
}
|
||||
}, [addToast, githubTrackedIssue, githubTrackingEnabled, isSavingGithubTracking, onTaskUpdated, projectId, task.id]);
|
||||
}, [addToast, githubTrackedIssue, githubTrackingEnabled, isSavingGithubTracking, onTaskUpdated, projectId, task]);
|
||||
|
||||
const enterEditMode = useCallback(() => {
|
||||
if (!canEdit) return;
|
||||
@@ -2494,9 +2512,19 @@ export function TaskDetailContent({
|
||||
)}
|
||||
<div className="detail-github-tracking-controls">
|
||||
{!githubTrackedIssue && githubTrackingEnabled && (
|
||||
<button className="btn btn-sm touch-target" onClick={() => void handleRetryGithubTrackingIssueCreate()} disabled={isSavingGithubTracking}>
|
||||
Create tracking issue
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm touch-target"
|
||||
onClick={() => void handleRetryGithubTrackingIssueCreate()}
|
||||
disabled={isSavingGithubTracking || !canCreateTrackingIssue}
|
||||
title={!canCreateTrackingIssue ? "Add a title or description so a tracking issue can be created." : undefined}
|
||||
>
|
||||
Create tracking issue
|
||||
</button>
|
||||
{!canCreateTrackingIssue && (
|
||||
<small className="detail-source-empty">Tracking issue will be created once this task has a title.</small>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{canEditGithubTracking && (
|
||||
<>
|
||||
|
||||
@@ -793,6 +793,38 @@ describe("SettingsModal", () => {
|
||||
expect(payload.githubTrackingEnabledByDefault).toBe(false);
|
||||
expect(payload.githubTrackingDefaultRepo).toBeUndefined();
|
||||
});
|
||||
|
||||
it("hides summarization model picker when summarization and default tracking are disabled", async () => {
|
||||
renderModal({ initialSection: "models" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Project Models" }));
|
||||
|
||||
expect(screen.queryByText("Title, commit message, and GitHub tracking issue summarization model")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows summarization model picker for GitHub tracking defaults", async () => {
|
||||
mockFetchSettings.mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
githubTrackingEnabledByDefault: true,
|
||||
});
|
||||
|
||||
renderModal({ initialSection: "models" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Project Models" }));
|
||||
|
||||
expect(screen.getByText("Title, commit message, and GitHub tracking issue summarization model")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("always shows GitHub tracking summarization helper copy", async () => {
|
||||
renderModal({ initialSection: "general" });
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(
|
||||
screen.getByText(/Tracking issues use this task's title\. If a task has no title yet, Fusion can summarize its description using the title summarization model configured above\./),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Appearance", () => {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import {
|
||||
makeTask,
|
||||
noop,
|
||||
noopDelete,
|
||||
noopMerge,
|
||||
noopMove,
|
||||
noopOpenDetail,
|
||||
setupTaskDetailModalHooks,
|
||||
} from "./TaskDetailModal.test-helpers";
|
||||
import { TaskDetailModal } from "../TaskDetailModal";
|
||||
|
||||
setupTaskDetailModalHooks();
|
||||
|
||||
describe("TaskDetailModal GitHub tracking CTA", () => {
|
||||
it("disables create tracking issue when task has no usable title", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
githubTracking: { enabled: true },
|
||||
title: "",
|
||||
description: "",
|
||||
})}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Expand GitHub tracking details" }));
|
||||
const button = screen.getByRole("button", { name: "Create tracking issue" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveAttribute("title", "Add a title or description so a tracking issue can be created.");
|
||||
expect(screen.getByText("Tracking issue will be created once this task has a title.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("enables create tracking issue when task title is present", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
githubTracking: { enabled: true },
|
||||
title: "Real title",
|
||||
description: "",
|
||||
})}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Expand GitHub tracking details" }));
|
||||
expect(screen.getByRole("button", { name: "Create tracking issue" })).toBeEnabled();
|
||||
expect(screen.queryByText("Tracking issue will be created once this task has a title.")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("enables create tracking issue when task description has a non-empty first line", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
githubTracking: { enabled: true },
|
||||
title: "",
|
||||
description: "A meaningful first line.\nMore text.",
|
||||
})}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Expand GitHub tracking details" }));
|
||||
expect(screen.getByRole("button", { name: "Create tracking issue" })).toBeEnabled();
|
||||
expect(screen.queryByText("Tracking issue will be created once this task has a title.")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user