Skip to content
Draft
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
abdeb86
feat: fix feature ID matching and adds 404 handling
brandonmcconnell Dec 4, 2025
721697b
feat: improve 404 handling with middleware
brandonmcconnell Dec 4, 2025
b029f89
feat: remove integration features data file
brandonmcconnell Dec 4, 2025
9e2d19c
feat: consolidate integration configurations
brandonmcconnell Dec 4, 2025
6accb6d
feat: consolidate integration configurations
brandonmcconnell Dec 4, 2025
e1b8d0a
feat: enforce type-safe agent configurations
brandonmcconnell Dec 4, 2025
dfde3ee
feat: refactor agents configuration for better type safety
brandonmcconnell Dec 4, 2025
d09e2cf
fix: enforce agent integration feature matching
brandonmcconnell Dec 5, 2025
e5ce07d
fix: enforce strict type checking for agent features
brandonmcconnell Dec 5, 2025
39b71a8
feat: refines agent mapping type for improved safety
brandonmcconnell Dec 5, 2025
e8dcd46
fix: revert change silencing errors re missing backend_tool_rendering
brandonmcconnell Dec 5, 2025
ae5811e
fix: temporarily add missing agents to silence errors
brandonmcconnell Dec 5, 2025
e506764
chore: re-add langchain (previously removed for reformatting)
brandonmcconnell Dec 5, 2025
32500d1
fix: silence TS errors re missing types and assign to Ran via comment
brandonmcconnell Dec 5, 2025
7fe84f6
fix: mastra agents type safety
brandonmcconnell Dec 5, 2025
cce20cc
chore: refine comment
brandonmcconnell Dec 5, 2025
e3ef86f
chore: clear unused @ts-ignore
brandonmcconnell Dec 5, 2025
187f245
fix: add `langchain` to `menuIntegrations`
brandonmcconnell Dec 5, 2025
4886302
fix: remove temporarily added missing agents to un-silence errors
brandonmcconnell Dec 5, 2025
ab6de27
Merge branch 'main' into fix/implement-feature-id-matching-and-404
brandonmcconnell Dec 8, 2025
75a890a
Merge branch 'main' into fix/implement-feature-id-matching-and-404
brandonmcconnell Dec 8, 2025
64aeef5
Merge branch 'main' into fix/implement-feature-id-matching-and-404
brandonmcconnell Dec 8, 2025
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
751 changes: 295 additions & 456 deletions apps/dojo/src/agents.ts

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions apps/dojo/src/app/[integrationId]/feature/layout-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client';

import React, { useMemo } from "react";
import filesJSON from '../../../files.json'
import Readme from "@/components/readme/readme";
import CodeViewer from "@/components/code-viewer/code-viewer";
import { useURLParams } from "@/contexts/url-params-context";
import { cn } from "@/lib/utils";
import { Feature } from "@/types/integration";

type FileItem = {
name: string;
content: string;
language: string;
type: string;
};

type FilesJsonType = Record<string, FileItem[]>;

interface Props {
integrationId: string;
featureId: Feature;
children: React.ReactNode;
}

export default function FeatureLayoutClient({ children, integrationId, featureId }: Props) {
const { sidebarHidden } = useURLParams();
const { view } = useURLParams();

const files = (filesJSON as FilesJsonType)[`${integrationId}::${featureId}`] || [];

const readme = files.find((file) => file?.name?.includes(".mdx")) || null;
const codeFiles = files.filter(
(file) => file && Object.keys(file).length > 0 && !file.name?.includes(".mdx"),
);

const content = useMemo(() => {
switch (view) {
case "code":
return (
<CodeViewer codeFiles={codeFiles} />
)
case "readme":
return (
<Readme content={readme?.content ?? ''} />
)
default:
return (
<div className="h-full">{children}</div>
)
}
}, [children, codeFiles, readme, view])

return (
<div className={cn(
"bg-white w-full h-full overflow-hidden",
// if used in iframe, match background to chat background color, otherwise, use white
sidebarHidden && "bg-(--copilot-kit-background-color)",
// if not used in iframe, round the corners of the content area
!sidebarHidden && "rounded-lg",
)}>
<div className="flex flex-col h-full overflow-auto">
{content}
</div>
</div>
);
}

83 changes: 23 additions & 60 deletions apps/dojo/src/app/[integrationId]/feature/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,38 @@
'use client';
import { headers } from "next/headers";
import { notFound } from "next/navigation";
import { Feature } from "@/types/integration";
import FeatureLayoutClient from "./layout-client";

