diff --git a/.changeset/fn-7027-skill-list-truncation.md b/.changeset/fn-7027-skill-list-truncation.md
new file mode 100644
index 0000000000..898eb2b6c4
--- /dev/null
+++ b/.changeset/fn-7027-skill-list-truncation.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Prevent long Skills list rows from overflowing the left pane.
+category: fix
+dev: Constrains SkillsView discovered-skill name, path, and source rows with ellipsis truncation.
diff --git a/packages/dashboard/app/components/SkillsView.css b/packages/dashboard/app/components/SkillsView.css
index 249dd584a9..2ae49ba676 100644
--- a/packages/dashboard/app/components/SkillsView.css
+++ b/packages/dashboard/app/components/SkillsView.css
@@ -192,7 +192,17 @@ The search controls at the top of the Skills list need breathing room below the
flex: 1;
}
+/*
+FNXC:Skills 2026-06-25-00:00:
+Left Discovered Skills rows must keep compact, intentional text sizing and truncate every text line with ellipsis so long names, paths, or sources never force horizontal scroll in wide clamped, narrow dock, or mobile list layouts. The chevron remains fixed-size while only the skill-name text truncates.
+*/
.skills-view-item-name {
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ max-width: 100%;
+ min-width: 0;
+ font-size: 0.9em;
font-weight: 500;
color: var(--text);
overflow: hidden;
@@ -200,7 +210,20 @@ The search controls at the top of the Skills list need breathing room below the
white-space: nowrap;
}
+.skills-view-item-name svg {
+ flex-shrink: 0;
+}
+
+.skills-view-item-name-text {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.skills-view-item-path {
+ max-width: 100%;
+ font-size: 0.85em;
color: var(--text-muted);
font-family: var(--font-mono);
overflow: hidden;
@@ -209,9 +232,14 @@ The search controls at the top of the Skills list need breathing room below the
}
.skills-view-item-source {
+ max-width: 100%;
+ font-size: 0.85em;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: calc(var(--space-xs) / 8);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
/* Toggle switch */
diff --git a/packages/dashboard/app/components/SkillsView.tsx b/packages/dashboard/app/components/SkillsView.tsx
index c818f990af..b314ed99b0 100644
--- a/packages/dashboard/app/components/SkillsView.tsx
+++ b/packages/dashboard/app/components/SkillsView.tsx
@@ -563,7 +563,7 @@ export function SkillsView({ projectId, addToast, onClose }: SkillsViewProps) {
{isSelected ? : }
- {skill.name}
+ {skill.name}
{skill.relativePath}
{skill.metadata.source}
diff --git a/packages/dashboard/app/components/__tests__/SkillsView.test.tsx b/packages/dashboard/app/components/__tests__/SkillsView.test.tsx
index ef74086244..0fb913c2f1 100644
--- a/packages/dashboard/app/components/__tests__/SkillsView.test.tsx
+++ b/packages/dashboard/app/components/__tests__/SkillsView.test.tsx
@@ -607,6 +607,70 @@ describe("SkillsView", () => {
});
});
+ it("declares ellipsis truncation for long and short discovered-skill row text", async () => {
+ const longToken = "skill".repeat(30);
+ const longSkill: DiscoveredSkill = {
+ id: "github::skills/" + longToken,
+ name: longToken,
+ path: "/project/.fusion/skills/" + longToken,
+ relativePath: "skills/" + longToken + "/nested/" + longToken,
+ enabled: true,
+ metadata: {
+ source: "github.com/example/" + longToken + "/" + longToken,
+ scope: "project",
+ origin: "package",
+ },
+ };
+ const emptyMetadataSkill = {
+ id: "local::skills/empty-metadata",
+ name: "empty-metadata-skill",
+ path: "/project/.fusion/skills/empty-metadata",
+ relativePath: "",
+ enabled: false,
+ metadata: {
+ scope: "project",
+ origin: "top-level",
+ },
+ } as unknown as DiscoveredSkill;
+ mockFetchDiscoveredSkills.mockResolvedValue([
+ mockDiscoveredSkills[0],
+ longSkill,
+ emptyMetadataSkill,
+ ]);
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText(longSkill.name)).toBeTruthy();
+ expect(screen.getByText("test-skill")).toBeTruthy();
+ expect(screen.getByText("empty-metadata-skill")).toBeTruthy();
+ });
+
+ const assertTruncation = (element: Element | null) => {
+ expect(element).toBeTruthy();
+ const styles = getComputedStyle(element as Element);
+ expect(styles.overflow).toBe("hidden");
+ expect(styles.textOverflow).toBe("ellipsis");
+ expect(styles.whiteSpace).toBe("nowrap");
+ };
+
+ const longRow = screen.getByText(longSkill.name).closest(".skills-view-item");
+ const shortRow = screen.getByText("test-skill").closest(".skills-view-item");
+ const emptyMetadataRow = screen.getByText("empty-metadata-skill").closest(".skills-view-item");
+
+ assertTruncation(longRow?.querySelector(".skills-view-item-name-text") ?? null);
+ assertTruncation(longRow?.querySelector(".skills-view-item-path") ?? null);
+ assertTruncation(longRow?.querySelector(".skills-view-item-source") ?? null);
+ assertTruncation(shortRow?.querySelector(".skills-view-item-name-text") ?? null);
+ assertTruncation(shortRow?.querySelector(".skills-view-item-path") ?? null);
+ assertTruncation(shortRow?.querySelector(".skills-view-item-source") ?? null);
+
+ expect(longRow?.querySelector(".skills-view-item-name svg")).toBeTruthy();
+ expect(longRow?.querySelector(".skills-view-item-toggle")).toBeTruthy();
+ expect(emptyMetadataRow?.querySelector(".skills-view-item-path")?.textContent).toBe("");
+ expect(emptyMetadataRow?.querySelector(".skills-view-item-source")?.textContent).toBe("");
+ });
+
it("renders toggle sliders with .skills-view-toggle-slider class", async () => {
render();