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:
5
.changeset/fn-4190-tracking-issue-ux-followup.md
Normal file
5
.changeset/fn-4190-tracking-issue-ux-followup.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Improve dashboard GitHub tracking UX by disabling the "Create tracking issue" action when a task has no usable title/description and showing clear helper text about when creation will occur. Also make the title summarization model more discoverable by surfacing it when GitHub tracking defaults are enabled and clarifying that the model is used for GitHub tracking issue title summarization.
|
||||
@@ -573,7 +573,7 @@ For post-merge prompt workflow steps, explicit step-level `modelProvider` + `mod
|
||||
|
||||
### Title summarization model
|
||||
|
||||
Used for task title auto-summarization and (when enabled) AI merge commit summaries.
|
||||
Used for task title auto-summarization, GitHub tracking issue title summarization when tasks are untitled, and (when enabled) AI merge commit summaries.
|
||||
|
||||
1. Project `titleSummarizerProvider` + `titleSummarizerModelId`
|
||||
2. Global `titleSummarizerGlobalProvider` + `titleSummarizerGlobalModelId`
|
||||
|
||||
@@ -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