feat(FN-4727): add github tracking toggle to quick entry box

Adds a GitHub tracking toggle to the QuickEntryBox component with co-located tests, shipped as a patch to `@runfusion/fusion`.

Fusion-Task-Id: FN-4727
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 08:28:00 -07:00
committed by gsxdsm
parent b61db63661
commit e9403df6ba
3 changed files with 145 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Add GitHub tracking indicator/toggle to the quick task entry dropdown — shows project default and overrides for the next task.

View File

@@ -12,6 +12,7 @@ import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/project
import { useNodes } from "../hooks/useNodes"; import { useNodes } from "../hooks/useNodes";
import type { NodeInfo } from "../api"; import type { NodeInfo } from "../api";
import { NodeHealthDot } from "./NodeHealthDot"; import { NodeHealthDot } from "./NodeHealthDot";
import { ProviderIcon } from "./ProviderIcon";
const STORAGE_KEY = "kb-quick-entry-text"; const STORAGE_KEY = "kb-quick-entry-text";
const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]; const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
@@ -157,6 +158,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
const [settings, setSettings] = useState<Settings | null>(null); const [settings, setSettings] = useState<Settings | null>(null);
const [selectedPresetId, setSelectedPresetId] = useState<string | undefined>(undefined); const [selectedPresetId, setSelectedPresetId] = useState<string | undefined>(undefined);
const [isFastMode, setIsFastMode] = useState(false); const [isFastMode, setIsFastMode] = useState(false);
const [githubTrackingOverride, setGithubTrackingOverride] = useState<boolean | null>(null);
const [priority, setPriority] = useState<TaskPriority>(DEFAULT_TASK_PRIORITY); const [priority, setPriority] = useState<TaskPriority>(DEFAULT_TASK_PRIORITY);
const [nodeId, setNodeId] = useState<string | undefined>(undefined); const [nodeId, setNodeId] = useState<string | undefined>(undefined);
const { nodes } = useNodes(); const { nodes } = useNodes();
@@ -435,6 +437,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
setPlanningModelId(undefined); setPlanningModelId(undefined);
setSelectedPresetId(undefined); setSelectedPresetId(undefined);
setIsFastMode(false); setIsFastMode(false);
setGithubTrackingOverride(null);
setPriority(DEFAULT_TASK_PRIORITY); setPriority(DEFAULT_TASK_PRIORITY);
setNodeId(undefined); setNodeId(undefined);
setShowDeps(false); setShowDeps(false);
@@ -508,6 +511,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
planningModelProvider: hasPlanningOverride ? planningProvider : undefined, planningModelProvider: hasPlanningOverride ? planningProvider : undefined,
planningModelId: hasPlanningOverride ? planningModelId : undefined, planningModelId: hasPlanningOverride ? planningModelId : undefined,
...(isFastMode ? { executionMode: "fast" } : {}), ...(isFastMode ? { executionMode: "fast" } : {}),
githubTracking: githubTrackingOverride !== null ? { enabled: githubTrackingOverride } : undefined,
priority, priority,
nodeId, nodeId,
}); });
@@ -555,6 +559,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
addToast, addToast,
resetForm, resetForm,
isFastMode, isFastMode,
githubTrackingOverride,
priority, priority,
nodeId, nodeId,
]); ]);
@@ -1325,6 +1330,11 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
const selectedAgent = selectedAgentId ? agents.find((agent) => agent.id === selectedAgentId) : undefined; const selectedAgent = selectedAgentId ? agents.find((agent) => agent.id === selectedAgentId) : undefined;
const selectedAgentLabel = selectedAgent?.name ?? selectedAgentId; const selectedAgentLabel = selectedAgent?.name ?? selectedAgentId;
const selectedNode = nodeId ? nodes.find((node) => node.id === nodeId) : undefined; const selectedNode = nodeId ? nodes.find((node) => node.id === nodeId) : undefined;
const projectGithubTrackingDefault = settings?.githubTrackingEnabledByDefault === true;
const effectiveGithubTracking = githubTrackingOverride ?? projectGithubTrackingDefault;
const githubToggleLabel = effectiveGithubTracking
? `GitHub tracking ON for next task (project default: ${projectGithubTrackingDefault ? "on" : "off"})`
: "GitHub tracking OFF for next task";
// Show expanded controls based on disclosure state (user preference), not textarea focus // Show expanded controls based on disclosure state (user preference), not textarea focus
const showExpandedControls = isDisclosureExpanded; const showExpandedControls = isDisclosureExpanded;
@@ -1835,6 +1845,19 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
Fast Fast
</button> </button>
<button
type="button"
className={`btn btn-sm ${effectiveGithubTracking ? "btn-primary" : ""}`}
onClick={() => setGithubTrackingOverride(!effectiveGithubTracking)}
onMouseDown={(e) => e.preventDefault()}
aria-pressed={effectiveGithubTracking}
data-testid="quick-entry-github-toggle"
title={githubToggleLabel}
aria-label={githubToggleLabel}
>
<ProviderIcon provider="github" size="sm" />
</button>
<button <button
type="button" type="button"
className="btn btn-task-create btn-sm" className="btn btn-task-create btn-sm"

View File

