Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 57 additions & 0 deletions packages/core/src/agents/agentsRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* The agents surface (scouts + the agent-applications platform) is mounted under
* both `/code/agents/*` (the Code view) and `/website/agents/*` (the Channels /
* Bluebird view), sharing the same view components. Internal navigation resolves
* its routes through the per-space map below so every `<Link>` / `navigate` stays
* in the space the user is currently in instead of jumping back to `/code`.
*/
export type AgentsSpace = "code" | "website";

/** Which space a pathname belongs to. Defaults to `code` off the website tree. */
export function agentsSpaceFromPath(pathname: string): AgentsSpace {
return pathname.startsWith("/website") ? "website" : "code";
}

/**
* Every agents route, keyed by space. `$idOrSlug` / `$skillName` / `$sessionId`
* params are filled in by the call site via TanStack's typed `params`.
*/
export const AGENTS_ROUTE = {
code: {
root: "/code/agents",
scouts: "/code/agents/scouts",
scoutDetail: "/code/agents/scouts/$skillName",
scratchpad: "/code/agents/scouts/scratchpad",
applications: "/code/agents/applications",
fleetApprovals: "/code/agents/applications/approvals",
application: "/code/agents/applications/$idOrSlug",
configuration: "/code/agents/applications/$idOrSlug/configuration",
sessions: "/code/agents/applications/$idOrSlug/sessions",
sessionDetail: "/code/agents/applications/$idOrSlug/sessions/$sessionId",
users: "/code/agents/applications/$idOrSlug/users",
memory: "/code/agents/applications/$idOrSlug/memory",
approvals: "/code/agents/applications/$idOrSlug/approvals",
observability: "/code/agents/applications/$idOrSlug/observability",
chat: "/code/agents/applications/$idOrSlug/chat",
},
website: {
root: "/website/agents",
scouts: "/website/agents/scouts",
scoutDetail: "/website/agents/scouts/$skillName",
scratchpad: "/website/agents/scouts/scratchpad",
applications: "/website/agents/applications",
fleetApprovals: "/website/agents/applications/approvals",
application: "/website/agents/applications/$idOrSlug",
configuration: "/website/agents/applications/$idOrSlug/configuration",
sessions: "/website/agents/applications/$idOrSlug/sessions",
sessionDetail: "/website/agents/applications/$idOrSlug/sessions/$sessionId",
users: "/website/agents/applications/$idOrSlug/users",
memory: "/website/agents/applications/$idOrSlug/memory",
approvals: "/website/agents/applications/$idOrSlug/approvals",
observability: "/website/agents/applications/$idOrSlug/observability",
chat: "/website/agents/applications/$idOrSlug/chat",
},
} as const satisfies Record<AgentsSpace, Record<string, string>>;

/** The set of route keys available per space. */
export type AgentsRouteKey = keyof (typeof AGENTS_ROUTE)["code"];
94 changes: 78 additions & 16 deletions packages/core/src/inbox/reportMembership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,25 +99,87 @@ export const INBOX_TAB_LABEL: Record<InboxTabKey, string> = {
};

/**
* Canonical inbox tab list routes. Use these constants instead of hard-coding
* `/code/inbox/pulls` etc., so renames stay in one place.
*
* Detail routes (`/code/inbox/<tab>/$reportId`) stay as TanStack Router
* literals at call sites – TanStack's typed-link API needs them as literal
* strings to infer params.
* The two navigation spaces that host the inbox. The inbox subtree is mounted
* under both `/code/inbox/*` (the Code view) and `/website/inbox/*` (the
* Channels / Bluebird view) — sharing the same view components. Internal
* navigation (tabs, cards, back-links, redirects) resolves routes through the
* per-space maps below so it never jumps the user out of the space they're in.
*/
export const INBOX_TAB_LIST_ROUTE: Record<
InboxTabKey,
`/code/inbox/${InboxTabKey}`
> = {
pulls: "/code/inbox/pulls",
reports: "/code/inbox/reports",
runs: "/code/inbox/runs",
dismissed: "/code/inbox/dismissed",
};
export type InboxSpace = "code" | "website";

/** Which space a pathname belongs to. Defaults to `code` off the website tree. */
export function inboxSpaceFromPath(pathname: string): InboxSpace {
return pathname.startsWith("/website") ? "website" : "code";
}

