Fix engine controls and job detail rendering

This commit is contained in:
2026-03-30 08:48:58 -04:00
parent f47a61a9ea
commit d17c8dbe23
10 changed files with 521 additions and 102 deletions

View File

@@ -0,0 +1,94 @@
import { expect, test } from "@playwright/test";
import { fulfillJson, mockDashboardData, mockEngineStatus } from "./helpers";
test.use({ storageState: undefined });
test.beforeEach(async ({ page }) => {
await mockEngineStatus(page);
await mockDashboardData(page);
await page.route("**/api/stats/daily", async (route) => {
await fulfillJson(route, 200, []);
});
await page.route("**/api/settings/watch-dirs**", async (route) => {
await fulfillJson(route, 200, []);
});
await page.route("**/api/profiles/presets", async (route) => {
await fulfillJson(route, 200, []);
});
await page.route("**/api/profiles", async (route) => {
await fulfillJson(route, 200, []);
});
});
test("Last 7 Days panel is shown on dashboard", async ({ page }) => {
await page.unroute("**/api/stats/daily");
await page.route("**/api/stats/daily", async (route) => {
await fulfillJson(route, 200, [
{
date: "2026-03-22",
jobs_completed: 5,
bytes_saved: 1_073_741_824,
},
{
date: "2026-03-23",
jobs_completed: 3,
bytes_saved: 536_870_912,
},
]);
});
await page.goto("/");
await expect(page.getByText("Last 7 Days")).toBeVisible();
await expect(page.getByText("Space recovered")).toBeVisible();
await expect(page.getByText("Jobs completed")).toBeVisible();
});
test("ENGINE PAUSED banner is shown when engine is paused and mentions auto-analysis", async ({
page,
}) => {
await page.goto("/");
await expect(page.getByText("ENGINE PAUSED")).toBeVisible();
await expect(page.getByText(/Analysis runs automatically/i)).toBeVisible();
await expect(page.getByText(/won't start encoding until/i)).not.toBeVisible();
});
test("ENGINE PAUSED banner is hidden when engine is running", async ({ page }) => {
await page.unroute("**/api/engine/status");
await page.unroute("**/api/engine/mode");
await mockEngineStatus(page, {
status: "running",
manual_paused: false,
});
await page.goto("/");
await expect(page.getByText("ENGINE PAUSED")).not.toBeVisible();
});
test("About modal opens and does not contain Al badge", async ({ page }) => {
await page.route("**/api/system/info", async (route) => {
await fulfillJson(route, 200, {
version: "0.3.0",
os_version: "macos aarch64",
is_docker: false,
telemetry_enabled: false,
ffmpeg_version: "N-12345",
});
});
await page.goto("/");
await page.getByRole("button", { name: "About" }).click();
await expect(page.getByRole("dialog")).toBeVisible();
await expect(page.getByRole("heading", { name: "Alchemist" })).toBeVisible();
await expect(page.getByText("v0.3.0")).toBeVisible();
await expect(page.getByText(/^Al$/)).toHaveCount(0);
});
test("Settings page does not show Setup & Runtime Controls banner", async ({ page }) => {
await page.goto("/settings");
await expect(page.getByText("Setup & Runtime Controls")).not.toBeVisible();
});

View File

@@ -1,21 +1,23 @@
import { expect, test } from "@playwright/test";
import { createEngineStatus, fulfillJson, mockDashboardData } from "./helpers";
import {
createEngineMode,
createEngineStatus,
fulfillJson,
mockDashboardData,
} from "./helpers";
test.use({ storageState: undefined });
test("start, stop, cancel stop, and pause update dashboard engine controls", async ({
test("engine cycle: Start transitions to Running, Stop transitions to Stopping", async ({
page,
}) => {
let engineStatus = createEngineStatus({
status: "paused",
manual_paused: true,
scheduler_paused: false,
draining: false,
});
let resumeCalls = 0;
let pauseCalls = 0;
let drainCalls = 0;
let stopDrainCalls = 0;
await mockDashboardData(page);
@@ -23,80 +25,46 @@ test("start, stop, cancel stop, and pause update dashboard engine controls", asy
await fulfillJson(route, 200, engineStatus);
});
await page.route("**/api/engine/mode", async (route) => {
await fulfillJson(route, 200, {
mode: engineStatus.mode,
is_manual_override: engineStatus.is_manual_override,
concurrent_limit: engineStatus.concurrent_limit,
cpu_count: 8,
computed_limits: {
background: 1,
balanced: 4,
throughput: 4,
},
});
await fulfillJson(route, 200, createEngineMode());
});
await page.route("**/api/engine/resume", async (route) => {
resumeCalls += 1;
engineStatus = createEngineStatus({
status: "running",
manual_paused: false,
scheduler_paused: false,
draining: false,
});
await fulfillJson(route, 200, { status: "running" });
});
await page.route("**/api/engine/pause", async (route) => {
pauseCalls += 1;
engineStatus = createEngineStatus({
status: "paused",
manual_paused: true,
scheduler_paused: false,
draining: false,
});
await fulfillJson(route, 200, { status: "paused" });
});
await page.route("**/api/engine/drain", async (route) => {
drainCalls += 1;
engineStatus = createEngineStatus({
status: "draining",
manual_paused: false,
scheduler_paused: false,
draining: true,
});
await fulfillJson(route, 200, { status: "draining" });
});
await page.route("**/api/engine/stop-drain", async (route) => {
stopDrainCalls += 1;
engineStatus = createEngineStatus({
status: "running",
manual_paused: false,
scheduler_paused: false,
draining: false,
});
await fulfillJson(route, 200, { status: "running" });
});
await page.goto("/");
await expect(page.getByText("Paused", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Start" })).toBeVisible();
await expect(page.getByRole("button", { name: "Pause" })).not.toBeVisible();
await expect(page.getByRole("button", { name: "Cancel Stop" })).not.toBeVisible();
await page.getByRole("button", { name: "Start" }).click();
await expect.poll(() => resumeCalls).toBe(1);
await expect(page.getByText("Running", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Pause" })).toBeVisible();
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible();
await expect(page.getByRole("button", { name: "Pause" })).not.toBeVisible();
await page.getByRole("button", { name: "Stop" }).click();
await expect.poll(() => drainCalls).toBe(1);
await expect(page.getByText("Draining", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Cancel Stop" })).toBeVisible();
await page.getByRole("button", { name: "Cancel Stop" }).click();
await expect.poll(() => stopDrainCalls).toBe(1);
await expect(page.getByText("Running", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Pause" }).click();
await expect.poll(() => pauseCalls).toBe(1);
await expect(page.getByText("Paused", { exact: true })).toBeVisible();
await expect(page.getByText("Stopping", { exact: true })).toBeVisible();
await expect(page.getByText("Draining", { exact: true })).not.toBeVisible();
await expect(page.getByRole("button", { name: "Cancel Stop" })).not.toBeVisible();
await expect(page.getByRole("button", { name: /Stopping/i })).toBeDisabled();
});
test("scheduler pause note is shown when pause comes from automation", async ({ page }) => {
@@ -113,17 +81,7 @@ test("scheduler pause note is shown when pause comes from automation", async ({
);
});
await page.route("**/api/engine/mode", async (route) => {
await fulfillJson(route, 200, {
mode: "balanced",
is_manual_override: false,
concurrent_limit: 2,
cpu_count: 8,
computed_limits: {
background: 1,
balanced: 4,
throughput: 4,
},
});
await fulfillJson(route, 200, createEngineMode());
});
await page.goto("/");
@@ -137,17 +95,7 @@ test("failed engine transitions surface an error toast", async ({ page }) => {
await fulfillJson(route, 200, createEngineStatus());
});
await page.route("**/api/engine/mode", async (route) => {
await fulfillJson(route, 200, {
mode: "balanced",
is_manual_override: false,
concurrent_limit: 2,
cpu_count: 8,
computed_limits: {
background: 1,
balanced: 4,
throughput: 4,
},
});
await fulfillJson(route, 200, createEngineMode());
});
await page.route("**/api/engine/resume", async (route) => {
await fulfillJson(route, 500, { message: "resume failed" });

View File

@@ -151,7 +151,7 @@ test("search requests are debounced and failed job details show summary and logs
await page.getByTitle("/media/failed.mkv").click();
await expect(page.getByRole("dialog")).toBeVisible();
await expect(page.getByText("What went wrong")).toBeVisible();
await expect(page.getByText("Failure Reason")).toBeVisible();
await expect(page.getByText("Unknown encoder 'missing_encoder'").first()).toBeVisible();
await page.getByText(/Show FFmpeg output \(2 lines\)/).click();
await expect(page.getByText("frame=5 fps=10")).toBeVisible();
@@ -267,3 +267,141 @@ test("detail modal delete action removes the job and closes the modal", async ({
await expect(page.getByRole("dialog")).toHaveCount(0);
await expect(page.getByTitle("/media/completed.mkv")).toHaveCount(0);
});
test("queued job with no metadata shows waiting for analysis placeholder", async ({ page }) => {
await page.route("**/api/jobs/table**", async (route) => {
await fulfillJson(route, 200, [queuedJob]);
});
await mockJobDetails(page, {
1: {
job: queuedJob,
job_logs: [],
},
});
await page.goto("/jobs");
await page.getByTitle("/media/queued.mkv").click();
await expect(page.getByRole("dialog")).toBeVisible();
await expect(page.getByText("Waiting for analysis")).toBeVisible();
await expect(page.getByText("Unknown bit depth")).not.toBeVisible();
});
test("analyzed queued job shows real metadata in detail modal", async ({ page }) => {
const analyzedJob: JobFixture = {
...queuedJob,
id: 10,
input_path: "/media/analyzed.mkv",
output_path: "/output/analyzed-av1.mkv",
};
await page.route("**/api/jobs/table**", async (route) => {
await fulfillJson(route, 200, [analyzedJob]);
});
await mockJobDetails(page, {
10: {
job: analyzedJob,
metadata: {
duration_secs: 3600,
codec_name: "h264",
width: 1920,
height: 1080,
bit_depth: 8,
size_bytes: 4_000_000_000,
video_bitrate_bps: 8_000_000,
container_bitrate_bps: 8_200_000,
fps: 23.976,
container: "mkv",
audio_codec: "aac",
audio_channels: 2,
dynamic_range: "sdr",
},
job_logs: [],
},
});
await page.goto("/jobs");
await page.getByTitle("/media/analyzed.mkv").click();
await expect(page.getByRole("dialog")).toBeVisible();
await expect(page.getByText("h264")).toBeVisible();
await expect(page.getByText("1920x1080")).toBeVisible();
await expect(page.getByText("Waiting for analysis")).not.toBeVisible();
});
test("skipped job shows humanized skip reason with measured values", async ({ page }) => {
const skippedJob: JobFixture = {
id: 20,
input_path: "/media/already-small.mkv",
output_path: "/output/already-small-av1.mkv",
status: "skipped",
priority: 0,
progress: 0,
decision_reason: "bpp_below_threshold|bpp=0.043,threshold=0.050",
created_at: "2025-01-01T00:00:00Z",
updated_at: "2025-01-01T00:00:00Z",
};
await page.route("**/api/jobs/table**", async (route) => {
await fulfillJson(route, 200, [skippedJob]);
});
await mockJobDetails(page, {
20: {
job: skippedJob,
metadata: {
duration_secs: 1800,
codec_name: "hevc",
width: 1920,
height: 1080,
bit_depth: 10,
size_bytes: 2_000_000_000,
fps: 24,
container: "mkv",
dynamic_range: "sdr",
},
job_logs: [],
},
});
await page.goto("/jobs");
await page.getByTitle("/media/already-small.mkv").click();
await expect(page.getByRole("dialog")).toBeVisible();
await expect(page.getByText("Already efficiently compressed")).toBeVisible();
await expect(page.getByText("0.043", { exact: true })).toBeVisible();
await expect(page.getByText("0.050", { exact: true })).toBeVisible();
await expect(page.getByText("bpp_below_threshold|")).not.toBeVisible();
});
test("failed job with no failure summary shows fallback message", async ({ page }) => {
const mysteryFailJob: JobFixture = {
id: 30,
input_path: "/media/mystery.mkv",
output_path: "/output/mystery-av1.mkv",
status: "failed",
priority: 0,
progress: 0,
created_at: "2025-01-01T00:00:00Z",
updated_at: "2025-01-01T00:00:00Z",
};
await page.route("**/api/jobs/table**", async (route) => {
await fulfillJson(route, 200, [mysteryFailJob]);
});
await mockJobDetails(page, {
30: {
job: mysteryFailJob,
metadata: undefined,
encode_stats: undefined,
job_logs: [],
job_failure_summary: null,
} as unknown as JobDetailFixture,
});
await page.goto("/jobs");
await page.getByTitle("/media/mystery.mkv").click();
await expect(page.getByRole("dialog")).toBeVisible();
await expect(page.getByText("Failure Reason")).toBeVisible();
await expect(page.getByText(/No error details captured/)).toBeVisible();
});

View File

@@ -0,0 +1,167 @@
import { expect, test } from "@playwright/test";
import {
createSettingsBundle,
fulfillEmpty,
fulfillJson,
mockEngineStatus,
mockSettingsBundle,
} from "./helpers";
test.use({ storageState: undefined });
test.beforeEach(async ({ page }) => {
await mockEngineStatus(page);
await page.route("**/api/library/profiles", async (route) => {
await fulfillJson(route, 200, []);
});
await page.route("**/api/profiles/presets", async (route) => {
await fulfillJson(route, 200, []);
});
await page.route("**/api/profiles", async (route) => {
await fulfillJson(route, 200, []);
});
await page.route("**/api/scan/status", async (route) => {
await fulfillJson(route, 200, {
is_running: false,
files_found: 0,
current_folder: null,
});
});
});
test("library intake shows unified folder list with no Library Directories or Watch Folders headings", async ({
page,
}) => {
await mockSettingsBundle(page, {
scanner: {
directories: ["/media/movies"],
watch_enabled: true,
extra_watch_dirs: [],
},
});
await page.route("**/api/settings/watch-dirs**", async (route) => {
await fulfillJson(route, 200, [
{
id: 1,
path: "/media/tv",
is_recursive: true,
profile_id: null,
},
]);
});
await page.goto("/settings?tab=watch");
await expect(page.getByText("/media/movies")).toBeVisible();
await expect(page.getByText("/media/tv")).toBeVisible();
await expect(page.getByRole("heading", { name: "Library Directories" })).not.toBeVisible();
await expect(page.getByRole("heading", { name: "Watch Folders" })).not.toBeVisible();
await expect(page.getByText(/watch subdirectories recursively/i)).not.toBeVisible();
});
test("Scan Now button is present on Library & Intake page", async ({ page }) => {
await mockSettingsBundle(page);
await page.route("**/api/settings/watch-dirs**", async (route) => {
await fulfillJson(route, 200, []);
});
await page.goto("/settings?tab=watch");
await expect(page.getByRole("button", { name: /scan now/i })).toBeVisible();
});
test("adding a folder via text input calls bundle and watch-dirs APIs", async ({ page }) => {
let bundlePutCalled = false;
let watchDirsPostCalled = false;
await page.route("**/api/settings/bundle", async (route) => {
if (route.request().method() === "PUT") {
bundlePutCalled = true;
await fulfillJson(route, 200, {});
return;
}
await fulfillJson(
route,
200,
createSettingsBundle({
scanner: {
directories: [],
watch_enabled: true,
extra_watch_dirs: [],
},
}),
);
});
await page.route("**/api/settings/watch-dirs**", async (route) => {
if (route.request().method() === "POST") {
watchDirsPostCalled = true;
await fulfillJson(route, 201, {
id: 99,
path: "/media/new",
is_recursive: true,
profile_id: null,
});
return;
}
await fulfillJson(route, 200, []);
});
await page.goto("/settings?tab=watch");
await page.getByPlaceholder(/path/i).fill("/media/new");
await page.getByRole("button", { name: "Add" }).click();
await expect.poll(() => bundlePutCalled).toBe(true);
await expect.poll(() => watchDirsPostCalled).toBe(true);
});
test("deleting a folder calls bundle PUT and watch-dirs DELETE", async ({ page }) => {
let bundlePutCalled = false;
let watchDirDeleteCalled = false;
await page.route("**/api/settings/bundle", async (route) => {
if (route.request().method() === "PUT") {
bundlePutCalled = true;
await fulfillJson(route, 200, {});
return;
}
await fulfillJson(
route,
200,
createSettingsBundle({
scanner: {
directories: ["/media/movies"],
watch_enabled: true,
extra_watch_dirs: [],
},
}),
);
});
await page.route("**/api/settings/watch-dirs**", async (route) => {
if (route.request().method() === "DELETE") {
watchDirDeleteCalled = true;
await fulfillEmpty(route, 204);
return;
}
await fulfillJson(route, 200, [
{
id: 1,
path: "/media/movies",
is_recursive: true,
profile_id: null,
},
]);
});
await page.goto("/settings?tab=watch");
await expect(page.getByText("/media/movies")).toBeVisible();
await page.getByRole("button", { name: "Remove /media/movies" }).click();
await page.getByRole("dialog").getByRole("button", { name: "Remove" }).click();
await expect.poll(() => bundlePutCalled).toBe(true);
await expect.poll(() => watchDirDeleteCalled).toBe(true);
});

View File

@@ -216,7 +216,7 @@ export default function Dashboard() {
{/* Stat row — compact horizontal strip */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<StatCard label="Active Jobs" value={stats.active} icon={Zap} colorClass="text-helios-solar" />
<StatCard label="Completed" value={stats.completed} icon={CheckCircle2} colorClass="text-emerald-500" />
<StatCard label="Completed" value={stats.completed} icon={CheckCircle2} colorClass="text-status-success" />
<StatCard label="Failed" value={stats.failed} icon={AlertCircle} colorClass="text-status-error" />
<StatCard label="Total Processed" value={stats.total} icon={Database} colorClass="text-helios-solar" />
</div>
@@ -259,7 +259,7 @@ export default function Dashboard() {
<div className="flex items-center gap-3 min-w-0">
<div className={`w-1.5 h-1.5 rounded-full shrink-0 ${
s === "completed"
? "bg-emerald-500"
? "bg-status-success"
: s === "failed"
? "bg-status-error"
: s === "encoding" || s === "analyzing"

View File

@@ -38,6 +38,8 @@ export default function HeaderActions() {
},
} as const;
const status = engineStatus?.status ?? "paused";
const refreshEngineStatus = async () => {
const data = await apiJson<EngineStatus>("/api/engine/status");
setEngineStatus(data);
@@ -83,6 +85,17 @@ export default function HeaderActions() {
};
}, []);
// Fast poll during draining state for responsive UI
useEffect(() => {
if (status !== "draining") return;
const id = window.setInterval(() => {
void refreshEngineStatus();
}, 1000);
return () => window.clearInterval(id);
}, [status]);
const handleStart = async () => {
setEngineLoading(true);
try {
@@ -128,8 +141,6 @@ export default function HeaderActions() {
}
};
const status = engineStatus?.status ?? "paused";
return (
<>
<div className="flex items-center gap-2">

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { createPortal } from "react-dom";
import {
Search, RefreshCw, Trash2, Ban,
Clock, X, Info, Activity, Database, Zap, Maximize2, MoreHorizontal, ArrowDown, ArrowUp, AlertCircle
@@ -374,6 +375,72 @@ export default function JobManager() {
};
}, []);
useEffect(() => {
let eventSource: EventSource | null = null;
let cancelled = false;
let reconnectTimeout: number | null = null;
const connect = () => {
if (cancelled) return;
eventSource?.close();
eventSource = new EventSource("/api/events");
eventSource.addEventListener("status", (e) => {
try {
const { job_id, status } = JSON.parse(e.data) as {
job_id: number;
status: string;
};
setJobs((prev) =>
prev.map((job) =>
job.id === job_id ? { ...job, status } : job
)
);
} catch {
/* ignore malformed */
}
});
eventSource.addEventListener("progress", (e) => {
try {
const { job_id, percentage } = JSON.parse(e.data) as {
job_id: number;
percentage: number;
};
setJobs((prev) =>
prev.map((job) =>
job.id === job_id ? { ...job, progress: percentage } : job
)
);
} catch {
/* ignore malformed */
}
});
eventSource.addEventListener("decision", () => {
// Re-fetch full job list when decisions are made
void fetchJobsRef.current();
});
eventSource.onerror = () => {
eventSource?.close();
if (!cancelled) {
reconnectTimeout = window.setTimeout(connect, 3000);
}
};
};
connect();
return () => {
cancelled = true;
eventSource?.close();
if (reconnectTimeout !== null) {
window.clearTimeout(reconnectTimeout);
}
};
}, []);
useEffect(() => {
if (!menuJobId) return;
const handleClick = (event: MouseEvent) => {
@@ -1027,18 +1094,19 @@ export default function JobManager() {
</button>
</div>
{/* Detail Overlay */}
<AnimatePresence>
{focusedJob && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setFocusedJob(null)}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100]"
/>
<div className="fixed inset-0 flex items-center justify-center pointer-events-none z-[101]">
{/* Detail Overlay - rendered via portal to escape layout constraints */}
{typeof document !== "undefined" && createPortal(
<AnimatePresence>
{focusedJob && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setFocusedJob(null)}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100]"
/>
<div className="fixed inset-0 flex items-center justify-center pointer-events-none z-[101]">
<motion.div
key="modal-content"
initial={{ opacity: 0, scale: 0.95, y: 10 }}
@@ -1396,7 +1464,9 @@ export default function JobManager() {
</div>
</>
)}
</AnimatePresence>
</AnimatePresence>,
document.body
)}
<ConfirmDialog
open={confirmState !== null}

View File

@@ -5,7 +5,6 @@ import LibraryStep from "./setup/LibraryStep";
import ProcessingStep from "./setup/ProcessingStep";
import ReviewStep from "./setup/ReviewStep";
import RuntimeStep from "./setup/RuntimeStep";
import ScanStep from "./setup/ScanStep";
import SetupFrame from "./setup/SetupFrame";
import {
DEFAULT_NOTIFICATION_DRAFT,
@@ -41,7 +40,6 @@ export default function SetupWizard() {
const [notificationDraft, setNotificationDraft] = useState<NotificationTargetConfig>(DEFAULT_NOTIFICATION_DRAFT);
const [recommendations, setRecommendations] = useState<FsRecommendation[]>([]);
const [preview, setPreview] = useState<FsPreviewResponse | null>(null);
const [scanRunId, setScanRunId] = useState(0);
const validatorRef = useRef<StepValidator>(async () => null);
const registerValidator = useCallback((validator: StepValidator) => {
@@ -103,8 +101,7 @@ export default function SetupWizard() {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "setup_complete", value: "true" }),
}).catch(() => undefined);
setStep(6);
setScanRunId((current) => current + 1);
window.location.href = "/";
} catch (err) {
let message = "Failed to save setup configuration.";
if (isApiError(err)) {
@@ -207,8 +204,6 @@ export default function SetupWizard() {
);
case 5:
return <ReviewStep setupSummary={setupSummary} settings={settings} preview={preview} error={null} />;
case 6:
return <ScanStep runId={scanRunId} onBackToReview={() => setStep(5)} />;
default:
return null;
}

View File

@@ -359,10 +359,6 @@ export default function WatchFolders() {
<div className="space-y-6" aria-live="polite">
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<h2 className="flex items-center gap-2 text-xl font-semibold text-helios-ink">
<FolderOpen size={20} className="text-helios-solar" />
Media Folders
</h2>
<p className="text-sm text-helios-slate">
Folders Alchemist scans and watches for new media.
</p>

View File

@@ -245,7 +245,7 @@ export default function LibraryStep({
: "rounded-lg px-2 py-1 transition-colors hover:bg-helios-surface-soft hover:text-helios-ink"
}
>
{crumb.name}
{crumb.name.replace(/^\//, "")}
</button>
</div>
);
@@ -316,7 +316,7 @@ export default function LibraryStep({
..
</span>
<span className="block truncate text-xs text-helios-slate">
Go up to {parentBreadcrumb.name}
Go up to {parentBreadcrumb.name.replace(/^\//, "")}
</span>
</div>
</button>
@@ -369,7 +369,7 @@ export default function LibraryStep({
disabled={!currentBrowsePath}
className="shrink-0 rounded-lg bg-helios-solar px-4 py-2 text-sm font-semibold text-helios-main transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
>
Add {currentBrowseName}
Add this folder
</button>
</div>
</div>