diff --git a/canvas/src/components/SidePanel.tsx b/canvas/src/components/SidePanel.tsx
index 80fe37b8..44a32940 100644
--- a/canvas/src/components/SidePanel.tsx
+++ b/canvas/src/components/SidePanel.tsx
@@ -280,7 +280,7 @@ export function SidePanel() {
className="flex-1 overflow-y-auto focus:outline-none"
>
{panelTab === "details" && }
- {panelTab === "skills" && }
+ {panelTab === "skills" && }
{panelTab === "activity" && }
{panelTab === "chat" && }
{panelTab === "terminal" && }
diff --git a/canvas/src/components/__tests__/tabs.a11y.test.tsx b/canvas/src/components/__tests__/tabs.a11y.test.tsx
index a7000917..be5446fe 100644
--- a/canvas/src/components/__tests__/tabs.a11y.test.tsx
+++ b/canvas/src/components/__tests__/tabs.a11y.test.tsx
@@ -123,7 +123,7 @@ describe("SkillsTab — aria-label on bare source input (WCAG 1.3.1)", () => {
});
it('install source input has aria-label="Install from source URL"', async () => {
- render();
+ render();
// The source input is inside the registry section (showRegistry=false initially).
// Click the "+ Install Plugin" button to reveal it.
@@ -138,7 +138,7 @@ describe("SkillsTab — aria-label on bare source input (WCAG 1.3.1)", () => {
});
it("install source input is a text input (not hidden)", async () => {
- render();
+ render();
const installBtn = screen.getByRole("button", { name: /install plugin/i });
fireEvent.click(installBtn);
diff --git a/canvas/src/components/tabs/SkillsTab.tsx b/canvas/src/components/tabs/SkillsTab.tsx
index c144f301..8c5da29e 100644
--- a/canvas/src/components/tabs/SkillsTab.tsx
+++ b/canvas/src/components/tabs/SkillsTab.tsx
@@ -6,6 +6,14 @@ import { useCanvasStore, summarizeWorkspaceCapabilities, type WorkspaceNodeData
import { showToast } from "../Toaster";
interface Props {
+ // The workspace's id is NOT a field on WorkspaceNodeData — that
+ // interface is the React Flow `node.data` blob, while the id lives
+ // on `node.id`. Pass it explicitly (matches every other tab in
+ // SidePanel) so the install/uninstall API calls don't end up
+ // POSTing to /workspaces/undefined/plugins. The interface extending
+ // Record meant TypeScript silently typed
+ // `data.id` as `unknown` instead of erroring — easy to miss.
+ workspaceId: string;
data: WorkspaceNodeData;
}
@@ -40,7 +48,7 @@ interface SourceSchemesResponse {
// Delay before reloading installed plugins after install/uninstall (workspace restarts)
const PLUGIN_RELOAD_DELAY_MS = 15_000;
-export function SkillsTab({ data }: Props) {
+export function SkillsTab({ workspaceId, data }: Props) {
const capability = summarizeWorkspaceCapabilities(data);
const skills = useMemo(() => extractSkills(data.agentCard), [data.agentCard]);
const setPanelTab = useCanvasStore((s) => s.setPanelTab);
@@ -74,8 +82,6 @@ export function SkillsTab({ data }: Props) {
};
}, []);
- const workspaceId = data.id;
-
// Tracks whether loadInstalled has completed at least once (success
// or empty-array success — NOT failure). Without this the auto-
// expand effect below would fire on the initial render where
@@ -233,7 +239,7 @@ export function SkillsTab({ data }: Props) {
const handleUninstall = async (pluginName: string) => {
setUninstalling(pluginName);
try {
- await api.del(`/workspaces/${data.id}/plugins/${pluginName}`);
+ await api.del(`/workspaces/${workspaceId}/plugins/${pluginName}`);
showToast(`Removed ${pluginName} — restarting workspace`, "success");
setInstalled((prev) => prev.filter((p) => p.name !== pluginName));
reloadTimerRef.current = setTimeout(() => loadInstalled(), PLUGIN_RELOAD_DELAY_MS);