/** Inbox root route per space (the index, which redirects to the pulls tab). */
export const INBOX_ROOT_ROUTE = {
code: "/code/inbox",
website: "/website/inbox",
} as const satisfies Record<InboxSpace, string>;

/**
* Canonical inbox tab list routes, keyed by space. Use these constants instead
* of hard-coding `/code/inbox/pulls` etc., so renames stay in one place and a
* call site stays in whichever space it's navigating within.
*/
export const INBOX_TAB_LIST_ROUTE_BY_SPACE = {
code: {
pulls: "/code/inbox/pulls",
reports: "/code/inbox/reports",
runs: "/code/inbox/runs",
dismissed: "/code/inbox/dismissed",
},
website: {
pulls: "/website/inbox/pulls",
reports: "/website/inbox/reports",
runs: "/website/inbox/runs",
dismissed: "/website/inbox/dismissed",
},
} as const satisfies Record<InboxSpace, Record<InboxTabKey, string>>;

/** Detail routes (`<root>/<tab>/$reportId`), keyed by space. */
export const INBOX_TAB_DETAIL_ROUTE_BY_SPACE = {
code: {
pulls: "/code/inbox/pulls/$reportId",
reports: "/code/inbox/reports/$reportId",
runs: "/code/inbox/runs/$reportId",
dismissed: "/code/inbox/dismissed/$reportId",
},
website: {
pulls: "/website/inbox/pulls/$reportId",
reports: "/website/inbox/reports/$reportId",
runs: "/website/inbox/runs/$reportId",
dismissed: "/website/inbox/dismissed/$reportId",
},
} as const satisfies Record<InboxSpace, Record<InboxTabKey, string>>;

/** Union of every list route across both spaces (a valid TanStack `to`). */
export type InboxListRoute =
(typeof INBOX_TAB_LIST_ROUTE_BY_SPACE)[InboxSpace][InboxTabKey];

/** Union of every detail route across both spaces (a valid TanStack `to`). */
export type InboxDetailRoute =
(typeof INBOX_TAB_DETAIL_ROUTE_BY_SPACE)[InboxSpace][InboxTabKey];

/**
* Back-compat alias for the Code-space list routes. Prefer
* `INBOX_TAB_LIST_ROUTE_BY_SPACE[space]` in space-aware call sites.
*/
export const INBOX_TAB_LIST_ROUTE = INBOX_TAB_LIST_ROUTE_BY_SPACE.code;

const INBOX_TAB_FROM_PATH_RE = new RegExp(
`^/(?:code|website)/inbox/(${INBOX_TAB_KEYS.join("|")})`,
);

/** Which inbox tab a pathname is on, in either space. */
export function inboxTabFromPath(pathname: string): InboxTabKey {
const match = pathname.match(INBOX_TAB_FROM_PATH_RE);
return (match?.[1] as InboxTabKey | undefined) ?? "pulls";
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

const INBOX_DETAIL_PATH_RE = new RegExp(
`^/code/inbox/(${INBOX_TAB_KEYS.join("|")})/[^/]+$`,
`^/(?:code|website)/inbox/(${INBOX_TAB_KEYS.join("|")})/[^/]+$`,
);

export function isInboxDetailPath(pathname: string): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useNavigate } from "@tanstack/react-router";
import { useCallback, useRef } from "react";
import type { ClientToolHandler } from "../hooks/useAgentChat";
import { useAgentsRoutes } from "../hooks/useAgentsRoutes";
import { useAgentBuilderStore } from "./agentBuilderStore";

