feat(FN-4731): add tracking repo dropdown to settings modal

Adds a `TrackingRepoSelect` dropdown component to the SettingsModal (with co-located CSS and full test coverage), updates the settings reference docs, and includes the FN-4731 changeset for the `@runfusion/fusion` package.

Fusion-Task-Id: FN-4731
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 09:14:55 -07:00
committed by gsxdsm
parent e5c9c7a3d9
commit 51c60fe1b5
8 changed files with 483 additions and 26 deletions

View File

@@ -12,8 +12,8 @@ import {
resolveTitleSummarizerSettingsModel,
} from "@fusion/core";
import type { AgentPermissionPolicyRules, Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, AgentPromptsConfig, ThinkingLevel } from "@fusion/core";
import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, cancelProviderLogin, saveApiKey, clearApiKey, fetchModels, testNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, triggerMemoryDreams, fetchGitRemotesDetailed, fetchDashboardHealth, checkForUpdates, fetchRemoteSettings, updateRemoteSettings, fetchRemoteStatus, installCloudflared, startRemoteTunnel, stopRemoteTunnel, killExternalTunnel, regenerateRemotePersistentToken, generateShortLivedRemoteToken, fetchRemoteQr, fetchRemoteUrl, submitProviderManualCode } from "../api";
import type { AuthProvider, ManualOAuthCodeInfo, ModelInfo, BackupListResponse, SettingsExportData, MemoryFileInfo, MemoryRetrievalTestResult, GitRemoteDetailed, RemoteSettings, RemoteStatus, UpdateCheckResponse, OAuthDeviceCodeInfo } from "../api";
import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, cancelProviderLogin, saveApiKey, clearApiKey, fetchModels, testNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, triggerMemoryDreams, fetchGitRemotes, fetchGitRemotesDetailed, fetchProjects, fetchDashboardHealth, checkForUpdates, fetchRemoteSettings, updateRemoteSettings, fetchRemoteStatus, installCloudflared, startRemoteTunnel, stopRemoteTunnel, killExternalTunnel, regenerateRemotePersistentToken, generateShortLivedRemoteToken, fetchRemoteQr, fetchRemoteUrl, submitProviderManualCode } from "../api";
import type { AuthProvider, ManualOAuthCodeInfo, ModelInfo, BackupListResponse, SettingsExportData, MemoryFileInfo, MemoryRetrievalTestResult, GitRemote, GitRemoteDetailed, ProjectInfo, RemoteSettings, RemoteStatus, UpdateCheckResponse, OAuthDeviceCodeInfo } from "../api";
import { useMemoryBackendStatus } from "../hooks/useMemoryBackendStatus";
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
import type { ToastType } from "../hooks/useToast";
@@ -51,6 +51,7 @@ import { useNodes } from "../hooks/useNodes";
import { useViewportMode } from "../hooks/useViewportMode";
import { useWorktrunkInstallStatus } from "../hooks/useWorktrunkInstallStatus";
import { NodeHealthDot } from "./NodeHealthDot";
import { TrackingRepoSelect, type TrackingRepoOption } from "./TrackingRepoSelect";
import { filterVisibleOnboardingAndSettingsProviders } from "./providerVisibility";
// ---------------------------------------------------------------------------
@@ -74,6 +75,17 @@ function getNodeStatusLabel(status: "online" | "offline" | "connecting" | "error
return "Offline";
}
function toTrackingRepoOptions(remotes: GitRemote[]): TrackingRepoOption[] {
const byValue = new Map<string, TrackingRepoOption>();
for (const remote of remotes) {
const value = `${remote.owner}/${remote.repo}`;
if (!byValue.has(value)) {
byValue.set(value, { value, label: value });
}
}
return [...byValue.values()].sort((a, b) => a.value.localeCompare(b.value));
}
/**
* Has the user already clicked the "Star on GitHub" button at any point in
* the past? Used to permanently hide the button afterward — clicking opens
@@ -595,6 +607,13 @@ export function SettingsModal({
// Git remotes for the worktree rebase dropdown. Loaded lazily; empty list
// is a valid state (fresh repo, no remotes configured yet).
const [gitRemotes, setGitRemotes] = useState<GitRemoteDetailed[]>([]);
const [projectTrackingRepoOptions, setProjectTrackingRepoOptions] = useState<TrackingRepoOption[]>([]);
const [projectTrackingRepoLoading, setProjectTrackingRepoLoading] = useState(false);
const [projectTrackingRepoError, setProjectTrackingRepoError] = useState<string | null>(null);
const [globalTrackingRepoOptions, setGlobalTrackingRepoOptions] = useState<TrackingRepoOption[]>([]);
const [globalTrackingRepoLoading, setGlobalTrackingRepoLoading] = useState(false);
const [globalTrackingRepoError, setGlobalTrackingRepoError] = useState<string | null>(null);
const globalTrackingRepoLoadedRef = useRef(false);
const [memoryFiles, setMemoryFiles] = useState<MemoryFileInfo[]>([]);
const [selectedMemoryPath, setSelectedMemoryPath] = useState(DEFAULT_MEMORY_EDITOR_PATH);
const [memoryTestQuery, setMemoryTestQuery] = useState("");
@@ -927,6 +946,109 @@ export function SettingsModal({
.catch(() => setGitRemotes([]));
}, [activeSection, projectId]);
useEffect(() => {
if (activeSection !== "general") {
return;
}
let cancelled = false;
setProjectTrackingRepoLoading(true);
setProjectTrackingRepoError(null);
fetchGitRemotes(projectId)
.then((remotes) => {
if (cancelled) {
return;
}
setProjectTrackingRepoOptions(toTrackingRepoOptions(remotes));
})
.catch(() => {
if (cancelled) {
return;
}
setProjectTrackingRepoOptions([]);
setProjectTrackingRepoError("Could not load detected remotes. Enter a custom owner/repo value.");
})
.finally(() => {
if (!cancelled) {
setProjectTrackingRepoLoading(false);
}
});
return () => {
cancelled = true;
};
}, [activeSection, projectId]);
useEffect(() => {
if (activeSection !== "global-general" || globalTrackingRepoLoadedRef.current) {
return;
}
let cancelled = false;
setGlobalTrackingRepoLoading(true);
setGlobalTrackingRepoError(null);
fetchProjects()
.then(async (projects) => {
const results = await Promise.allSettled(
projects.map(async (project: ProjectInfo) => {
const remotes = await fetchGitRemotes(project.id);
return remotes.map((remote) => ({
value: `${remote.owner}/${remote.repo}`,
label: `${remote.owner}/${remote.repo}`,
source: project.name,
}));
}),
);
if (cancelled) {
return;
}
const optionsByValue = new Map<string, TrackingRepoOption>();
let successCount = 0;
for (const result of results) {
if (result.status !== "fulfilled") {
continue;
}
successCount += 1;
for (const option of result.value) {
if (!optionsByValue.has(option.value)) {
optionsByValue.set(option.value, option);
}
}
}
const flattenedOptions = [...optionsByValue.values()].sort((a, b) => a.value.localeCompare(b.value));
setGlobalTrackingRepoOptions(flattenedOptions);
if (projects.length > 0 && successCount === 0) {
setGlobalTrackingRepoError("Could not load remotes from registered projects. Enter a custom owner/repo value.");
}
globalTrackingRepoLoadedRef.current = true;
})
.catch(() => {
if (cancelled) {
return;
}
setGlobalTrackingRepoOptions([]);
setGlobalTrackingRepoError("Could not load project list. Enter a custom owner/repo value.");
globalTrackingRepoLoadedRef.current = true;
})
.finally(() => {
if (!cancelled) {
setGlobalTrackingRepoLoading(false);
}
});
return () => {
cancelled = true;
};
}, [activeSection, projectId]);
useEffect(() => {
if (activeSection !== "memory" || memoryDirty) {
return;
@@ -2279,14 +2401,16 @@ export function SettingsModal({
</div>
<div className="form-group">
<label htmlFor="projectGithubTrackingDefaultRepoGeneral">Project default tracking repo</label>
<input
<TrackingRepoSelect
id="projectGithubTrackingDefaultRepoGeneral"
type="text"
className="input"
placeholder="owner/repo"
ariaLabel="Project default tracking repo"
value={form.githubTrackingDefaultRepo ?? ""}
onChange={(e) =>
setForm((f) => ({ ...f, githubTrackingDefaultRepo: e.target.value || undefined }))
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>
@@ -2317,14 +2441,16 @@ export function SettingsModal({
</div>
<div className="form-group">
<label htmlFor="globalGithubTrackingDefaultRepo">Global default tracking repo</label>
<input
<TrackingRepoSelect
id="globalGithubTrackingDefaultRepo"
type="text"
className="input"
placeholder="owner/repo"
ariaLabel="Global default tracking repo"
value={form.githubTrackingDefaultRepo ?? ""}
onChange={(e) =>
setForm((f) => ({ ...f, githubTrackingDefaultRepo: e.target.value || undefined }))
options={globalTrackingRepoOptions}
loading={globalTrackingRepoLoading}
error={globalTrackingRepoError ?? undefined}
placeholder="owner/repo"
onChange={(nextValue) =>
setForm((f) => ({ ...f, githubTrackingDefaultRepo: nextValue || undefined }))
}
/>
<small>Projects inherit this value when they do not set a project default tracking repo.</small>

View File

@@ -0,0 +1,23 @@
.tracking-repo-select {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.tracking-repo-select__custom-input {
margin-top: 0;
}
.tracking-repo-select__hint {
color: var(--text-muted);
}
.tracking-repo-select__hint--error {
color: var(--color-error);
}
@media (max-width: 768px) {
.tracking-repo-select {
gap: var(--space-xs);
}
}

View File

@@ -0,0 +1,114 @@
import { useEffect, useId, useMemo, useRef, useState } from "react";
import "./TrackingRepoSelect.css";
const CUSTOM_VALUE = "__custom__";
export interface TrackingRepoOption {
value: string;
label: string;
source?: string;
}
interface TrackingRepoSelectProps {
id: string;
value: string;
onChange: (value: string) => void;
options: TrackingRepoOption[];
loading?: boolean;
error?: string;
placeholder?: string;
ariaLabel?: string;
}
function normalizeOptions(options: TrackingRepoOption[]): TrackingRepoOption[] {
const byValue = new Map<string, TrackingRepoOption>();
for (const option of options) {
const normalized = option.value.trim();
if (!normalized || byValue.has(normalized)) {
continue;
}
byValue.set(normalized, {
...option,
value: normalized,
label: option.label?.trim() || normalized,
});
}
return [...byValue.values()].sort((a, b) => a.value.localeCompare(b.value));
}
export function TrackingRepoSelect({
id,
value,
onChange,
options,
loading = false,
error,
placeholder = "owner/repo",
ariaLabel,
}: TrackingRepoSelectProps) {
const customInputRef = useRef<HTMLInputElement>(null);
const hintId = useId();
const [showCustomInput, setShowCustomInput] = useState(false);
const normalizedOptions = useMemo(() => normalizeOptions(options), [options]);
const selectedOption = normalizedOptions.find((option) => option.value === value);
const selectValue = selectedOption ? selectedOption.value : CUSTOM_VALUE;
useEffect(() => {
setShowCustomInput(selectValue === CUSTOM_VALUE);
}, [selectValue]);
const handleSelectChange = (nextValue: string) => {
if (nextValue === CUSTOM_VALUE) {
setShowCustomInput(true);
requestAnimationFrame(() => {
customInputRef.current?.focus();
});
return;
}
setShowCustomInput(false);
onChange(nextValue);
};
const renderedHint = error ? error : loading ? "Loading detected GitHub remotes…" : null;
return (
<div className="tracking-repo-select">
<select
id={id}
className="select"
value={selectValue}
onChange={(event) => handleSelectChange(event.target.value)}
aria-label={ariaLabel}
aria-describedby={renderedHint ? hintId : undefined}
>
{normalizedOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.source ? `${option.label} — ${option.source}` : option.label}
</option>
))}
<option value={CUSTOM_VALUE}>Custom…</option>
</select>
{showCustomInput ? (
<input
ref={customInputRef}
className="input tracking-repo-select__custom-input"
type="text"
value={value}
placeholder={placeholder}
onChange={(event) => onChange(event.target.value)}
aria-describedby={renderedHint ? hintId : undefined}
/>
) : null}
{renderedHint ? (
<small
id={hintId}
className={error ? "tracking-repo-select__hint tracking-repo-select__hint--error" : "tracking-repo-select__hint"}
>
{renderedHint}
</small>
) : null}
</div>
);
}

View File

@@ -38,7 +38,9 @@ const mockUpdateGlobalConcurrency = vi.fn();
const mockFetchMemoryBackendStatus = vi.fn();
const mockTestMemoryRetrieval = vi.fn();
const mockInstallQmd = vi.fn();
const mockFetchGitRemotes = vi.fn();
const mockFetchGitRemotesDetailed = vi.fn();
const mockFetchProjects = vi.fn();
const mockFetchDashboardHealth = vi.fn();
const mockCheckForUpdates = vi.fn();
const mockFetchRemoteSettings = vi.fn();
@@ -95,7 +97,9 @@ vi.mock("../../api", async (importOriginal) => {
fetchMemoryBackendStatus: (...args: unknown[]) => mockFetchMemoryBackendStatus(...args),
testMemoryRetrieval: (...args: unknown[]) => mockTestMemoryRetrieval(...args),
installQmd: (...args: unknown[]) => mockInstallQmd(...args),
fetchGitRemotes: (...args: unknown[]) => mockFetchGitRemotes(...args),
fetchGitRemotesDetailed: (...args: unknown[]) => mockFetchGitRemotesDetailed(...args),
fetchProjects: (...args: unknown[]) => mockFetchProjects(...args),
fetchDashboardHealth: (...args: unknown[]) => mockFetchDashboardHealth(...args),
checkForUpdates: (...args: unknown[]) => mockCheckForUpdates(...args),
fetchRemoteSettings: (...args: unknown[]) => mockFetchRemoteSettings(...args),
@@ -409,7 +413,9 @@ describe("SettingsModal", () => {
qmdAvailable: true,
qmdInstallCommand: "bun install -g @tobilu/qmd",
});
mockFetchGitRemotes.mockResolvedValue([]);
mockFetchGitRemotesDetailed.mockResolvedValue([]);
mockFetchProjects.mockResolvedValue([]);
mockUseWorktrunkInstallStatus.mockReturnValue({
status: "missing",
requestInstall: vi.fn(),
@@ -697,16 +703,23 @@ describe("SettingsModal", () => {
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();
const input = screen.getByLabelText("Global default tracking repo") as HTMLInputElement;
expect(input.value).toBe("");
const control = screen.getByRole("combobox", { name: "Global default tracking repo" }) as HTMLSelectElement;
expect(control).toBeInTheDocument();
expect(screen.getByText(/Projects inherit this value when they do not set a project default tracking repo/i)).toBeInTheDocument();
});
it("saves global default tracking repo via global settings payload only", async () => {
mockFetchProjects.mockResolvedValueOnce([{ id: "p-1", name: "Alpha" }]);
mockFetchGitRemotes.mockResolvedValueOnce([{ name: "origin", owner: "octo", repo: "global-default", url: "https://github.com/octo/global-default.git" }]);
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();
await userEvent.type(screen.getByLabelText("Global default tracking repo"), "octo/global-default");
await waitFor(() => {
expect(screen.getByRole("combobox", { name: "Global default tracking repo" })).toHaveValue("__custom__");
});
await userEvent.selectOptions(screen.getByRole("combobox", { name: "Global default tracking repo" }), "octo/global-default");
await userEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
@@ -721,6 +734,16 @@ describe("SettingsModal", () => {
expect(projectPayload.githubTrackingDefaultRepo).toBeUndefined();
}
});
it("shows global tracking repo error hint and keeps custom entry when lookups fail", async () => {
mockFetchProjects.mockRejectedValueOnce(new Error("no projects"));
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();
expect(await screen.findByText(/Could not load project list/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText("owner/repo")).toBeInTheDocument();
});
});
it("renders and saves agent provisioning approval settings", async () => {
@@ -917,12 +940,12 @@ describe("SettingsModal", () => {
expect(screen.getByRole("heading", { name: "GitHub Tracking" })).toBeInTheDocument();
const modeSelect = screen.getByLabelText("Default tracking mode for new tasks") as HTMLSelectElement;
const repoInput = screen.getByLabelText("Project default tracking repo") as HTMLInputElement;
const repoSelect = screen.getByRole("combobox", { name: "Project default tracking repo" }) as HTMLSelectElement;
expect(modeSelect.value).toBe("off");
expect(repoInput.value).toBe("");
expect(repoSelect.value).toBe("__custom__");
await userEvent.selectOptions(modeSelect, "new-tasks");
await userEvent.type(repoInput, "octo/repo");
await userEvent.type(screen.getByPlaceholderText("owner/repo"), "octo/repo");
await userEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
@@ -950,13 +973,13 @@ describe("SettingsModal", () => {
await waitForSettingsModalReady();
const modeSelect = screen.getByLabelText("Default tracking mode for new tasks") as HTMLSelectElement;
const repoInput = screen.getByLabelText("Project default tracking repo") as HTMLInputElement;
const repoSelect = screen.getByRole("combobox", { name: "Project default tracking repo" }) as HTMLSelectElement;
expect(modeSelect.value).toBe("new-tasks");
expect(repoInput.value).toBe("octo/existing");
expect(repoSelect.value).toBe("__custom__");
await userEvent.selectOptions(modeSelect, "off");
await userEvent.clear(repoInput);
await userEvent.clear(screen.getByPlaceholderText("owner/repo"));
await userEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
@@ -991,6 +1014,40 @@ describe("SettingsModal", () => {
expect(screen.getByText("Title, commit message, and GitHub tracking issue summarization model")).toBeInTheDocument();
});
it("picks a project repo suggestion and preserves label association", async () => {
mockFetchGitRemotes.mockResolvedValueOnce([
{ name: "origin", owner: "octo", repo: "repo", url: "https://github.com/octo/repo.git" },
]);
renderModal({ initialSection: "general" });
await waitForSettingsModalReady();
const repoSelect = screen.getByRole("combobox", { name: "Project default tracking repo" }) as HTMLSelectElement;
await waitFor(() => {
expect(within(repoSelect).getByRole("option", { name: "octo/repo" })).toBeInTheDocument();
});
await userEvent.selectOptions(repoSelect, "octo/repo");
await userEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(mockUpdateSettings).toHaveBeenCalled();
});
const payload = mockUpdateSettings.mock.calls[0][0] as Record<string, unknown>;
expect(payload.githubTrackingDefaultRepo).toBe("octo/repo");
});
it("shows project tracking repo error hint and keeps custom entry when remotes fail", async () => {
mockFetchGitRemotes.mockRejectedValueOnce(new Error("remotes failed"));
renderModal({ initialSection: "general" });
await waitForSettingsModalReady();
expect(await screen.findByText(/Could not load detected remotes/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText("owner/repo")).toBeInTheDocument();
});
it("always shows GitHub tracking summarization helper copy", async () => {
renderModal({ initialSection: "general" });
await waitForSettingsModalReady();

View File

@@ -0,0 +1,132 @@
import React from "react";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { TrackingRepoSelect } from "../TrackingRepoSelect";
describe("TrackingRepoSelect", () => {
it("renders suggestions and selected option", () => {
render(
<TrackingRepoSelect
id="tracking"
value="octo/widgets"
onChange={vi.fn()}
options={[
{ value: "octo/widgets", label: "octo/widgets" },
{ value: "acme/cli", label: "acme/cli" },
]}
/>,
);
const select = screen.getByRole("combobox");
expect(select).toHaveValue("octo/widgets");
expect(screen.queryByRole("textbox")).toBeNull();
});
it("fires onChange when selecting suggestion", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
render(
<TrackingRepoSelect
id="tracking"
value=""
onChange={onChange}
options={[
{ value: "octo/widgets", label: "octo/widgets" },
{ value: "acme/cli", label: "acme/cli" },
]}
/>,
);
await user.selectOptions(screen.getByRole("combobox"), "acme/cli");
expect(onChange).toHaveBeenCalledWith("acme/cli");
});
it("shows custom input and edits custom value", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
function Harness() {
const [value, setValue] = React.useState("");
return (
<TrackingRepoSelect
id="tracking"
value={value}
onChange={(next) => {
setValue(next);
onChange(next);
}}
options={[{ value: "octo/widgets", label: "octo/widgets" }]}
/>
);
}
render(<Harness />);
await user.selectOptions(screen.getByRole("combobox"), "__custom__");
const input = screen.getByRole("textbox");
await user.type(input, "custom/repo");
expect(onChange).toHaveBeenLastCalledWith("custom/repo");
});
it("treats non-suggestion value as custom", () => {
render(
<TrackingRepoSelect
id="tracking"
value="other/repo"
onChange={vi.fn()}
options={[{ value: "octo/widgets", label: "octo/widgets" }]}
/>,
);
expect(screen.getByRole("combobox")).toHaveValue("__custom__");
expect(screen.getByRole("textbox")).toHaveValue("other/repo");
});
it("deduplicates duplicate suggestions", () => {
render(
<TrackingRepoSelect
id="tracking"
value=""
onChange={vi.fn()}
options={[
{ value: "octo/widgets", label: "octo/widgets", source: "A" },
{ value: "octo/widgets", label: "octo/widgets", source: "B" },
]}
/>,
);
const options = screen.getAllByRole("option");
expect(options.filter((option) => option.getAttribute("value") === "octo/widgets")).toHaveLength(1);
});
it("renders loading/error hints without disabling custom entry", () => {
const { rerender } = render(
<TrackingRepoSelect
id="tracking"
value=""
onChange={vi.fn()}
options={[]}
loading
/>,
);
expect(screen.getByText("Loading detected GitHub remotes…")).toBeInTheDocument();
expect(screen.getByRole("textbox")).toBeInTheDocument();
rerender(
<TrackingRepoSelect
id="tracking"
value=""
onChange={vi.fn()}
options={[]}
error="Unable to load remotes"
/>,
);
expect(screen.getByText("Unable to load remotes")).toBeInTheDocument();
expect(screen.getByRole("textbox")).toBeInTheDocument();
});
});

View File

@@ -11,7 +11,7 @@ const qualityAppTests = [
"app/api/**/*.test.ts",
// Representative workflow/component coverage. Exhaustive modal/view suites
// stay available in the full `dashboard-app` project.
"app/components/__tests__/{ActiveAgentsPanel,AgentMentionPopup,AgentMetricsBar,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile-view-switch,ChatView,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DirectoryPicker,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,InlineCreateCard,LoginInstructions,MemoryView,MessageComposer,MobileNavBar,NewTaskModal,NodeCard,NodeHealthDot,NodeStatusIndicator,ProjectCard,ProjectSelector,ProviderIcon,QuickChatFAB,ReliabilityView,ResearchView,SettingsModal.worktrunk,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskChangesTab,TaskComments,TaskDetailModal.github-tracking-header,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,ThemeSelectorSwatchContract,WorktrunkInstallApprovalDetails,WorkflowResultsTab}.test.tsx",
"app/components/__tests__/{ActiveAgentsPanel,AgentMentionPopup,AgentMetricsBar,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile-view-switch,ChatView,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DirectoryPicker,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,InlineCreateCard,LoginInstructions,MemoryView,MessageComposer,MobileNavBar,NewTaskModal,NodeCard,NodeHealthDot,NodeStatusIndicator,ProjectCard,ProjectSelector,ProviderIcon,QuickChatFAB,ReliabilityView,ResearchView,SettingsModal,SettingsModal.worktrunk,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskChangesTab,TaskComments,TaskDetailModal.github-tracking-header,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,ThemeSelectorSwatchContract,TrackingRepoSelect,WorktrunkInstallApprovalDetails,WorkflowResultsTab}.test.tsx",
// Hooks and utilities are fast, user-visible state/formatting behavior.
"app/context/**/*.test.tsx",
"app/hooks/__tests__/{useAgents,useAgentLogs,useAppSettings,useAuthOnboarding,useConfirm,useCurrentProject,useNodes,useNodeSettingsSync,useProjects,useQuickChat,useTasks,useTerminalSessions,useTheme,useToast,useUsageData,useViewState}.test.{ts,tsx}",