import React, { useMemo } from "react";
import { usePathname } from "next/navigation";
import filesJSON from '../../../files.json'
import Readme from "@/components/readme/readme";
import CodeViewer from "@/components/code-viewer/code-viewer";
import { useURLParams } from "@/contexts/url-params-context";
import { cn } from "@/lib/utils";

type FileItem = {
name: string;
content: string;
language: string;
type: string;
};

type FilesJsonType = Record<string, FileItem[]>;
// Force dynamic rendering to ensure proper 404 handling
export const dynamic = "force-dynamic";

interface Props {
params: Promise<{
integrationId: string;
}>;
children: React.ReactNode
children: React.ReactNode;
}

export default function FeatureLayout({ children, params }: Props) {
const { sidebarHidden } = useURLParams();
const { integrationId } = React.use(params);
const pathname = usePathname();
const { view } = useURLParams();
export default async function FeatureLayout({ children, params }: Props) {
const { integrationId } = await params;

// Extract featureId from pathname: /[integrationId]/feature/[featureId]
const pathParts = pathname.split('/');
const featureId = pathParts[pathParts.length - 1]; // Last segment is the featureId
// Get headers set by middleware
const headersList = await headers();
const pathname = headersList.get("x-pathname") || "";
const notFoundType = headersList.get("x-not-found");

const files = (filesJSON as FilesJsonType)[`${integrationId}::${featureId}`] || [];
// If middleware flagged this as not found, trigger 404
if (notFoundType) {
notFound();
}

const readme = files.find((file) => file?.name?.includes(".mdx")) || null;
const codeFiles = files.filter(
(file) => file && Object.keys(file).length > 0 && !file.name?.includes(".mdx"),
);


const content = useMemo(() => {
switch (view) {
case "code":
return (
<CodeViewer codeFiles={codeFiles} />
)
case "readme":
return (
<Readme content={readme?.content ?? ''} />
)
default:
return (
<div className="h-full">{children}</div>
)
}
}, [children, codeFiles, readme, view])
// Extract featureId from pathname: /[integrationId]/feature/[featureId]
const pathParts = pathname.split("/");
const featureId = pathParts[pathParts.length - 1] as Feature;

return (
<div className={cn(
"bg-white w-full h-full overflow-hidden",
// if used in iframe, match background to chat background color, otherwise, use white
sidebarHidden && "bg-(--copilot-kit-background-color)",
// if not used in iframe, round the corners of the content area
!sidebarHidden && "rounded-lg",
)}>
<div className="flex flex-col h-full overflow-auto">
{content}
</div>
</div>
<FeatureLayoutClient integrationId={integrationId} featureId={featureId}>
{children}
</FeatureLayoutClient>
);
}
19 changes: 19 additions & 0 deletions apps/dojo/src/app/[integrationId]/feature/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Link from "next/link";

export default function FeatureNotFound() {
return (
<div className="flex-1 h-screen w-full flex flex-col items-center justify-center p-8 bg-white rounded-lg">
<h1 className="text-4xl font-bold text-center mb-4">Feature Not Found</h1>
<p className="text-muted-foreground mb-6 text-center">
This feature is not available for the selected integration.
</p>
<Link
href="/"
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
>
Back to Home
</Link>
</div>
);
}

3 changes: 3 additions & 0 deletions apps/dojo/src/app/[integrationId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export async function generateStaticParams() {
}));
}

// Return 404 for any params not in generateStaticParams
export const dynamicParams = false;

