Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changeset/fn-1467-pr-feedback-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"@runfusion/fusion": patch

Fix project selector review regressions around optional selection handlers and bookmarked search matches, and tighten retry/backoff timeout and rate-limit handling.
1 change: 1 addition & 0 deletions packages/dashboard/app/components/AgentsView.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
position: relative;
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
overflow: hidden;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/dashboard/app/components/ChatView.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

.chat-view {
display: flex;
flex: 1;
width: 100%;
height: 100%;
overflow: hidden;
}
Expand Down
130 changes: 7 additions & 123 deletions packages/dashboard/app/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { useState, useEffect, useRef, useCallback, useMemo, type KeyboardEvent a
import { useTranslation } from "react-i18next";
import { Settings, Pause, Play, Square, LayoutGrid, List, Terminal, Lightbulb, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Monitor, Server, Workflow, Bot, Target, ChevronRight, FileCode, Loader2, Grid3X3, Mail, MessageSquare, ChevronDown, Check, Zap, Sparkles, FileText, Brain, CheckSquare, Lock } from "lucide-react";
import "./Header.css";
// Header renders an inline ProjectSelector dropdown using project-selector-* classes.
// ProjectSelector styles used by the imported standalone component.
import "./ProjectSelector.css";
import { ProjectSelector as StandaloneProjectSelector } from "./ProjectSelector";
import type { ProjectInfo } from "../api";
import type { NodeConfig, ProjectStatus } from "@fusion/core";
import { fetchScripts } from "../api";
Expand All @@ -30,125 +31,8 @@ const PROJECT_STATUS_CONFIG: Record<ProjectStatus, { color: string }> = {
initializing: { color: "var(--info)" },
};

/**
* ProjectSelector - A component for project navigation.
* Shows project dropdown for switching projects and navigating to project management.
*/
function ProjectSelector({
projects,
currentProject,
onViewAll,
onSelectProject,
}: {
projects: ProjectInfo[];
currentProject: ProjectInfo | null;
onViewAll: () => void;
onSelectProject?: (project: ProjectInfo) => void;
}) {
const { t } = useTranslation("app");
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

// Close dropdown on outside click
useEffect(() => {
if (!isOpen) return;
const handleClickOutside = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [isOpen]);

// Close on Escape
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setIsOpen(false);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen]);

const handleSelectProject = useCallback(
(project: ProjectInfo) => {
onSelectProject?.(project);
setIsOpen(false);
},
[onSelectProject]
);

return (
<div className="project-selector" ref={dropdownRef}>
{projects.length > 0 && (
<>
<button
className={`project-selector-trigger${isOpen ? " project-selector-trigger--open" : ""}`}
onClick={() => setIsOpen((prev) => !prev)}
title={currentProject?.name ? t("header.switchProjectCurrent", "Switch project (current: {{name}})", { name: currentProject.name }) : t("header.switchProject", "Switch project")}
aria-label={t("header.switchProject", "Switch project")}
aria-expanded={isOpen}
aria-haspopup="listbox"
data-testid="project-selector-trigger"
>
<span className="project-selector-trigger-label">
{currentProject?.name ?? t("header.projects", "Projects")}
</span>
<ChevronDown size={12} className={`project-selector-chevron${isOpen ? " project-selector-chevron--open" : ""}`} />
</button>
{isOpen && (
<div
className="project-selector-dropdown"
role="listbox"
aria-label={t("header.selectProject", "Select project")}
data-testid="project-selector-dropdown"
>
{projects.map((project) => {
const isCurrent = currentProject?.id === project.id;
const statusColor = PROJECT_STATUS_CONFIG[project.status]?.color;
return (
<button
key={project.id}
className={`project-selector-item${isCurrent ? " project-selector-item--current" : ""}`}
onClick={() => handleSelectProject(project)}
role="option"
aria-selected={isCurrent}
>
<span
className="project-selector-dot"
style={{ backgroundColor: statusColor || "var(--text-muted)" }}
/>
<div className="project-selector-info">
<span className="project-selector-name">{project.name}</span>
<span className="project-selector-path">
{getTrailingPath(project.path, 2)}
</span>
</div>
{isCurrent && <Check size={14} className="project-selector-check" />}
</button>
);
})}
<div className="project-selector-divider" role="presentation" />
<button
className="project-selector-manage"
onClick={() => {
onViewAll();
setIsOpen(false);
}}
data-testid="manage-projects-action"
>
{t("header.manageProjects", "Manage Projects")}
</button>
</div>
)}
</>
)}
</div>
);
}
// Inline ProjectSelector removed — now imports StandaloneProjectSelector from ./ProjectSelector
// which has scroll fix, autocomplete, and bookmarking features.

