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:
committed by
gsxdsm
parent
e5c9c7a3d9
commit
51c60fe1b5
5
.changeset/fn-4731-tracking-repo-dropdown.md
Normal file
5
.changeset/fn-4731-tracking-repo-dropdown.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Settings now provide a tracking-repo picker for both project and global defaults, populated from detected GitHub remotes with a Custom fallback for manual `owner/repo` entry.
|
||||||
@@ -68,7 +68,7 @@ Fusion automatically falls back to ntfy's JSON publish format when a notificatio
|
|||||||
| `openrouterProviderPreferences` | `{ order?: string[]; ignore?: string[]; only?: string[]; allow_fallbacks?: boolean; sort?: "price" \| "throughput" \| "latency"; require_parameters?: boolean }` | `undefined` | Optional OpenRouter provider routing preferences forwarded via `compat.openRouterRouting` on chat-completion requests. See OpenRouter provider routing: <https://openrouter.ai/docs/features/provider-routing>. |
|
| `openrouterProviderPreferences` | `{ order?: string[]; ignore?: string[]; only?: string[]; allow_fallbacks?: boolean; sort?: "price" \| "throughput" \| "latency"; require_parameters?: boolean }` | `undefined` | Optional OpenRouter provider routing preferences forwarded via `compat.openRouterRouting` on chat-completion requests. See OpenRouter provider routing: <https://openrouter.ai/docs/features/provider-routing>. |
|
||||||
| `opencodeGoModelSync` | `boolean` | `true` | Sync opencode-go model catalog at startup via `opencode models opencode --refresh`, normalizing discovered `opencode/...` IDs into the `opencode-go` provider surface used by `/api/models`. |
|
| `opencodeGoModelSync` | `boolean` | `true` | Sync opencode-go model catalog at startup via `opencode models opencode --refresh`, normalizing discovered `opencode/...` IDs into the `opencode-go` provider surface used by `/api/models`. |
|
||||||
| `updateCheckEnabled` | `boolean` | `true` | When enabled, Fusion performs a daily npm registry check for new `@runfusion/fusion` versions and shows update notices in CLI/dashboard. |
|
| `updateCheckEnabled` | `boolean` | `true` | When enabled, Fusion performs a daily npm registry check for new `@runfusion/fusion` versions and shows update notices in CLI/dashboard. |
|
||||||
| `githubTrackingDefaultRepo` | `string` | `undefined` | Global fallback issue-tracking repo (`owner/repo`) used when task-level tracking is enabled and no project/task override is set. This key is dual-scope: global saves go through `PUT /api/settings/global` (Settings → Global General). |
|
| `githubTrackingDefaultRepo` | `string` | `undefined` | Global fallback issue-tracking repo (`owner/repo`) used when task-level tracking is enabled and no project/task override is set. In Settings UI this is a detected-remote dropdown with a Custom fallback for manual entry. This key is dual-scope: global saves go through `PUT /api/settings/global` (Settings → Global General). |
|
||||||
| `autoReloadOnVersionChange` | `boolean` | `true` | When enabled (default), the dashboard automatically reloads when a new build version is detected via `/version.json` polling or service worker activation. Set to `false` to suppress automatic reloads — the user must manually refresh to pick up updates. |
|
| `autoReloadOnVersionChange` | `boolean` | `true` | When enabled (default), the dashboard automatically reloads when a new build version is detected via `/version.json` polling or service worker activation. Set to `false` to suppress automatic reloads — the user must manually refresh to pick up updates. |
|
||||||
| `modelOnboardingComplete` | `boolean` | `undefined` | Whether AI onboarding has been completed or dismissed. |
|
| `modelOnboardingComplete` | `boolean` | `undefined` | Whether AI onboarding has been completed or dismissed. |
|
||||||
| `executionGlobalProvider` | `string` | `undefined` | Global baseline provider for task execution. Project `executionProvider` overrides this. |
|
| `executionGlobalProvider` | `string` | `undefined` | Global baseline provider for task execution. Project `executionProvider` overrides this. |
|
||||||
@@ -340,7 +340,7 @@ Default notes:
|
|||||||
| `githubCommentOnDone` | `boolean` | `false` | When enabled, tasks imported from GitHub issues post a completion comment to the source issue when the task moves to `done`. |
|
| `githubCommentOnDone` | `boolean` | `false` | When enabled, tasks imported from GitHub issues post a completion comment to the source issue when the task moves to `done`. |
|
||||||
| `githubCommentTemplate` | `string` | `undefined` | Optional issue comment template used by `githubCommentOnDone`. Supports `{taskId}` and `{taskTitle}` placeholders. If unset, Fusion uses a default completion message. |
|
| `githubCommentTemplate` | `string` | `undefined` | Optional issue comment template used by `githubCommentOnDone`. Supports `{taskId}` and `{taskTitle}` placeholders. If unset, Fusion uses a default completion message. |
|
||||||
| `githubTrackingEnabledByDefault` | `boolean` | `false` | Project-level default for enabling issue tracking on new tasks. Even when this is false, issue creation can still occur per task if tracking is explicitly enabled. |
|
| `githubTrackingEnabledByDefault` | `boolean` | `false` | Project-level default for enabling issue tracking on new tasks. Even when this is false, issue creation can still occur per task if tracking is explicitly enabled. |
|
||||||
| `githubTrackingDefaultRepo` | `string` | `undefined` | Project default issue-tracking repo (`owner/repo`) used before global fallback for tracked task creation (precedence: task override → project default → global default). This key is dual-scope: project saves go through `PUT /api/settings` (Settings → General → GitHub Tracking) while global saves go through `PUT /api/settings/global` (Settings → Global General). |
|
| `githubTrackingDefaultRepo` | `string` | `undefined` | Project default issue-tracking repo (`owner/repo`) used before global fallback for tracked task creation (precedence: task override → project default → global default). In Settings UI this is a detected-remote dropdown with a Custom fallback for manual entry. This key is dual-scope: project saves go through `PUT /api/settings` (Settings → General → GitHub Tracking) while global saves go through `PUT /api/settings/global` (Settings → Global General). |
|
||||||
| `githubAuthMode` | `"gh-cli" \| "token"` | `"gh-cli"` | Project GitHub auth strategy used by tracking lifecycle integration. `"gh-cli"` requires an installed/authenticated `gh` CLI. `"token"` requires a non-empty `githubAuthToken` (or `GITHUB_TOKEN` env fallback). Tracking lifecycle auth is strict per selected mode (no cross-fallback). |
|
| `githubAuthMode` | `"gh-cli" \| "token"` | `"gh-cli"` | Project GitHub auth strategy used by tracking lifecycle integration. `"gh-cli"` requires an installed/authenticated `gh` CLI. `"token"` requires a non-empty `githubAuthToken` (or `GITHUB_TOKEN` env fallback). Tracking lifecycle auth is strict per selected mode (no cross-fallback). |
|
||||||
| `githubAuthToken` | `string` | `undefined` | Optional project PAT used when `githubAuthMode` is `"token"` (takes precedence over server startup token for tracking flows). |
|
| `githubAuthToken` | `string` | `undefined` | Optional project PAT used when `githubAuthMode` is `"token"` (takes precedence over server startup token for tracking flows). |
|
||||||
| `autoCreatePr` | `boolean` | `false` | Auto-create PRs for completed tasks. |
|
| `autoCreatePr` | `boolean` | `false` | Auto-create PRs for completed tasks. |
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
resolveTitleSummarizerSettingsModel,
|
resolveTitleSummarizerSettingsModel,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import type { AgentPermissionPolicyRules, Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, AgentPromptsConfig, ThinkingLevel } 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 { 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, GitRemoteDetailed, RemoteSettings, RemoteStatus, UpdateCheckResponse, OAuthDeviceCodeInfo } 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 { useMemoryBackendStatus } from "../hooks/useMemoryBackendStatus";
|
||||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||||
import type { ToastType } from "../hooks/useToast";
|
import type { ToastType } from "../hooks/useToast";
|
||||||
@@ -51,6 +51,7 @@ import { useNodes } from "../hooks/useNodes";
|
|||||||
import { useViewportMode } from "../hooks/useViewportMode";
|
import { useViewportMode } from "../hooks/useViewportMode";
|
||||||
import { useWorktrunkInstallStatus } from "../hooks/useWorktrunkInstallStatus";
|
import { useWorktrunkInstallStatus } from "../hooks/useWorktrunkInstallStatus";
|
||||||
import { NodeHealthDot } from "./NodeHealthDot";
|
import { NodeHealthDot } from "./NodeHealthDot";
|
||||||
|
import { TrackingRepoSelect, type TrackingRepoOption } from "./TrackingRepoSelect";
|
||||||
import { filterVisibleOnboardingAndSettingsProviders } from "./providerVisibility";
|
import { filterVisibleOnboardingAndSettingsProviders } from "./providerVisibility";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -74,6 +75,17 @@ function getNodeStatusLabel(status: "online" | "offline" | "connecting" | "error
|
|||||||
return "Offline";
|
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
|
* 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
|
* 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
|
// Git remotes for the worktree rebase dropdown. Loaded lazily; empty list
|
||||||
// is a valid state (fresh repo, no remotes configured yet).
|
// is a valid state (fresh repo, no remotes configured yet).
|
||||||
const [gitRemotes, setGitRemotes] = useState<GitRemoteDetailed[]>([]);
|
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 [memoryFiles, setMemoryFiles] = useState<MemoryFileInfo[]>([]);
|
||||||
const [selectedMemoryPath, setSelectedMemoryPath] = useState(DEFAULT_MEMORY_EDITOR_PATH);
|
const [selectedMemoryPath, setSelectedMemoryPath] = useState(DEFAULT_MEMORY_EDITOR_PATH);
|
||||||
const [memoryTestQuery, setMemoryTestQuery] = useState("");
|
const [memoryTestQuery, setMemoryTestQuery] = useState("");
|
||||||
@@ -927,6 +946,109 @@ export function SettingsModal({
|
|||||||
.catch(() => setGitRemotes([]));
|
.catch(() => setGitRemotes([]));
|
||||||
}, [activeSection, projectId]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (activeSection !== "memory" || memoryDirty) {
|
if (activeSection !== "memory" || memoryDirty) {
|
||||||
return;
|
return;
|
||||||
@@ -2279,14 +2401,16 @@ export function SettingsModal({
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="projectGithubTrackingDefaultRepoGeneral">Project default tracking repo</label>
|
<label htmlFor="projectGithubTrackingDefaultRepoGeneral">Project default tracking repo</label>
|
||||||
<input
|
<TrackingRepoSelect
|
||||||
id="projectGithubTrackingDefaultRepoGeneral"
|
id="projectGithubTrackingDefaultRepoGeneral"
|
||||||
type="text"
|
ariaLabel="Project default tracking repo"
|
||||||
className="input"
|
|
||||||
placeholder="owner/repo"
|
|
||||||
value={form.githubTrackingDefaultRepo ?? ""}
|
value={form.githubTrackingDefaultRepo ?? ""}
|
||||||
onChange={(e) =>
|
options={projectTrackingRepoOptions}
|
||||||
setForm((f) => ({ ...f, githubTrackingDefaultRepo: e.target.value || undefined }))
|
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>
|
<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>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="globalGithubTrackingDefaultRepo">Global default tracking repo</label>
|
<label htmlFor="globalGithubTrackingDefaultRepo">Global default tracking repo</label>
|
||||||
<input
|
<TrackingRepoSelect
|
||||||
id="globalGithubTrackingDefaultRepo"
|
id="globalGithubTrackingDefaultRepo"
|
||||||
type="text"
|
ariaLabel="Global default tracking repo"
|
||||||
className="input"
|
|
||||||
placeholder="owner/repo"
|
|
||||||
value={form.githubTrackingDefaultRepo ?? ""}
|
value={form.githubTrackingDefaultRepo ?? ""}
|
||||||
onChange={(e) =>
|
options={globalTrackingRepoOptions}
|
||||||
setForm((f) => ({ ...f, githubTrackingDefaultRepo: e.target.value || undefined }))
|
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>
|
<small>Projects inherit this value when they do not set a project default tracking repo.</small>
|
||||||
|
|||||||
23
packages/dashboard/app/components/TrackingRepoSelect.css
Normal file
23
packages/dashboard/app/components/TrackingRepoSelect.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
114
packages/dashboard/app/components/TrackingRepoSelect.tsx
Normal file
114
packages/dashboard/app/components/TrackingRepoSelect.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -38,7 +38,9 @@ const mockUpdateGlobalConcurrency = vi.fn();
|
|||||||
const mockFetchMemoryBackendStatus = vi.fn();
|
const mockFetchMemoryBackendStatus = vi.fn();
|
||||||
const mockTestMemoryRetrieval = vi.fn();
|
const mockTestMemoryRetrieval = vi.fn();
|
||||||
const mockInstallQmd = vi.fn();
|
const mockInstallQmd = vi.fn();
|
||||||
|
const mockFetchGitRemotes = vi.fn();
|
||||||
const mockFetchGitRemotesDetailed = vi.fn();
|
const mockFetchGitRemotesDetailed = vi.fn();
|
||||||
|
const mockFetchProjects = vi.fn();
|
||||||
const mockFetchDashboardHealth = vi.fn();
|
const mockFetchDashboardHealth = vi.fn();
|
||||||
const mockCheckForUpdates = vi.fn();
|
const mockCheckForUpdates = vi.fn();
|
||||||
const mockFetchRemoteSettings = vi.fn();
|
const mockFetchRemoteSettings = vi.fn();
|
||||||
@@ -95,7 +97,9 @@ vi.mock("../../api", async (importOriginal) => {
|
|||||||
fetchMemoryBackendStatus: (...args: unknown[]) => mockFetchMemoryBackendStatus(...args),
|
fetchMemoryBackendStatus: (...args: unknown[]) => mockFetchMemoryBackendStatus(...args),
|
||||||
testMemoryRetrieval: (...args: unknown[]) => mockTestMemoryRetrieval(...args),
|
testMemoryRetrieval: (...args: unknown[]) => mockTestMemoryRetrieval(...args),
|
||||||
installQmd: (...args: unknown[]) => mockInstallQmd(...args),
|
installQmd: (...args: unknown[]) => mockInstallQmd(...args),
|
||||||
|
fetchGitRemotes: (...args: unknown[]) => mockFetchGitRemotes(...args),
|
||||||
fetchGitRemotesDetailed: (...args: unknown[]) => mockFetchGitRemotesDetailed(...args),
|
fetchGitRemotesDetailed: (...args: unknown[]) => mockFetchGitRemotesDetailed(...args),
|
||||||
|
fetchProjects: (...args: unknown[]) => mockFetchProjects(...args),
|
||||||
fetchDashboardHealth: (...args: unknown[]) => mockFetchDashboardHealth(...args),
|
fetchDashboardHealth: (...args: unknown[]) => mockFetchDashboardHealth(...args),
|
||||||
checkForUpdates: (...args: unknown[]) => mockCheckForUpdates(...args),
|
checkForUpdates: (...args: unknown[]) => mockCheckForUpdates(...args),
|
||||||
fetchRemoteSettings: (...args: unknown[]) => mockFetchRemoteSettings(...args),
|
fetchRemoteSettings: (...args: unknown[]) => mockFetchRemoteSettings(...args),
|
||||||
@@ -409,7 +413,9 @@ describe("SettingsModal", () => {
|
|||||||
qmdAvailable: true,
|
qmdAvailable: true,
|
||||||
qmdInstallCommand: "bun install -g @tobilu/qmd",
|
qmdInstallCommand: "bun install -g @tobilu/qmd",
|
||||||
});
|
});
|
||||||
|
mockFetchGitRemotes.mockResolvedValue([]);
|
||||||
mockFetchGitRemotesDetailed.mockResolvedValue([]);
|
mockFetchGitRemotesDetailed.mockResolvedValue([]);
|
||||||
|
mockFetchProjects.mockResolvedValue([]);
|
||||||
mockUseWorktrunkInstallStatus.mockReturnValue({
|
mockUseWorktrunkInstallStatus.mockReturnValue({
|
||||||
status: "missing",
|
status: "missing",
|
||||||
requestInstall: vi.fn(),
|
requestInstall: vi.fn(),
|
||||||
@@ -697,16 +703,23 @@ describe("SettingsModal", () => {
|
|||||||
renderModal({ initialSection: "global-general" });
|
renderModal({ initialSection: "global-general" });
|
||||||
await waitForSettingsModalReady();
|
await waitForSettingsModalReady();
|
||||||
|
|
||||||
const input = screen.getByLabelText("Global default tracking repo") as HTMLInputElement;
|
const control = screen.getByRole("combobox", { name: "Global default tracking repo" }) as HTMLSelectElement;
|
||||||
expect(input.value).toBe("");
|
expect(control).toBeInTheDocument();
|
||||||
expect(screen.getByText(/Projects inherit this value when they do not set a project default tracking repo/i)).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 () => {
|
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" });
|
renderModal({ initialSection: "global-general" });
|
||||||
await waitForSettingsModalReady();
|
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 userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -721,6 +734,16 @@ describe("SettingsModal", () => {
|
|||||||
expect(projectPayload.githubTrackingDefaultRepo).toBeUndefined();
|
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 () => {
|
it("renders and saves agent provisioning approval settings", async () => {
|
||||||
@@ -917,12 +940,12 @@ describe("SettingsModal", () => {
|
|||||||
expect(screen.getByRole("heading", { name: "GitHub Tracking" })).toBeInTheDocument();
|
expect(screen.getByRole("heading", { name: "GitHub Tracking" })).toBeInTheDocument();
|
||||||
|
|
||||||
const modeSelect = screen.getByLabelText("Default tracking mode for new tasks") as HTMLSelectElement;
|
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(modeSelect.value).toBe("off");
|
||||||
expect(repoInput.value).toBe("");
|
expect(repoSelect.value).toBe("__custom__");
|
||||||
|
|
||||||
await userEvent.selectOptions(modeSelect, "new-tasks");
|
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 userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -950,13 +973,13 @@ describe("SettingsModal", () => {
|
|||||||
await waitForSettingsModalReady();
|
await waitForSettingsModalReady();
|
||||||
|
|
||||||
const modeSelect = screen.getByLabelText("Default tracking mode for new tasks") as HTMLSelectElement;
|
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(modeSelect.value).toBe("new-tasks");
|
||||||
expect(repoInput.value).toBe("octo/existing");
|
expect(repoSelect.value).toBe("__custom__");
|
||||||
|
|
||||||
await userEvent.selectOptions(modeSelect, "off");
|
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 userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -991,6 +1014,40 @@ describe("SettingsModal", () => {
|
|||||||
expect(screen.getByText("Title, commit message, and GitHub tracking issue summarization model")).toBeInTheDocument();
|
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 () => {
|
it("always shows GitHub tracking summarization helper copy", async () => {
|
||||||
renderModal({ initialSection: "general" });
|
renderModal({ initialSection: "general" });
|
||||||
await waitForSettingsModalReady();
|
await waitForSettingsModalReady();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,7 +11,7 @@ const qualityAppTests = [
|
|||||||
"app/api/**/*.test.ts",
|
"app/api/**/*.test.ts",
|
||||||
// Representative workflow/component coverage. Exhaustive modal/view suites
|
// Representative workflow/component coverage. Exhaustive modal/view suites
|
||||||
// stay available in the full `dashboard-app` project.
|
// 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.
|
// Hooks and utilities are fast, user-visible state/formatting behavior.
|
||||||
"app/context/**/*.test.tsx",
|
"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}",
|
"app/hooks/__tests__/{useAgents,useAgentLogs,useAppSettings,useAuthOnboarding,useConfirm,useCurrentProject,useNodes,useNodeSettingsSync,useProjects,useQuickChat,useTasks,useTerminalSessions,useTheme,useToast,useUsageData,useViewState}.test.{ts,tsx}",
|
||||||
|
|||||||
Reference in New Issue
Block a user