interface IntegrationPageProps {
params: Promise<{
integrationId: string;
Expand Down
9 changes: 5 additions & 4 deletions apps/dojo/src/app/api/copilotkit/[integrationId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ import {
import { NextRequest } from "next/server";

import { agentsIntegrations } from "@/agents";
import { IntegrationId } from "@/menu";

export async function POST(request: NextRequest) {
const integrationId = request.url.split("/").pop();
const integrationId = request.url.split("/").pop() as IntegrationId;

const integration = agentsIntegrations.find((i) => i.id === integrationId);
if (!integration) {
const getAgents = agentsIntegrations[integrationId];
if (!getAgents) {
return new Response("Integration not found", { status: 404 });
}

const agents = await integration.agents();
const agents = await getAgents();
const runtime = new CopilotRuntime({
// @ts-ignore for now
agents,
Expand Down
1 change: 1 addition & 0 deletions apps/dojo/src/app/not-found-page/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

39 changes: 36 additions & 3 deletions apps/dojo/src/menu.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { MenuIntegrationConfig } from "./types/integration";
import { MenuIntegrationConfig, Feature, IntegrationFeatures } from "./types/integration";

export const menuIntegrations: MenuIntegrationConfig[] = [
/**
* Integration configuration - SINGLE SOURCE OF TRUTH
*
* This file defines all integrations and their available features.
* Used by:
* - UI menu components
* - middleware.ts (for route validation)
* - agents.ts validates agent keys against these features
*/

export const menuIntegrations = [
{
id: "langgraph",
name: "LangGraph (Python)",
Expand Down Expand Up @@ -221,4 +231,27 @@ export const menuIntegrations: MenuIntegrationConfig[] = [
"human_in_the_loop",
],
},
];
] as const satisfies readonly MenuIntegrationConfig[];

/** Type representing all valid integration IDs */
export type IntegrationId = (typeof menuIntegrations)[number]["id"];

/** Type to get features for a specific integration ID */
export type FeaturesFor<Id extends IntegrationId> = IntegrationFeatures<
typeof menuIntegrations,
Id
>;

// Helper functions for route validation
export function isIntegrationValid(integrationId: string): boolean {
return menuIntegrations.some((i) => i.id === integrationId);
}

export function isFeatureAvailable(integrationId: string, featureId: string): boolean {
const integration = menuIntegrations.find((i) => i.id === integrationId);
return (integration?.features as readonly string[])?.includes(featureId) ?? false;
}

export function getIntegration(integrationId: string): MenuIntegrationConfig | undefined {
return menuIntegrations.find((i) => i.id === integrationId);
}
53 changes: 53 additions & 0 deletions apps/dojo/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { isIntegrationValid, isFeatureAvailable } from "./menu";

export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-pathname", pathname);

// Check for feature routes: /[integrationId]/feature/[featureId]
const featureMatch = pathname.match(/^\/([^/]+)\/feature\/([^/]+)\/?$/);

if (featureMatch) {
const [, integrationId, featureId] = featureMatch;

// Check if integration exists
if (!isIntegrationValid(integrationId)) {
requestHeaders.set("x-not-found", "integration");
}
// Check if feature is available for this integration
else if (!isFeatureAvailable(integrationId, featureId)) {
requestHeaders.set("x-not-found", "feature");
}
}

// Check for integration routes: /[integrationId] (but not /[integrationId]/feature/...)
const integrationMatch = pathname.match(/^\/([^/]+)\/?$/);

if (integrationMatch) {
const [, integrationId] = integrationMatch;

// Skip the root path
if (integrationId && integrationId !== "") {
if (!isIntegrationValid(integrationId)) {
requestHeaders.set("x-not-found", "integration");
}
}
}

return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}

export const config = {
matcher: [
// Match all paths except static files and api routes
"/((?!api|_next/static|_next/image|favicon.ico|images).*)",
],
};

13 changes: 7 additions & 6 deletions apps/dojo/src/types/integration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { AbstractAgent } from "@ag-ui/client";

export type Feature =
| "agentic_chat"
| "agentic_generative_ui"
Expand All @@ -19,7 +17,10 @@ export interface MenuIntegrationConfig {
features: Feature[];
}

export interface AgentIntegrationConfig {
id: string;
agents: () => Promise<Partial<Record<Feature, AbstractAgent>>>;
}
/**
* Helper type to extract features for a specific integration from menu config
*/
export type IntegrationFeatures<
T extends readonly MenuIntegrationConfig[],
Id extends string
> = Extract<T[number], { id: Id }>["features"][number];
12 changes: 6 additions & 6 deletions integrations/mastra/typescript/src/mastra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,15 +381,15 @@ export class MastraAgent extends AbstractAgent {
}
}

static async getRemoteAgents(
options: GetRemoteAgentsOptions,
): Promise<Record<string, AbstractAgent>> {
static async getRemoteAgents<K extends string = string>(
options: GetRemoteAgentsOptions<K>,
): Promise<Record<K, AbstractAgent>> {
return getRemoteAgents(options);
}

static getLocalAgents(
options: GetLocalAgentsOptions,
): Record<string, AbstractAgent> {
static getLocalAgents<K extends string = string>(
options: GetLocalAgentsOptions<K>,
): Record<K, AbstractAgent> {
return getLocalAgents(options);
}

Expand Down
Loading
Loading