// GitHub logo icon (Octocat mark) - uses currentColor for theme compatibility
function GitHubLogo({ size = 16 }: { size?: number }) {
Expand Down Expand Up @@ -952,12 +836,12 @@ export function Header({
)}

{/* Project Selector - Back button when project selected, dropdown when 2+ projects (tablet + desktop) */}
{!isMobile && projects.length >= 1 && onViewAllProjects && (
<ProjectSelector
{!isMobile && projects.length >= 1 && onViewAllProjects && onSelectProject && (
<StandaloneProjectSelector
projects={projects}
currentProject={currentProject ?? null}
onViewAll={onViewAllProjects}
onSelectProject={onSelectProject}
onSelect={onSelectProject}
/>
)}

Expand Down
104 changes: 104 additions & 0 deletions packages/dashboard/app/components/ProjectSelector.css
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,29 @@
z-index: 100;
min-width: 240px;
max-width: 360px;
max-height: min(480px, calc(100vh - 120px));
overflow-y: auto;
overscroll-behavior: contain;
padding: var(--space-sm);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--surface);
box-shadow: var(--shadow-lg);
scrollbar-width: thin;
scrollbar-color: var(--text-dim) transparent;
}

.project-selector__dropdown::-webkit-scrollbar {
width: 6px;
}

.project-selector__dropdown::-webkit-scrollbar-track {
background: transparent;
}

.project-selector__dropdown::-webkit-scrollbar-thumb {
background-color: var(--text-dim);
border-radius: 3px;
}

.project-selector-item,
Expand Down Expand Up @@ -199,11 +217,59 @@
}

.project-selector__no-results {
display: flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-sm) calc(var(--space-sm) + var(--space-xs));
color: var(--text-muted);
font-size: 13px;
}

.project-selector__no-results-icon {
flex-shrink: 0;
opacity: 0.5;
}

/* Autocomplete highlight — marks the matched text substring */
.project-selector__highlight {
background: transparent;
color: inherit;
font-weight: 700;
text-decoration: underline;
text-decoration-color: var(--todo);
text-underline-offset: 2px;
text-decoration-thickness: 2px;
}

/* Exact match indicator banner */
.project-selector__exact-match {
padding: var(--space-xs) calc(var(--space-sm) + var(--space-xs));
margin-bottom: var(--space-xs);
font-size: 12px;
color: var(--todo);
background: color-mix(in srgb, var(--todo) 8%, transparent);
border-radius: var(--radius-md);
text-align: center;
}

/* Exact match badge shown on item */
.project-selector__exact-badge {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 1px 5px;
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--todo) 15%, transparent);
color: var(--todo);
flex-shrink: 0;
}

/* Exact match item subtle highlight */
.project-selector__item.exact-match {
background: color-mix(in srgb, var(--todo) 5%, transparent);
}

.project-selector__footer {
margin-top: var(--space-xs);
padding-top: var(--space-xs);
Expand Down Expand Up @@ -279,6 +345,44 @@
color: var(--todo);
}

/* Bookmark star toggle */
.project-selector__bookmark {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px;
background: transparent;
border: none;
border-radius: var(--radius-sm);
color: var(--text-dim);
cursor: pointer;
user-select: none;
opacity: 0;
transition: opacity var(--transition-fast), color var(--transition-fast), background var(--transition-fast);
}

/* Show star on row hover or when bookmarked */
.project-selector__item:hover .project-selector__bookmark,
.project-selector__item.highlighted .project-selector__bookmark,
.project-selector__bookmark.bookmarked {
opacity: 1;
}

.project-selector__bookmark:hover {
color: var(--todo);
background: var(--card-hover);
}

.project-selector__bookmark.bookmarked {
color: var(--todo);
}

.project-selector__bookmark:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}

/* Light theme overrides for project selector */
[data-theme="light"] .project-selector-trigger:hover {
background: var(--card-hover);
Expand Down
Loading
Loading