/**
Expand Down Expand Up @@ -33,6 +34,7 @@ export const AGENT_BUILDER_CLIENT_TOOLS = [
*/
export function useAgentBuilderClientTools(): ClientToolHandler {
const navigate = useNavigate();
const routes = useAgentsRoutes();
const followMode = useAgentBuilderStore((s) => s.followMode);
const setPendingSecret = useAgentBuilderStore((s) => s.setPendingSecret);
const setPendingMcpConnect = useAgentBuilderStore(
Expand Down Expand Up @@ -115,66 +117,45 @@ export function useAgentBuilderClientTools(): ClientToolHandler {
const tab = str(args.tab) ?? "overview";
switch (tab) {
case "configuration":
navigate({
to: "/code/agents/applications/$idOrSlug/configuration",
params,
});
navigate({ to: routes.configuration, params });
break;
case "sessions":
navigate({
to: "/code/agents/applications/$idOrSlug/sessions",
params,
});
navigate({ to: routes.sessions, params });
break;
case "memory":
navigate({
to: "/code/agents/applications/$idOrSlug/memory",
params,
});
navigate({ to: routes.memory, params });
break;
case "approvals":
navigate({
to: "/code/agents/applications/$idOrSlug/approvals",
params,
});
navigate({ to: routes.approvals, params });
break;
case "observability":
navigate({
to: "/code/agents/applications/$idOrSlug/observability",
params,
});
navigate({ to: routes.observability, params });
break;
case "chat":
navigate({
to: "/code/agents/applications/$idOrSlug/chat",
params,
});
navigate({ to: routes.chat, params });
break;
default:
navigate({
to: "/code/agents/applications/$idOrSlug",
params,
});
navigate({ to: routes.application, params });
}
return { result: { focused: true } };
}
case "focus_file":
navigate({
to: "/code/agents/applications/$idOrSlug/configuration",
to: routes.configuration,
params,
search: { node: str(args.path) },
});
return { result: { focused: true } };
case "focus_spec_section":
navigate({
to: "/code/agents/applications/$idOrSlug/configuration",
to: routes.configuration,
params,
search: { node: str(args.section) },
});
return { result: { focused: true } };
case "focus_revision":
navigate({
to: "/code/agents/applications/$idOrSlug/configuration",
to: routes.configuration,
params,
search: { revision: str(args.revisionId) },
});
Expand All @@ -185,7 +166,7 @@ export function useAgentBuilderClientTools(): ClientToolHandler {
return { result: { focused: false, reason: "missing_session_id" } };
}
navigate({
to: "/code/agents/applications/$idOrSlug/sessions/$sessionId",
to: routes.sessionDetail,
params: { idOrSlug: slug, sessionId },
});
return { result: { focused: true } };
Expand All @@ -194,6 +175,6 @@ export function useAgentBuilderClientTools(): ClientToolHandler {
return { result: { focused: false, reason: "unknown_focus_target" } };
}
},
[navigate, setPendingSecret, setPendingMcpConnect],
[navigate, setPendingSecret, setPendingMcpConnect, routes],
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Link } from "@tanstack/react-router";
import { useAgentAnalytics } from "../hooks/useAgentAnalytics";
import { useAgentApplication } from "../hooks/useAgentApplication";
import { useAgentApplicationSessions } from "../hooks/useAgentApplicationSessions";
import { useAgentsRoutes } from "../hooks/useAgentsRoutes";
import { AgentAnalyticsKpiStrip } from "./AgentAnalyticsView";
import { AgentDetailEmptyState, AgentDetailLayout } from "./AgentDetailLayout";
import { AgentSessionRow } from "./AgentSessionRow";
Expand All @@ -24,6 +25,7 @@ export function AgentApplicationDetailView({ idOrSlug }: { idOrSlug: string }) {
isLoading: sessionsLoading,
isError: sessionsError,
} = useAgentApplicationSessions(idOrSlug, { limit: 25 });
const routes = useAgentsRoutes();

