Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1077677
Move schedules beside Plugins & Skills
brsbl Jul 29, 2026
c9b36a8
Add Tools Hub navigation stories
brsbl Jul 29, 2026
d2c803e
Separate Automations from Extensions
brsbl Jul 29, 2026
2283b74
Add multiple plugin pages story
brsbl Jul 29, 2026
4efb857
Show all bundled plugin pages in story
brsbl Jul 29, 2026
1601f8e
Consolidate Extensions sidebar stories
brsbl Jul 29, 2026
6dfc788
Show Plugins before Skills in Extensions
brsbl Jul 29, 2026
9d6666a
Clarify plugin details and automation stories
brsbl Jul 29, 2026
12bed3f
Refine plugin capability cards
brsbl Jul 29, 2026
f3e922b
Lighten Release and anchor Includes capability cards
brsbl Jul 29, 2026
5f71671
Use the house collection pattern for Includes and Activity
brsbl Jul 30, 2026
fdfd22c
Refine plugin capabilities and health
brsbl Jul 30, 2026
7d33a77
Lift plugin runtime health alerts
brsbl Jul 30, 2026
0ce7d3a
Rebuild the plugin detail page around Capabilities
brsbl Jul 31, 2026
4d810d9
Tighten the plugin detail tables to the house row density
brsbl Jul 31, 2026
1719bb7
Make plugin banners full-width bars above the page
brsbl Jul 31, 2026
5a17fa1
Move plugin version and install date into the header
brsbl Jul 31, 2026
891c71c
Flatten the plugin detail tables
brsbl Jul 31, 2026
01a05fe
Add the plugin detail design-pass handoff
brsbl Jul 31, 2026
fe40d59
Polish resource detail states
brsbl Aug 1, 2026
bc8dfb1
refine plugin detail release and provenance UI
brsbl Aug 2, 2026
34fc00d
polish automation detail surfaces
brsbl Aug 2, 2026
0b52be7
refine automation detail and browse states
brsbl Aug 2, 2026
711d48e
Organize plugin sidebar and Ladle stories
brsbl Aug 2, 2026
55e1f1a
Fix plugin and automation review findings
brsbl Aug 2, 2026
b53daf6
Align plugin tables and automation metadata icons
brsbl Aug 2, 2026
1735346
Fix tools hub review findings
brsbl Aug 2, 2026
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
5 changes: 5 additions & 0 deletions apps/app/.ladle/ladle.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
@import "../src/app.css";
@source "../src";
@source ".";
/* Plugin-owned views render in these stories but live outside apps/app, so
Tailwind never saw their classes: anything a plugin used that the app did not
silently rendered unstyled here while working in the real app, which builds
the plugin's CSS separately. */
@source "../../../plugins/automations";

