feat(FN-3198): improve ListView split-pane UX and add agent runs tab

This merge lands six commits that overhaul ListView split-pane UX across the dashboard and add an agent runs tab to both the ListView and AgentDetailView, with auto-expand behavior when a run is selected. It also fixes overflow issues in the workspace selector (desktop) and clamps long labels in the

Fusion-Task-Id: FN-3198
This commit is contained in:
Fusion
2026-05-02 05:07:03 -07:00
committed by gsxdsm
parent 1ddebaa142
commit 93ecf83a1a
6 changed files with 101 additions and 11 deletions

View File

@@ -34,6 +34,24 @@ const MEMORY_LAYER_DESCRIPTIONS: Record<MemoryFileInfo["layer"], string> = {
dreams: "Synthesized patterns and open loops promoted from daily memory.",
};
const MEMORY_FILE_OPTION_LABEL_MAX_CHARS = 72;
function truncateMiddle(value: string, maxChars: number): string {
if (value.length <= maxChars) {
return value;
}
const visibleChars = Math.max(1, maxChars - 1);
const startChars = Math.ceil(visibleChars / 2);
const endChars = Math.floor(visibleChars / 2);
return `${value.slice(0, startChars)}${value.slice(value.length - endChars)}`;
}
function formatMemoryFileOptionLabel(file: MemoryFileInfo): string {
const fullLabel = `${file.label}${file.path}`;
return truncateMiddle(fullLabel, MEMORY_FILE_OPTION_LABEL_MAX_CHARS);
}
interface ParsedInsightCategory {
name: string;
key: string;
@@ -437,8 +455,8 @@ export function MemoryView({ projectId, addToast }: MemoryViewProps) {
disabled={selectedFileDirty}
>
{memoryFiles.map((file) => (
<option key={file.path} value={file.path}>
{file.label} - {file.path}
<option key={file.path} value={file.path} title={`${file.label}${file.path}`}>
{formatMemoryFileOptionLabel(file)}
</option>
))}
</select>

View File

@@ -172,6 +172,23 @@ type SettingsSection = {
const MOBILE_SETTINGS_MEDIA_QUERY = "(max-width: 768px)";
const DEFAULT_MEMORY_EDITOR_PATH = ".fusion/memory/DREAMS.md";
const MEMORY_FILE_OPTION_LABEL_MAX_CHARS = 72;
function truncateMiddle(value: string, maxChars: number): string {
if (value.length <= maxChars) {
return value;
}
const visibleChars = Math.max(1, maxChars - 1);
const startChars = Math.ceil(visibleChars / 2);
const endChars = Math.floor(visibleChars / 2);
return `${value.slice(0, startChars)}${value.slice(value.length - endChars)}`;
}
function formatMemoryFileOptionLabel(file: MemoryFileInfo): string {
const fullLabel = `${file.label}${file.path}`;
return truncateMiddle(fullLabel, MEMORY_FILE_OPTION_LABEL_MAX_CHARS);
}
const SETTINGS_SECTIONS: SettingsSection[] = [
// Account group (scope-less items — independent of settings storage)
@@ -3672,8 +3689,8 @@ export function SettingsModal({
disabled={memoryDirty}
>
{memoryFiles.map((file) => (
<option key={file.path} value={file.path}>
{file.label} - {file.path}
<option key={file.path} value={file.path} title={`${file.label} — ${file.path}`}>
{formatMemoryFileOptionLabel(file)}
</option>
))}
</select>

View File

@@ -41,11 +41,12 @@
.workspace-selector-menu {
position: absolute;
top: calc(100% + 8px);
left: 0;
top: calc(100% + var(--space-sm));
left: auto;
right: 0;
z-index: 20;
min-width: 300px;
max-width: 360px;
max-width: min(360px, calc(100vw - var(--space-xl)));
padding: 8px;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
@@ -117,10 +118,8 @@
}
.workspace-selector-menu {
left: auto;
right: 0;
min-width: min(300px, calc(100vw - 24px));
max-width: calc(100vw - 24px);
min-width: min(300px, calc(100vw - var(--space-xl)));
max-width: calc(100vw - var(--space-xl));
}
.workspace-selector-option-meta {

View File

@@ -278,6 +278,30 @@ describe("MemoryView", () => {
const button = screen.getByRole("button", { name: /Dreaming…/i });
expect(button).toBeDisabled();
});
it("truncates long memory file option labels to avoid wide native dropdown expansion", () => {
mockUseMemoryData.mockReturnValue(
createMemoryData({
memoryFiles: [
{
path: ".fusion/memory/very/deep/path/that/keeps/growing/until/the/browser/native/select/dropdown/can-overflow-on-the-right-edge.md",
label: "Long-term memory",
layer: "long-term",
size: 12,
updatedAt: "2026-04-17T12:00:00.000Z",
},
],
selectedFilePath: ".fusion/memory/very/deep/path/that/keeps/growing/until/the/browser/native/select/dropdown/can-overflow-on-the-right-edge.md",
}),
);
render(<MemoryView addToast={vi.fn()} />);
const option = screen.getByRole("option", { name: /Long-term memory/ });
expect(option.textContent).toContain("…");
expect(option.textContent).not.toContain("dropdown/can-overflow-on-the-right-edge.md");
});
it("hides Dream Now button when dreams are disabled", () => {
render(<MemoryView addToast={vi.fn()} />);

View File

@@ -1209,6 +1209,31 @@ describe("SettingsModal", () => {
expect(checkbox).toBeChecked();
});
it("truncates long memory file option labels to keep native dropdown width bounded", async () => {
mockFetchMemoryFiles.mockResolvedValue({
files: [
{
path: ".fusion/memory/very/deep/path/that/keeps/growing/until/the/browser/native/select/dropdown/can-overflow-on-the-right-edge.md",
label: "Long-term memory",
layer: "long-term",
size: 42,
updatedAt: "2026-04-17T12:00:00.000Z",
},
],
});
renderModal();
await waitFor(() => {
expect(mockFetchSettings).toHaveBeenCalled();
});
await userEvent.click(await screen.findByText("Memory"));
const option = await screen.findByRole("option", { name: /Long-term memory/ });
expect(option.textContent).toContain("…");
expect(option.textContent).not.toContain("dropdown/can-overflow-on-the-right-edge.md");
});
it("shows only loading copy while backend status is unresolved", async () => {
mockUseMemoryBackendStatus.mockReturnValue({
// Simulate stale negative payload while a refresh is still in-flight.

View File

@@ -3,6 +3,7 @@ import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { WorkspaceSelector } from "../WorkspaceSelector";
import type { WorkspaceInfo } from "../../hooks/useWorkspaces";
import { loadAllAppCss } from "../../test/cssFixture";
const workspaces: WorkspaceInfo[] = [
{
@@ -89,4 +90,10 @@ describe("WorkspaceSelector", () => {
expect(screen.getByText(/Implement a very long task title that shoul/)).toBeInTheDocument();
expect(screen.getByText(/…$/)).toBeInTheDocument();
});
it("anchors the menu to the trigger's right edge on desktop to avoid right overflow", () => {
const css = loadAllAppCss();
expect(css).toMatch(/\.workspace-selector-menu\s*\{[^}]*left:\s*auto;[^}]*right:\s*0;/);
});
});