@@ -134,6 +134,7 @@ vi.mock("lucide-react", () => ({
Bot: () => null, Bot: () => null,
Server: () => null, Server: () => null,
Flag: () => null, Flag: () => null,
Github: () => null,
Maximize2: () => null, Maximize2: () => null,
Minimize2: () => null, Minimize2: () => null,
})); }));
@@ -756,6 +757,57 @@ describe("QuickEntryBox", () => {
expect(fastToggle.getAttribute("aria-pressed")).toBe("false"); expect(fastToggle.getAttribute("aria-pressed")).toBe("false");
}); });
it("renders GitHub toggle off by default when project setting is disabled", async () => {
vi.mocked(fetchSettings).mockResolvedValueOnce({
githubTrackingEnabledByDefault: false,
} as any);
renderQuickEntryBox({ availableModels: undefined });
expandQuickEntry();
const githubToggle = await screen.findByTestId("quick-entry-github-toggle");
expect(githubToggle.getAttribute("aria-pressed")).toBe("false");
expect(githubToggle.className.includes("btn-primary")).toBe(false);
});
it("renders GitHub toggle on by default when project setting is enabled", async () => {
vi.mocked(fetchSettings).mockResolvedValueOnce({
githubTrackingEnabledByDefault: true,
} as any);
renderQuickEntryBox({ availableModels: undefined });
expandQuickEntry();
await waitFor(() => {
expect(screen.getByTestId("quick-entry-github-toggle").getAttribute("aria-pressed")).toBe("true");
});
expect(screen.getByTestId("quick-entry-github-toggle").className.includes("btn-primary")).toBe(true);
});
it("flips GitHub toggle from both project-default directions", async () => {
vi.mocked(fetchSettings).mockResolvedValueOnce({
githubTrackingEnabledByDefault: false,
} as any);
const { unmount } = renderQuickEntryBox({ availableModels: undefined });
expandQuickEntry();
const githubToggle = await screen.findByTestId("quick-entry-github-toggle");
expect(githubToggle.getAttribute("aria-pressed")).toBe("false");
fireEvent.click(githubToggle);
expect(githubToggle.getAttribute("aria-pressed")).toBe("true");
unmount();
vi.mocked(fetchSettings).mockResolvedValueOnce({
githubTrackingEnabledByDefault: true,
} as any);
renderQuickEntryBox({ availableModels: undefined });
expandQuickEntry();
await waitFor(() => {
expect(screen.getByTestId("quick-entry-github-toggle").getAttribute("aria-pressed")).toBe("true");
});
fireEvent.click(screen.getByTestId("quick-entry-github-toggle"));
expect(screen.getByTestId("quick-entry-github-toggle").getAttribute("aria-pressed")).toBe("false");
});
it.each(["Enter", "Save"] as const)("submits executionMode=fast when Fast is active via %s", async (submitPath) => { it.each(["Enter", "Save"] as const)("submits executionMode=fast when Fast is active via %s", async (submitPath) => {
const { props } = renderQuickEntryBox({}); const { props } = renderQuickEntryBox({});
expandQuickEntry(); expandQuickEntry();
@@ -796,6 +848,71 @@ describe("QuickEntryBox", () => {
expect(firstPayload.executionMode).toBeUndefined(); expect(firstPayload.executionMode).toBeUndefined();
}); });
it("omits githubTracking when toggle is untouched", async () => {
const { props } = renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "No github override" } });
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => {
expect(props.onCreate).toHaveBeenCalled();
});
const payload = props.onCreate.mock.calls[0]?.[0];
expect(payload.githubTracking).toBeUndefined();
});
it("submits githubTracking override when toggle is clicked", async () => {
vi.mocked(fetchSettings).mockResolvedValueOnce({
githubTrackingEnabledByDefault: false,
} as any);
const { props } = renderQuickEntryBox({ availableModels: undefined });
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
const githubToggle = await screen.findByTestId("quick-entry-github-toggle");
fireEvent.click(githubToggle);
fireEvent.change(textarea, { target: { value: "Override github tracking" } });
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => {
expect(props.onCreate).toHaveBeenCalledWith(
expect.objectContaining({
description: "Override github tracking",
githubTracking: { enabled: true },
}),
);
});
});
it("resets GitHub toggle to project default after successful task creation", async () => {
vi.mocked(fetchSettings).mockResolvedValueOnce({
githubTrackingEnabledByDefault: true,
} as any);
const { props } = renderQuickEntryBox({ availableModels: undefined });
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
await waitFor(() => {
expect(screen.getByTestId("quick-entry-github-toggle").getAttribute("aria-pressed")).toBe("true");
});
fireEvent.click(screen.getByTestId("quick-entry-github-toggle"));
expect(screen.getByTestId("quick-entry-github-toggle").getAttribute("aria-pressed")).toBe("false");
fireEvent.change(textarea, { target: { value: "Reset github toggle" } });
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => {
expect(props.onCreate).toHaveBeenCalledTimes(1);
});
expandQuickEntry();
expect(screen.getByTestId("quick-entry-github-toggle").getAttribute("aria-pressed")).toBe("true");
});
it("resets Fast toggle to standard after successful task creation", async () => { it("resets Fast toggle to standard after successful task creation", async () => {
const { props } = renderQuickEntryBox({}); const { props } = renderQuickEntryBox({});
expandQuickEntry(); expandQuickEntry();