return (
<AgentDetailLayout idOrSlug={idOrSlug} activeTab="overview">
Expand All @@ -34,7 +36,7 @@ export function AgentApplicationDetailView({ idOrSlug }: { idOrSlug: string }) {
Activity · last 7 days
</Text>
<Link
to="/code/agents/applications/$idOrSlug/observability"
to={routes.observability}
params={{ idOrSlug }}
className="text-[12px] text-gray-11 no-underline hover:text-gray-12"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
AgentAnalyticsAgentRow,
AgentApplication,
} from "@posthog/shared/agent-platform-types";
import { useAgentsRoutes } from "@posthog/ui/features/agent-applications/hooks/useAgentsRoutes";
import { AgentsTabLayout } from "@posthog/ui/features/agents/components/AgentsTabLayout";
import { Badge } from "@posthog/ui/primitives/Badge";
import { Button } from "@posthog/ui/primitives/Button";
Expand Down Expand Up @@ -277,9 +278,10 @@ function ApplicationRow({
stats?: AgentAnalyticsAgentRow;
}) {
const isLive = application.live_revision != null;
const routes = useAgentsRoutes();
return (
<Link
to="/code/agents/applications/$idOrSlug"
to={routes.application}
params={{ idOrSlug: application.slug ?? application.id }}
className="flex items-center justify-between gap-3 rounded-(--radius-2) border border-border bg-(--color-panel-solid) px-4 py-3.5 no-underline transition-colors duration-150 hover:border-(--gray-6) hover:bg-(--gray-2)"
>
Expand Down Expand Up @@ -356,10 +358,11 @@ function RowStat({
*/
function OperationalStrip({ pendingCount }: { pendingCount: number }) {
const pendingAttention = pendingCount > 0;
const routes = useAgentsRoutes();
return (
<Flex align="center" gap="5" className="text-[12.5px]">
<Link
to="/code/agents/applications/approvals"
to={routes.fleetApprovals}
className="inline-flex items-center gap-1 text-gray-11 no-underline hover:text-gray-12"
>
<LockKeyIcon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useAgentApplication } from "../hooks/useAgentApplication";
import { useAgentChat } from "../hooks/useAgentChat";
import { useAgentChatPendingApproval } from "../hooks/useAgentChatPendingApproval";
import { useAgentRevision } from "../hooks/useAgentRevision";
import { useAgentsRoutes } from "../hooks/useAgentsRoutes";
import { resolveIngressBaseUrl } from "../utils/ingress";
import { AgentChatPendingApprovalCard } from "./AgentChatPendingApprovalCard";
import { AgentChatSurface } from "./AgentChatSurface";
Expand Down Expand Up @@ -72,6 +73,7 @@ export function AgentChatPane({
resumeSessionId?: string | null;
}) {
const navigate = useNavigate();
const routes = useAgentsRoutes();
const { data: application } = useAgentApplication(idOrSlug);
// null/equal-to-live → fall back to the live revision; explicit non-live
// revision id → route this chat to the draft.
Expand Down Expand Up @@ -140,11 +142,11 @@ export function AgentChatPane({
// Anchored to this route so TanStack can resolve the search-fn against the
// chat route's typed shape (the no-`to` form widens to `never`).
navigate({
to: "/code/agents/applications/$idOrSlug/chat",
to: routes.chat,
params: { idOrSlug },
search: (prev) => ({ ...prev, session: undefined }),
});
}, [resumeSessionId, chat.resume, navigate, idOrSlug]);
}, [resumeSessionId, chat.resume, navigate, idOrSlug, routes.chat]);

// The rail mixes chats from every revision; decide per click whether to
// resume inline (same target) or navigate to a different revision's surface.
Expand All @@ -154,7 +156,7 @@ export function AgentChatPane({
return;
}
navigate({
to: "/code/agents/applications/$idOrSlug/chat",
to: routes.chat,
params: { idOrSlug },
search: entry.revisionId
? { revision: entry.revisionId, session: entry.sessionId }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";
import { useAuthStateValue } from "../../auth/store";
import { agentApplicationsKeys } from "../hooks/agentApplicationsKeys";
import { useAgentsRoutes } from "../hooks/useAgentsRoutes";
import { approvalStateColor, approvalStateLabel } from "../utils/format";
import { AgentApprovalDecisionForm } from "./AgentApprovalDecisionForm";
import { ArgsSection } from "./AgentApprovalDetail";
Expand Down Expand Up @@ -40,6 +41,7 @@ export function AgentChatPendingApprovalCard({
}) {
const queryClient = useQueryClient();
const projectId = useAuthStateValue((state) => state.currentProjectId);
const routes = useAgentsRoutes();
const isOwnerGate = approval.approver_scope?.type === "agent";

const mutation = useMutation<void, Error, DecideApprovalRequest>({
Expand Down Expand Up @@ -85,7 +87,7 @@ export function AgentChatPendingApprovalCard({
</Text>
</Flex>
<Link
to="/code/agents/applications/$idOrSlug/approvals"
to={routes.approvals}
params={{ idOrSlug }}
search={{ request: approval.id }}
className="inline-flex shrink-0 items-center gap-1 text-[11.5px] text-gray-11 no-underline hover:text-gray-12"
Expand Down
Loading
Loading