.ladle-main {
padding: 0;
Expand Down
88 changes: 88 additions & 0 deletions apps/app/src/App.legacy-automation-routes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// @vitest-environment jsdom

import type { ReactNode } from "react";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import {
LegacyAutomationCollectionRedirect,
LegacyAutomationDetailRedirect,
} from "./App";
import {
LEGACY_AUTOMATION_DETAIL_ROUTE_PATH,
LEGACY_AUTOMATIONS_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_BROWSE_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_DETAIL_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_EDIT_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATIONS_ROUTE_PATH,
} from "./lib/route-paths";

function LocationPath() {
const location = useLocation();
return <span>{`${location.pathname}${location.search}`}</span>;
}

function renderRedirect(path: string, pattern: string, element: ReactNode) {
render(
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route path={pattern} element={element} />
<Route path="*" element={<LocationPath />} />
</Routes>
</MemoryRouter>,
);
}

describe("legacy automation route redirects", () => {
afterEach(cleanup);

it.each([
[
"/tools/automations",
LEGACY_TOOLS_AUTOMATIONS_ROUTE_PATH,
"/plugins/automations/automations",
],
[
"/tools/automations?view=browse",
LEGACY_TOOLS_AUTOMATIONS_ROUTE_PATH,
"/plugins/automations/automations/browse",
],
[
"/tools/automations/browse",
LEGACY_TOOLS_AUTOMATION_BROWSE_ROUTE_PATH,
"/plugins/automations/automations/browse",
],
[
"/automations",
LEGACY_AUTOMATIONS_ROUTE_PATH,
"/plugins/automations/automations",
],
])("redirects %s to %s", (from, pattern, to) => {
renderRedirect(from, pattern, <LegacyAutomationCollectionRedirect />);
expect(screen.getByText(to)).toBeTruthy();
});

it.each([
[
"/tools/automations/proj_1/auto_1",
LEGACY_TOOLS_AUTOMATION_DETAIL_ROUTE_PATH,
"/plugins/automations/automations/proj_1/auto_1",
],
[
"/tools/automations/proj_1/auto_1/edit",
LEGACY_TOOLS_AUTOMATION_EDIT_ROUTE_PATH,
"/plugins/automations/automations/proj_1/auto_1/edit",
],
[
"/automations/proj_1/auto_1",
LEGACY_AUTOMATION_DETAIL_ROUTE_PATH,
"/plugins/automations/automations/proj_1/auto_1",
],
])(
"preserves automation identity when redirecting %s",
(from, pattern, to) => {
renderRedirect(from, pattern, <LegacyAutomationDetailRedirect />);
expect(screen.getByText(to)).toBeTruthy();
},
);
});
96 changes: 62 additions & 34 deletions apps/app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { lazy, Suspense } from "react";
import { Navigate, Route, Routes, useParams } from "react-router-dom";
import {
Navigate,
Route,
Routes,
useLocation,
useParams,
} from "react-router-dom";
import { AppLayout } from "./components/layout/AppLayout";
import { AuthCallbackView } from "./views/AuthCallbackView";
import { QuickCreateProjectProvider } from "./hooks/useQuickCreateProject";
Expand All @@ -10,12 +16,14 @@ import { useDesktopThemeSync } from "./hooks/useDesktopThemeSync";
import { usePluginFrontendBoot } from "./hooks/usePluginFrontendBoot";
import { useWebSocket } from "./hooks/useWebSocket";
import {
AUTOMATION_DETAIL_ROUTE_PATH,
AUTOMATIONS_ROUTE_PATH,
AUTH_CALLBACK_ROUTE_PATH,
LEGACY_AUTOMATION_DETAIL_ROUTE_PATH,
LEGACY_AUTOMATIONS_ROUTE_PATH,
LEGACY_SKILLS_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_BROWSE_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_DETAIL_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_EDIT_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATIONS_ROUTE_PATH,
LEGACY_TOOLS_SKILL_DETAIL_ROUTE_PATH,
PROJECT_ARCHIVED_ROUTE_PATH,
PROJECTLESS_ARCHIVED_ROUTE_PATH,
Expand All @@ -26,8 +34,6 @@ import {
SETTINGS_ROUTE_PATH,
SETTINGS_SECTION_ROUTE_PATH,
SKILLS_ROUTE_PATH,
TOOLS_AUTOMATION_BROWSE_ROUTE_PATH,
TOOLS_AUTOMATION_EDIT_ROUTE_PATH,
TOOLS_PLUGIN_BROWSE_ROUTE_PATH,
TOOLS_PLUGIN_DETAIL_ROUTE_PATH,
TOOLS_PLUGINS_ROUTE_PATH,
Expand All @@ -36,6 +42,8 @@ import {
TOOLS_ROUTE_PATH,
TOOLS_SKILL_DETAIL_ROUTE_PATH,
getAutomationDetailRoutePath,
getAutomationEditRoutePath,
getAutomationsRoutePath,
getSettingsRoutePath,
getSkillDetailRoutePath,
} from "./lib/route-paths";
Expand All @@ -61,18 +69,40 @@ const ProjectSettingsView = lazy(() =>
);
const SplitWorkspaceRoute = lazy(() => import("./views/SplitWorkspaceRoute"));

function LegacyAutomationDetailRedirect() {
export function LegacyAutomationDetailRedirect() {
const location = useLocation();
const { projectId, automationId } = useParams<{
projectId?: string;
automationId?: string;
}>();

if (!projectId || !automationId) {
return <Navigate to={AUTOMATIONS_ROUTE_PATH} replace />;
return <Navigate to={getAutomationsRoutePath()} replace />;
}
return (
<Navigate
to={getAutomationDetailRoutePath({ projectId, automationId })}
to={
location.pathname.endsWith("/edit")
? getAutomationEditRoutePath({ projectId, automationId })
: getAutomationDetailRoutePath({ projectId, automationId })
}
replace
/>
);
}

export function LegacyAutomationCollectionRedirect() {
const location = useLocation();
const browse =
location.pathname.endsWith("/browse") ||
new URLSearchParams(location.search).get("view") === "browse";
return (
<Navigate
to={
browse
? `${getAutomationsRoutePath()}/browse`
: getAutomationsRoutePath()
}
replace
/>
);
Expand Down Expand Up @@ -130,6 +160,30 @@ function AppRoutes() {
path={PROJECTLESS_ARCHIVED_ROUTE_PATH}
element={<Navigate to={getSettingsRoutePath("archived")} replace />}
/>
<Route
path={LEGACY_TOOLS_AUTOMATIONS_ROUTE_PATH}
element={<LegacyAutomationCollectionRedirect />}
/>
<Route
path={LEGACY_TOOLS_AUTOMATION_BROWSE_ROUTE_PATH}
element={<LegacyAutomationCollectionRedirect />}
/>
<Route
path={LEGACY_TOOLS_AUTOMATION_DETAIL_ROUTE_PATH}
element={<LegacyAutomationDetailRedirect />}
/>
<Route
path={LEGACY_TOOLS_AUTOMATION_EDIT_ROUTE_PATH}
element={<LegacyAutomationDetailRedirect />}
/>
<Route
path={LEGACY_AUTOMATIONS_ROUTE_PATH}
element={<LegacyAutomationCollectionRedirect />}
/>
<Route
path={LEGACY_AUTOMATION_DETAIL_ROUTE_PATH}
element={<LegacyAutomationDetailRedirect />}
/>
<Route element={<ToolsExperimentGate />}>
<Route
path={TOOLS_ROUTE_PATH}
Expand Down Expand Up @@ -166,36 +220,10 @@ function AppRoutes() {
path={TOOLS_PLUGIN_DETAIL_ROUTE_PATH}
element={<ToolsView />}
/>
<Route path={AUTOMATIONS_ROUTE_PATH} element={<ToolsView />} />
<Route
path={TOOLS_AUTOMATION_BROWSE_ROUTE_PATH}
element={
<Navigate
to={`${AUTOMATIONS_ROUTE_PATH}?view=browse`}
replace
/>
}
/>
<Route
path={AUTOMATION_DETAIL_ROUTE_PATH}
element={<ToolsView />}
/>
<Route
path={TOOLS_AUTOMATION_EDIT_ROUTE_PATH}
element={<ToolsView />}
/>
<Route
path={LEGACY_SKILLS_ROUTE_PATH}
element={<Navigate to={SKILLS_ROUTE_PATH} replace />}
/>
<Route
path={LEGACY_AUTOMATIONS_ROUTE_PATH}
element={<Navigate to={AUTOMATIONS_ROUTE_PATH} replace />}
/>
<Route
path={LEGACY_AUTOMATION_DETAIL_ROUTE_PATH}
element={<LegacyAutomationDetailRedirect />}
/>
</Route>
<Route path="*" element={<SplitWorkspaceRoute />} />
</Routes>
Expand Down
26 changes: 16 additions & 10 deletions apps/app/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -578,31 +578,37 @@
}

/*
* Keep thread timelines visually quiet at rest while preserving the native
* scroll container, its hit area, and its layout geometry. The timeline
* scroll owner briefly sets `data-scrollbar-scrolling` for real scroll
* events, matching the transient scrollbar behavior users expect from the
* platform without making pane content reflow as the thumb appears.
* Keep reading surfaces visually quiet at rest while preserving the native
* scroll container, its hit area, and its layout geometry. The scroll owner
* briefly sets `data-scrollbar-scrolling` for real scroll events, matching
* the platform's transient scrollbar behavior without making content reflow
* as the thumb appears.
*/
.thread-scrollbar {
.thread-scrollbar,
.transient-scrollbar {
scrollbar-color: transparent transparent;
}

.thread-scrollbar::-webkit-scrollbar-thumb,
.thread-scrollbar::-webkit-scrollbar-thumb:hover {
.thread-scrollbar::-webkit-scrollbar-thumb:hover,
.transient-scrollbar::-webkit-scrollbar-thumb,
.transient-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: transparent;
}

.thread-scrollbar[data-scrollbar-scrolling="true"] {
.thread-scrollbar[data-scrollbar-scrolling="true"],
.transient-scrollbar[data-scrollbar-scrolling="true"] {
scrollbar-color: color-mix(in oklab, var(--foreground) 20%, transparent)
transparent;
}

.thread-scrollbar[data-scrollbar-scrolling="true"]::-webkit-scrollbar-thumb {
.thread-scrollbar[data-scrollbar-scrolling="true"]::-webkit-scrollbar-thumb,
.transient-scrollbar[data-scrollbar-scrolling="true"]::-webkit-scrollbar-thumb {
background-color: color-mix(in oklab, var(--foreground) 20%, transparent);
}

.thread-scrollbar[data-scrollbar-scrolling="true"]::-webkit-scrollbar-thumb:hover {
.thread-scrollbar[data-scrollbar-scrolling="true"]::-webkit-scrollbar-thumb:hover,
.transient-scrollbar[data-scrollbar-scrolling="true"]::-webkit-scrollbar-thumb:hover {
background-color: color-mix(in oklab, var(--foreground) 40%, transparent);
}

Expand Down
32 changes: 5 additions & 27 deletions apps/app/src/components/layout/AppLayout.tools-breadcrumbs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,13 @@ describe("resolveToolsBreadcrumbs", () => {
to,
})),
).toEqual([
{ id: "skills", label: "Skills", icon: "Zap", to: "/tools/skills" },
{
id: "plugins",
label: "Plugins",
icon: "ElectricPlugs",
to: "/tools/plugins",
},
{
id: "automations",
label: "Automations",
icon: "TimeSchedule",
to: "/tools/automations",
},
{ id: "skills", label: "Skills", icon: "Zap", to: "/tools/skills" },
]);
});

Expand All @@ -43,12 +37,6 @@ describe("resolveToolsBreadcrumbs", () => {
{ label: "Plugins", to: "/tools/plugins" },
{ label: "Installed" },
]);
expect(
resolveToolsBreadcrumbs("/tools/automations", "?view=browse"),
).toEqual([
{ label: "Automations", to: "/tools/automations" },
{ label: "Browse" },
]);
});

it("resolves literal browse paths as Browse, not as a resource named browse", () => {
Expand All @@ -63,10 +51,6 @@ describe("resolveToolsBreadcrumbs", () => {
{ label: "Skills", to: "/tools/skills" },
{ label: "Browse" },
]);
expect(resolveToolsBreadcrumbs("/tools/automations/browse")).toEqual([
{ label: "Automations", to: "/tools/automations" },
{ label: "Browse" },
]);
});

it("makes every detail ancestor clickable and keeps the resource passive", () => {
Expand Down Expand Up @@ -103,15 +87,9 @@ describe("resolveToolsBreadcrumbs", () => {
{ label: "UI Patterns" },
]);
expect(
resolveToolsBreadcrumbs("/tools/automations/personal/weekly-review/edit"),
).toEqual([
{ label: "Automations", to: "/tools/automations" },
{ label: "Installed", to: "/tools/automations" },
{
label: "weekly-review",
to: "/tools/automations/personal/weekly-review",
},
{ label: "Edit" },
]);
resolveToolsBreadcrumbs(
"/plugins/automations/automations/personal/weekly-review",
),
).toBeNull();
});
});
6 changes: 3 additions & 3 deletions apps/app/src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -637,9 +637,9 @@ export function AppLayout({ children }: AppLayoutProps) {
? `Thread ${threadId.slice(0, 8)}`
: "Thread";
// Gated with the rest of the Tools surface: ROOT_ROUTE_ALIASES maps /skills
// and /automations into Tools crumbs, so a gate-off user following an old
// link would otherwise see Tools chrome for the whole config fetch before
// ToolsExperimentGate redirects them away.
// into Tools crumbs, so a gate-off user following an old link would otherwise
// see Tools chrome for the whole config fetch before ToolsExperimentGate
// redirects them away.
const toolsBreadcrumbs = toolsHubEnabled
? resolveToolsBreadcrumbs(
location.pathname,
Expand Down
Loading
Loading