Files
paperclip/ui/src/App.tsx
T
DottaandPaperclip e34abee670 feat(mcp): connect assistants to a team with user OAuth (#14846)
## Thinking Path

> - Paperclip gives teams durable tasks, agent execution, budgets, and
approvals.
> - People also use assistants in Codex, Claude, and other MCP clients.
> - Those assistants need a scoped connection that preserves the
person’s permissions and attribution.
> - Delegating a task must not turn the assistant into the assigned
agent.
> - This PR adds opt-in user OAuth, ten first-party tools, browser
consent, and workflow packages.
> - Paid product evals verify the resulting tasks, documents,
attribution, retries, and access boundaries.
> - The team keeps working after the assistant conversation ends.

## Linked Issues or Issue Description

**Problem or motivation**

A person cannot connect an external assistant to an existing team
through browser consent and safely delegate durable work as themselves.

**Proposed solution**

Expose an opt-in `/mcp/paperclip` endpoint with individually described
first-party operations. Bind every connection to a person, client,
company, resource, and scopes. Reuse domain authorization and
scheduling. Package shared team-review, delegation, and follow-up
workflows for OpenAI/Codex and Claude.

**Alternatives considered**

Related PRs #9393 and #12549 cover earlier remote MCP and board-operator
approaches. This change uses user OAuth and a bounded public catalog. It
does not expose a generic executor, operator administration, static
shared board credentials, or external agent execution. Registry listing
work in #9851 is a separate distribution step.

**Roadmap alignment**

This maintainer-requested implementation extends the governed MCP
gateway, activity attribution, durable work products, and hosted
deployment direction in `ROADMAP.md`. It implements the first release of
the saved design plan; external agent participation and granted
third-party tools remain later releases.

## What Changed

- Add MCP 2.0 discovery and task status/comment/document Events on the
same authenticated endpoint. Persist subscriptions and delivery
receipts, verify HTTPS callbacks, sign Standard Webhooks, encrypt
callback material, recheck permissions/Cloud membership, and bound
retries/expiry. Older MCP clients keep their existing tools.
- Add discovery, dynamic client registration, S256 PKCE, resource
validation, rotating refresh tokens, revocation, and company consent.
Store credentials as hashes and recheck membership at execution.
- Add tools for connection identity, agents/projects, task
search/read/create, human comments, documents/deliverables, and
pending-approval links. Preserve current domain permissions and
scheduling.
- Add durable mutation receipts across reconnects. Matching retries
replay results; uncertain outcomes keep the same request ID and require
inspection.
- Add consent and connection-management pages, OAuth log redaction,
shared plugin workflows, and separate OpenAI/Codex and Claude package
outputs.
- Add eight paid Product E2E cases across three models, independent
durable-state grading, usage evidence, cleanup, and report integration.
Add task-document guidance and regenerate the runner capability
inventories.
- Add migrations 0301 and 0302, the dated implementation plan, result
notes, and direct-client setup instructions in `doc/public-mcp.md`.

## Verification

- Merge integration `e180b1948`: resolved conflicts with current master,
preserved both eval registries, regenerated capability catalogs, and
regenerated migrations as 0301/0302 while keeping the original
replay-safe SQL byte-identical. Local migration safety/snapshot tests
(26), MCP/OAuth tests (38), redaction/OpenAPI tests (71), and eval
catalog/grading tests (198) pass. Token and capability gates pass. Full
recursive typecheck passed. Fresh Greptile review is 5/5 with no
unresolved findings. CI is green on this exact head (55 successes, two
intentional skips, one neutral result): one unchanged Cursor sandbox
test timed out at 10 seconds, then passed locally in 856 ms. A single
retry of that failed shard and the aggregate workflow passed. Merge
remains blocked on the repository code-owner approval rule.

Earlier checks passed at `6aa0962d4fb715f2190bb7bb22efacab2e58495d`: 55
successes, two intentional skips and one neutral result. [The earlier CI
run](https://github.com/paperclipai/paperclip/actions/runs/36901592350)
includes all test shards, browser tests, typecheck, build and canary dry
run. Greptile was 5/5 on that commit with no unresolved review threads.
GitHub still requires code-owner review under the repository merge
rules; passing checks do not bypass that approval. Paid source
fingerprints remain separate below and in the dated result note.

- Paid Events qualification passes **3/3**: GPT-5.4 Mini, Claude Haiku
4.5 and Claude Sonnet 4.6. Each uses a real public HTTPS callback,
signature verification and report retrieval in a fresh conversation. A
final Mini regression passes after the quota/status fixes. All evidence
validates. Bounded tunnel startup retries occur before provider calls
and remain visible; failed earlier attempts retain their original
grades.
- The earlier complete seven-case matrix passes **21/21**, with a
separate **3/3** delegation regression. Two preceding matrices also
passed 21/21 each. A complete 24-cell matrix including Events has not
been run. [The dated
results](doc/plans/2026-10-01-public-mcp-paid-eval-results.md) retain
exact source fingerprints, failures, model IDs and partial costs.
- Node 24: repository-wide `pnpm -r typecheck` and `pnpm build` pass
after merging master. Server typecheck passes after the final
quota/status changes. Eval typecheck and all 892 eval-support tests
pass.
- All 33 real MCP/OAuth tests pass. The preceding combined MCP,
redaction, private-address and DNS-rebinding run passed 129 tests; two
later MCP regressions cover quota reuse and unchanged-status
suppression. All 28 adjacent issue-tree/stale-lock route tests pass. CI
then found a null checkout result in the existing concurrent-workspace
path; logging now uses optional status access. All 12 closed-workspace
tests and all 33 MCP tests pass after that correction. The exact-start
event calibration exposed a timestamp gap; scanning now includes the
subscription start, with all 33 MCP tests and server typecheck passing.
These two narrow corrections follow the paid regression.
- A real Core → Cloud → Core authority round trip passes OAuth, MCP 2.0
subscription/delivery, current membership loss, unsubscribe, legacy SDK
tools, refresh and revocation. Its callback transport is a fixture with
independent HMAC verification. The paid Events campaigns separately
prove public HTTPS delivery.
- Earlier component qualification passed UI 7,117 tests, CLI 502, shared
832, skills catalog 20, database 160 and OpenAPI 10. Token gates, module
boundaries, migration order and plugin regeneration passed. CI covers
general/serialized suites, eight browser shards, runner checks,
typecheck, build and canary dry run.
- **Local full-suite limitation:** the earlier monolithic run was not
clean. It encountered overlapping schema rebuilding, Mac database
shared-memory limits and isolated CLI/fixture failures. Targeted reruns
passed. The existing >32 MiB Git filename stress test still hit its
300-second Mac timeout. The additional serialized sweep stopped after 62
passing suites once CI passed. Original failures and partial logs
remain; this PR does not claim a wholly green local monolithic run.
- Local Codex CLI and Claude Code OAuth login and MCP SDK
interoperability were verified. Public-store installation, actual
ChatGPT Work Cloud Events UI, staging HTTPS client behavior and hosted
newcomer provisioning remain release gates.

Enablement is moving to **Settings → Experimental → Assistant
connections (MCP)** in the stacked follow-up
[#14933](https://github.com/paperclipai/paperclip/pull/14933). Merge
both for the intended setup experience. This foundation branch alone
still uses `PAPERCLIP_PUBLIC_MCP_ENABLED=true`. After deployment, set
`PAPERCLIP_PUBLIC_URL` to the authenticated instance's HTTPS origin, and
connect to `/mcp/paperclip`. Select a team and allow writes in browser
consent. Configure an available agent and budget, then delegate and
retrieve results later. For Events, rescan the deployed plugin catalog
in ChatGPT Work Cloud; the host supplies its webhook credentials when
the user asks to watch a task. See [the setup
runbook](doc/public-mcp.md).

## Risks

- Events are at-least-once and may arrive out of order. No replay cursor
is advertised. Clients must refresh finite subscriptions, read current
state and avoid comment feedback loops. Callback material uses the
instance secrets master key; hosted subscriptions require the updated
Cloud broker and are bounded to five minutes/the access proof expiry.
- ChatGPT Work Cloud/dot event UI, plugin rescan and a hosted staging
subscription remain deployment gates. Local signed-webhook and paid
model evidence does not claim those surfaces have been exercised.
- Disabled by default. Merging adds schema and opt-in code; it does not
deploy a public endpoint, publish a store listing, create a team, or
start paid agents.
- Migrations 0301 and 0302 are additive and idempotent. Their SQL is
unchanged from the earlier preview numbers, so hash-aware upgrade
reconciliation preserves prior staging applications. Normal instance
upgrades must apply it before enabling MCP.
- Task creation and comments can schedule paid agent work. Consent and
tool descriptions disclose that effect. Revocation blocks future calls
but does not undo delegated work.
- Public deployments need edge rate limits and credential-safe logging.
Internal dispatch is restricted to the closed catalog and carries a
request-local verified actor.
- Hosted onboarding requires the companion Cloud broker, encryption-key
configuration, and tenant rollout. Self-hosted direct connections can
use this PR alone.
- Store acceptance and agent-mode participation are not claimed.
Checked-in plugin endpoints are development defaults; rebuild packages
for a real deployment before installation.

## Model Used

OpenAI GPT-6 in Codex, with reasoning, tool use, and code execution. A
more specific serving version and context-window size were not exposed
by the session. Paid eval models: `gpt-5.4-mini-2026-03-17`,
`claude-haiku-4-5-20251001`, and `claude-sonnet-4-6`.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass (targeted/component checks;
full local-run limitations are recorded above)
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <[email protected]>
2026-10-06 11:48:53 -05:00

898 lines
45 KiB
TypeScript

import { lazy, Suspense, type ReactNode } from "react";
import type { ToolConnectionCredentialSource } from "@paperclipai/shared";
import { Navigate, Outlet, Route, Routes, useActiveCompanyPrefix, useLocation, useParams } from "@/lib/router";
import { Button } from "@/components/ui/button";
import { useTranslation } from "@/i18n";
import { Layout } from "./components/Layout";
import { Layout as ProductionLayout } from "./components/Layout.production";
import { ConferenceRoomChatGate } from "./components/ConferenceRoomChatGate";
import { TaskChatLab } from "./pages/TaskChatLab";
import { PipelinesExperimentalGate } from "./components/PipelinesExperimentalGate";
import { CasesExperimentalGate } from "./components/CasesExperimentalGate";
import { StatusCardsExperimentalGate } from "./components/StatusCardsExperimentalGate";
import { CloudManagedPageGate } from "./components/CloudManagedPageGate";
import { HiddenSettingsPageGate } from "./components/HiddenSettingsPageGate";
import { IsolatedWorkspacesRouteGate } from "./components/IsolatedWorkspacesRouteGate";
import {
ExecutionWorkspaceCompanyGate,
UnprefixedExecutionWorkspaceRedirect,
} from "./components/UnprefixedExecutionWorkspaceRedirect";
import { useHiddenSettings } from "./hooks/useHiddenSettings";
import { Cases } from "./pages/Cases";
import { CaseDetail } from "./pages/CaseDetail";
import { OnboardingWizardVariant } from "./components/OnboardingWizardVariant";
import { CloudAccessGate } from "./components/CloudAccessGate";
import { PaperclipLoading } from "./components/AnimatedPaperclipIcon";
import { Dashboard } from "./pages/Dashboard";
import { DashboardLive } from "./pages/DashboardLive";
import { Timeline } from "./pages/Timeline";
import { Companies } from "./pages/Companies";
import { AGENT_FILTER_TABS, Agents } from "./pages/Agents";
import { AgentDetail } from "./pages/AgentDetail";
import { Projects } from "./pages/Projects";
import { ProjectDetail } from "./pages/ProjectDetail";
import { ProjectWorkspaceDetail } from "./pages/ProjectWorkspaceDetail";
import { Workspaces } from "./pages/Workspaces";
import { Issues } from "./pages/Issues";
import { Search } from "./pages/Search";
import { IssueDetail } from "./pages/IssueDetail";
import { AgentChats } from "./pages/AgentChats";
import { AgentChat } from "./pages/AgentChat";
import { IssueChatLongThreadPerf } from "./pages/IssueChatLongThreadPerf";
import { Routines } from "./pages/Routines";
import { Learnings, PipelineItemDetail, PipelineItemLegacyRedirect, Pipelines, ReviewQueue } from "./pages/Pipelines";
import { PipelineSettings } from "./pages/PipelineSettings";
import { StatusCards } from "./pages/StatusCards";
import { RoutineDetail } from "./pages/RoutineDetail";
import { UserProfile } from "./pages/UserProfile";
import { ExecutionWorkspaceDetail } from "./pages/ExecutionWorkspaceDetail";
import { Goals } from "./pages/Goals";
import { Artifacts } from "./pages/Artifacts";
import { GoalDetail } from "./pages/GoalDetail";
import { Approvals } from "./pages/Approvals";
import { ApprovalDetail } from "./pages/ApprovalDetail";
import { CompanyActivity } from "./pages/audit/CompanyActivity";
import { AuditHub } from "./pages/audit/AuditHub";
import { Inbox } from "./pages/Inbox";
import { useCombinedInboxTasksEnabled } from "./hooks/useCombinedInboxTasksEnabled";
import { WhatNeedsMe } from "./pages/WhatNeedsMe";
import { DecisionQueuePage } from "./pages/DecisionQueuePage";
import { BoardChat } from "./pages/BoardChat";
import { CompanySettings } from "./pages/CompanySettings";
import { CompanyEnvironments } from "./pages/CompanyEnvironments";
import { BootstrapSetupUxLab } from "./pages/BootstrapSetupUxLab";
import { ResponsibleUserDenialUxLab } from "./pages/ResponsibleUserDenialUxLab";
import { CrossIssueCollaborationUxLab } from "./pages/CrossIssueCollaborationUxLab";
import { CompanySettingsPluginPage } from "./pages/CompanySettingsPluginPage";
import { CompanyAccess, CompanyAccessLegacyRoute } from "./pages/CompanyAccess";
import { AdvancedToolsRoute } from "./pages/tools/AdvancedToolsRoute";
import { ProfileWizardRoute } from "./pages/tools/profiles/ProfileWizardRoute";
import { ProfileDetailRoute } from "./pages/tools/profiles/ProfileDetailRoute";
import { Browse } from "./pages/apps/Browse";
import { AppsConnect } from "./pages/apps/AppsConnect";
import { ChatEndpointSetup } from "./pages/apps/chat/ChatEndpointSetup";
import { ChatEndpointDetail } from "./pages/apps/chat/ChatEndpointDetail";
import { ChatIdentityConfirm } from "./pages/apps/chat/ChatIdentityConfirm";
import { ChatConnectorsExperimentalGate } from "./components/ChatConnectorsExperimentalGate";
import { useChatConnectorsEnabled } from "./hooks/useChatConnectorsEnabled";
import { canEnterAppsConnect } from "./pages/apps/app-connect-policy";
import { AppsReview } from "./pages/apps/AppsReview";
import { AppDetail } from "./pages/apps/AppDetail";
import { AppNotConnected } from "./pages/apps/AppNotConnected";
import { PaperclipCloudOAuthHandoffPage } from "./pages/apps/PaperclipCloudOAuthHandoff";
import { GatewaysList } from "./pages/apps/gateways/GatewaysList";
import { GatewayDetail } from "./pages/apps/gateways/GatewayDetail";
import { CompanySkills } from "./pages/CompanySkills";
import { SkillSources } from "./pages/SkillSources";
import { SkillStudio } from "./pages/SkillStudio";
import { Secrets } from "./pages/Secrets";
import { CompanyImport } from "./pages/CompanyImport";
import { DesignGuide } from "./pages/DesignGuide";
import { InstanceExperimentalSettings } from "./pages/InstanceExperimentalSettings";
import { InstanceAccess } from "./pages/InstanceAccess";
import { ProfileSettings } from "./pages/ProfileSettings";
import { PluginManager } from "./pages/PluginManager";
import { PluginSettings } from "./pages/PluginSettings";
import { AdapterManager } from "./pages/AdapterManager";
import { PluginPage } from "./pages/PluginPage";
import { NewAgent } from "./pages/NewAgent";
import { AuthPage } from "./pages/Auth";
import { BoardClaimPage } from "./pages/BoardClaim";
import { McpConnectPage, AssistantConnectionsPage } from "./pages/McpConnect";
import { CliAuthPage } from "./pages/CliAuth";
import { InviteLandingPage } from "./pages/InviteLanding";
import { JoinRequestQueue } from "./pages/JoinRequestQueue";
import { NotFoundPage } from "./pages/NotFound";
import { useCompany } from "./context/CompanyContext";
import { useDialogActions, useDialogState } from "./context/DialogContext";
import { loadLastInboxTab } from "./lib/inbox";
import { TASK_VIEW_PARAM, taskViewForInboxTab, type TaskViewKey } from "./lib/task-views";
import {
isOnboardingWizardActive,
onboardingStepForCompany,
shouldRedirectCompanylessRouteToOnboarding,
} from "./lib/onboarding-route";
import { filterHiddenInstanceSettingsPath, normalizeRememberedInstanceSettingsPath } from "./lib/instance-settings";
import { useCloudInstance } from "./hooks/useCloudInstance";
import { useStreamlinedUiEnabled } from "./hooks/useStreamlinedUiEnabled";
import { cloudStackCreateUrl } from "./lib/cloudLinks";
import { navigateTopLevel } from "@/lib/browserNavigation";
const CompanyExport = lazy(() =>
import("./pages/CompanyExport").then((module) => ({ default: module.CompanyExport })),
);
const ProductionAgents = lazy(() =>
import("./pages/Agents.production").then((module) => ({ default: module.Agents })),
);
const ProductionRoutines = lazy(() =>
import("./pages/Routines.production").then((module) => ({ default: module.Routines })),
);
const ProductionRoutineDetail = lazy(() =>
import("./pages/RoutineDetail.production").then((module) => ({ default: module.RoutineDetail })),
);
const ProductionCompanySkills = lazy(() =>
import("./pages/CompanySkills.production").then((module) => ({ default: module.CompanySkills })),
);
const ProductionCompanyActivity = lazy(() =>
import("./pages/audit/CompanyActivity.production").then((module) => ({ default: module.CompanyActivity })),
);
const ProductionCosts = lazy(() =>
import("./pages/Costs.production").then((module) => ({ default: module.Costs })),
);
const ProductionOrgChart = lazy(() =>
import("./pages/OrgChart.production").then((module) => ({ default: module.OrgChart })),
);
function ProductionSurface({ children }: { children: ReactNode }) {
return <Suspense fallback={<PaperclipLoading />}>{children}</Suspense>;
}
function boardRoutes(streamlinedUiEnabled: boolean, combinedInboxTasksEnabled: boolean) {
// Combined Inbox + Task List (PAP-670) only exists in the streamlined shell.
const mergedTasks = streamlinedUiEnabled && combinedInboxTasksEnabled;
return (
<>
<Route index element={<Navigate to="dashboard" replace />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="dashboard/live" element={<DashboardLive />} />
<Route
path="timeline"
element={streamlinedUiEnabled ? <AuditCompatibilityRedirect to="/activity/timeline" /> : <Timeline />}
/>
<Route path="onboarding" element={<OnboardingRoutePage />} />
<Route path="companies" element={<Companies />} />
<Route path="company/settings" element={<CompanySettings />} />
<Route path="company/settings/environments" element={<Navigate to="/company/settings/instance/environments" replace />} />
<Route path="company/settings/cloud-upstream" element={<Navigate to="/company/export" replace />} />
<Route element={<HiddenSettingsPageGate pageKey="company.members" />}>
<Route path="company/settings/members" element={<CompanyAccess />} />
<Route path="company/settings/access" element={<CompanyAccessLegacyRoute />} />
</Route>
{/* Invites moved into the Members page; the old URL redirects (and stays
gated so a hidden Invites surface never round-trips through it). */}
<Route element={<HiddenSettingsPageGate pageKey="company.invites" />}>
<Route
path="company/settings/invites"
element={<Navigate to="/company/settings/members?tab=invites" replace />}
/>
</Route>
<Route element={<HiddenSettingsPageGate pageKey="company.export" />}>
<Route
path="company/export/*"
element={(
<Suspense fallback={<PaperclipLoading />}>
<CompanyExport />
</Suspense>
)}
/>
</Route>
<Route element={<CloudManagedPageGate />}>
<Route element={<HiddenSettingsPageGate pageKey="company.import" />}>
<Route path="company/import" element={<CompanyImport />} />
</Route>
</Route>
<Route element={<HiddenSettingsPageGate pageKey="company.secrets" />}>
<Route path="company/settings/secrets" element={<Secrets />} />
</Route>
<Route path="company/settings/tools" element={<LegacyToolsSettingsRedirect />} />
<Route path="company/settings/tools/:tab" element={<LegacyToolsSettingsRedirect />} />
<Route path="tools" element={<LegacyToolsRedirect />} />
<Route path="tools/:tab" element={<LegacyToolsRedirect />} />
<Route path="apps" element={<Browse />} />
<Route path="apps/browse" element={<Navigate to="/apps" replace />} />
<Route path="apps/connections" element={<Navigate to="/apps" replace />} />
<Route path="apps/byo" element={<AppsConnect byoOnly />} />
<Route
path="apps/vercel-connect"
element={<AppsConnectEntryRoute credentialSource="vercel_connect" />}
/>
<Route path="apps/connect" element={<AppsConnectEntryRoute />} />
<Route path="apps/chat/connect" element={
<ChatConnectorsExperimentalGate><ChatEndpointSetup /></ChatConnectorsExperimentalGate>
} />
<Route path="apps/chat/:endpointId" element={
<ChatConnectorsExperimentalGate><Navigate to="settings" replace /></ChatConnectorsExperimentalGate>
} />
<Route path="apps/chat/:endpointId/:tab" element={
<ChatConnectorsExperimentalGate><ChatEndpointDetail /></ChatConnectorsExperimentalGate>
} />
<Route path="apps/connect/:appKey" element={<Navigate to="/apps" replace />} />
<Route path="apps/connect/:appKey/:stage" element={<Navigate to="/apps" replace />} />
<Route path="apps/review" element={<AppsReview />} />
{/* Connector health is inline on the Apps landing page; keep legacy links working. */}
<Route path="apps/attention" element={<Navigate to="/apps" replace />} />
<Route path="apps/gateways" element={<GatewaysList />} />
<Route path="apps/gateways/:gatewayId" element={<Navigate to="overview" replace />} />
<Route path="apps/gateways/:gatewayId/:tab" element={<GatewayDetail />} />
<Route path="apps/advanced" element={<AdvancedToolsRoute />} />
<Route path="apps/advanced/gateways" element={<GatewaysList />} />
<Route path="apps/advanced/profiles/new" element={<ProfileWizardRoute mode="new" />} />
<Route path="apps/advanced/profiles/:profileId/edit" element={<ProfileWizardRoute mode="edit" />} />
<Route path="apps/advanced/profiles/:profileId" element={<ProfileDetailRoute />} />
<Route path="apps/advanced/audit" element={<Navigate to="/apps" replace />} />
<Route path="apps/advanced/run-your-own" element={<Navigate to="/apps" replace />} />
<Route path="apps/advanced/:tab" element={<AdvancedToolsRoute />} />
<Route path="apps/app/:applicationId" element={<AppNotConnected />} />
<Route path="apps/app/:applicationId/:tab" element={<AppNotConnected />} />
<Route path="apps/:connectionId" element={<Navigate to="permissions" replace />} />
<Route path="apps/:connectionId/:tab" element={<AppDetail />} />
<Route path="company/settings/instance" element={<Navigate to="/company/settings" replace />} />
<Route element={<HiddenSettingsPageGate pageKey="instance.profile" />}>
<Route path="company/settings/instance/profile" element={<ProfileSettings />} />
</Route>
<Route path="company/settings/instance/general" element={<Navigate to="/company/settings" replace />} />
<Route path="company/settings/instance/heartbeats" element={<Navigate to="/company/settings" replace />} />
<Route element={<HiddenSettingsPageGate pageKey="instance.environments" />}>
<Route path="company/settings/instance/environments" element={<CompanyEnvironments />} />
<Route path="company/settings/instance/environments/new" element={<CompanyEnvironments mode="create" />} />
<Route path="company/settings/instance/environments/:environmentId/edit" element={<CompanyEnvironments mode="edit" />} />
</Route>
<Route element={<HiddenSettingsPageGate pageKey="instance.access" />}>
<Route path="company/settings/instance/access" element={<InstanceAccess />} />
</Route>
<Route element={<HiddenSettingsPageGate pageKey="instance.experimental" />}>
<Route path="company/settings/instance/experimental" element={<InstanceExperimentalSettings />} />
</Route>
<Route element={<HiddenSettingsPageGate pageKey="instance.plugins" />}>
<Route path="company/settings/instance/plugins" element={<PluginManager />} />
<Route path="company/settings/instance/plugins/:pluginId" element={<PluginSettings />} />
</Route>
<Route element={<HiddenSettingsPageGate pageKey="instance.adapters" />}>
<Route path="company/settings/instance/adapters" element={<AdapterManager />} />
</Route>
<Route path="company/settings/:settingsRoutePath/*" element={<CompanySettingsPluginPage />} />
<Route path="skills/sources" element={<SkillSources />} />
<Route path="skills/sources/:sourceId" element={<SkillSources />} />
<Route path="skills/studio" element={<SkillStudio />} />
<Route path="skills/studio/new" element={<SkillStudio />} />
<Route path="skills/studio/:skillId" element={<SkillStudio />} />
<Route path="skills/:skillId/studio" element={<LegacySkillStudioRedirect />} />
<Route
path="skills/*"
element={streamlinedUiEnabled ? <CompanySkills /> : <ProductionSurface><ProductionCompanySkills /></ProductionSurface>}
/>
<Route path="settings" element={<LegacySettingsRedirect />} />
<Route path="settings/*" element={<LegacySettingsRedirect />} />
<Route path="plugins/:pluginId" element={<PluginPage />} />
<Route
path="org"
element={streamlinedUiEnabled ? <Navigate to="/agents/all" replace /> : <ProductionSurface><ProductionOrgChart /></ProductionSurface>}
/>
<Route path="agents" element={<Navigate to="/agents/all" replace />} />
{AGENT_FILTER_TABS.map((tab) => (
<Route
key={tab}
path={`agents/${tab}`}
element={streamlinedUiEnabled ? <Agents /> : <ProductionSurface><ProductionAgents /></ProductionSurface>}
/>
))}
<Route path="agents/new" element={<NewAgent />} />
<Route path="agents/:agentId" element={<AgentDetail />} />
<Route path="agents/:agentId/:tab" element={<AgentDetail />} />
<Route path="agents/:agentId/runs/:runId" element={<AgentDetail />} />
<Route path="projects" element={<Projects />} />
<Route path="projects/:projectId" element={<ProjectDetail />} />
<Route path="projects/:projectId/overview" element={<ProjectDetail />} />
<Route path="projects/:projectId/issues" element={<ProjectDetail />} />
<Route path="projects/:projectId/issues/:filter" element={<ProjectDetail />} />
<Route element={<IsolatedWorkspacesRouteGate />}>
<Route path="projects/:projectId/workspaces/:workspaceId" element={<ProjectWorkspaceDetail />} />
</Route>
<Route path="projects/:projectId/workspaces" element={<ProjectDetail />} />
<Route path="projects/:projectId/configuration" element={<ProjectDetail />} />
<Route path="projects/:projectId/budget" element={<ProjectDetail />} />
<Route element={<IsolatedWorkspacesRouteGate />}>
<Route path="workspaces" element={<Workspaces />} />
</Route>
<Route path="issues" element={<Issues />} />
<Route path="tasks" element={<Navigate to="/issues" replace />} />
<Route path="search" element={<Search />} />
{mergedTasks ? (
<>
{/* Combined Inbox + Task List: the status presets are real views, not aliases of /issues. */}
<Route path="issues/all" element={<TaskViewRedirect view="all" />} />
<Route path="issues/active" element={<TaskViewRedirect view="active" />} />
<Route path="issues/backlog" element={<TaskViewRedirect view="backlog" />} />
<Route path="issues/done" element={<TaskViewRedirect view="done" />} />
<Route path="issues/recent" element={<TaskViewRedirect view="recent" />} />
</>
) : (
<>
<Route path="issues/all" element={<Navigate to="/issues" replace />} />
<Route path="issues/active" element={<Navigate to="/issues" replace />} />
<Route path="issues/backlog" element={<Navigate to="/issues" replace />} />
<Route path="issues/done" element={<Navigate to="/issues" replace />} />
<Route path="issues/recent" element={<Navigate to="/issues" replace />} />
</>
)}
<Route path="chats" element={<AgentChats />} />
<Route path="chats/:agentRef" element={<AgentChat />} />
<Route path="issues/:issueId" element={<IssueDetail />} />
{import.meta.env.DEV ? (
<Route path="tests/perf/long-thread" element={<IssueChatLongThreadPerf />} />
) : null}
<Route path="routines" element={streamlinedUiEnabled ? <Routines /> : <ProductionSurface><ProductionRoutines /></ProductionSurface>} />
<Route
path="cases"
element={<CasesExperimentalGate><Cases /></CasesExperimentalGate>}
/>
<Route
path="cases/:caseIdentifier"
element={<CasesExperimentalGate><CaseDetail /></CasesExperimentalGate>}
/>
<Route
path="status"
element={<StatusCardsExperimentalGate><StatusCards /></StatusCardsExperimentalGate>}
/>
<Route
path="status/:cardId"
element={<StatusCardsExperimentalGate><StatusCards /></StatusCardsExperimentalGate>}
/>
{/* Back-compat: the board lived at /status-cards before PAP-15223. */}
<Route path="status-cards" element={<StatusCardsLegacyRedirect />} />
<Route path="status-cards/:cardId" element={<StatusCardsLegacyRedirect />} />
<Route
path="review-queue"
element={<PipelinesExperimentalGate><ReviewQueue /></PipelinesExperimentalGate>}
/>
<Route
path="learnings"
element={<PipelinesExperimentalGate><Learnings /></PipelinesExperimentalGate>}
/>
<Route
path="pipelines"
element={<PipelinesExperimentalGate><Pipelines /></PipelinesExperimentalGate>}
/>
<Route
path="pipelines/:pipelineId"
element={<PipelinesExperimentalGate><Pipelines /></PipelinesExperimentalGate>}
/>
<Route
path="pipelines/:pipelineId/add"
element={<PipelinesExperimentalGate><Pipelines /></PipelinesExperimentalGate>}
/>
<Route
path="pipelines/:pipelineId/settings"
element={<PipelinesExperimentalGate><PipelineSettings /></PipelinesExperimentalGate>}
/>
<Route
path="pipelines/:pipelineId/items/:caseId"
element={<PipelinesExperimentalGate><PipelineItemDetail /></PipelinesExperimentalGate>}
/>
<Route
path="pipelines/:pipelineId/cases/:caseId"
element={<PipelinesExperimentalGate><PipelineItemLegacyRedirect /></PipelinesExperimentalGate>}
/>
<Route path="routines/:routineId" element={streamlinedUiEnabled ? <RoutineDetail /> : <ProductionSurface><ProductionRoutineDetail /></ProductionSurface>} />
<Route path="routines/:routineId/:section" element={streamlinedUiEnabled ? <RoutineDetail /> : <ProductionSurface><ProductionRoutineDetail /></ProductionSurface>} />
<Route element={<IsolatedWorkspacesRouteGate />}>
<Route element={<ExecutionWorkspaceCompanyGate />}>
<Route path="execution-workspaces/:workspaceId" element={<ExecutionWorkspaceDetail />} />
<Route path="execution-workspaces/:workspaceId/services" element={<ExecutionWorkspaceDetail />} />
<Route path="execution-workspaces/:workspaceId/configuration" element={<ExecutionWorkspaceDetail />} />
<Route path="execution-workspaces/:workspaceId/runtime-logs" element={<ExecutionWorkspaceDetail />} />
<Route path="execution-workspaces/:workspaceId/issues" element={<ExecutionWorkspaceDetail />} />
<Route path="execution-workspaces/:workspaceId/routines" element={<ExecutionWorkspaceDetail />} />
</Route>
</Route>
<Route path="goals" element={<Goals />} />
<Route path="goals/:goalId" element={<GoalDetail />} />
<Route path="artifacts" element={<Artifacts />} />
<Route path="approvals" element={<Navigate to="/approvals/pending" replace />} />
<Route path="approvals/pending" element={<Approvals />} />
<Route path="approvals/all" element={<Approvals />} />
<Route path="approvals/:approvalId" element={<ApprovalDetail />} />
<Route path="activity" element={streamlinedUiEnabled ? <CompanyActivity /> : <ProductionSurface><ProductionCompanyActivity /></ProductionSurface>} />
{streamlinedUiEnabled ? (
<>
<Route path="activity/runs" element={<AuditHub section="runs" />} />
<Route path="activity/costs" element={<AuditHub section="costs" />} />
<Route path="activity/budgets" element={<AuditHub section="budgets" />} />
<Route path="activity/timeline" element={<AuditHub section="timeline" />} />
<Route path="audit" element={<AuditCompatibilityRedirect to="/activity" forceAgentMode />} />
<Route path="audit/activity" element={<AuditCompatibilityRedirect to="/activity" />} />
<Route path="audit/runs" element={<AuditCompatibilityRedirect to="/activity/runs" />} />
<Route path="audit/costs" element={<AuditCompatibilityRedirect to="/activity/costs" />} />
<Route path="audit/budgets" element={<AuditCompatibilityRedirect to="/activity/budgets" />} />
<Route path="audit/timeline" element={<AuditCompatibilityRedirect to="/activity/timeline" />} />
<Route path="runs" element={<AuditCompatibilityRedirect to="/activity/runs" />} />
<Route path="costs" element={<AuditCompatibilityRedirect to="/activity/costs" />} />
<Route path="budgets" element={<AuditCompatibilityRedirect to="/activity/budgets" />} />
</>
) : (
<>
<Route path="costs" element={<ProductionSurface><ProductionCosts /></ProductionSurface>} />
<Route path="audit" element={<Navigate to="/activity?mode=agents" replace />} />
</>
)}
{/* Conference Room Chat surfaces (PAP-136/PAP-137): routes stay
registered but redirect to the company home while the experimental
flag is off. The board-level `artifacts` mount below is the new
conference-room one; the master-level mount above it still serves
`/artifacts` in both modes. */}
<Route element={<ConferenceRoomChatGate />}>
<Route path="board-chat" element={<BoardChat />} />
<Route path="artifacts" element={<Artifacts />} />
</Route>
{/* Task chat dev harness — dev builds only. */}
{import.meta.env.DEV ? (
<Route path="dev/task-chat-lab" element={<TaskChatLab />} />
) : null}
<Route path="decisions" element={<WhatNeedsMe />} />
<Route path="decisions/queues/:key" element={<DecisionQueuePage />} />
{/* Combined Inbox + Task List: Inbox is a view inside Tasks. Every /inbox/* URL still
resolves — it redirects into the matching view — and with the flag off
(or in the legacy shell) the standalone pages stay. /inbox/requests
stays its own page either way; Settings → Members links straight to it. */}
{mergedTasks ? (
<>
<Route path="inbox" element={<InboxRootRedirect />} />
<Route path="inbox/mine" element={<TaskViewRedirect view="mine" />} />
<Route path="inbox/recent" element={<TaskViewRedirect view="recent" />} />
<Route path="inbox/unread" element={<TaskViewRedirect view="unread" />} />
<Route path="inbox/blocked" element={<TaskViewRedirect view="blocked" />} />
<Route path="inbox/all" element={<TaskViewRedirect view="everything" />} />
<Route path="inbox/new" element={<TaskViewRedirect view="mine" />} />
</>
) : (
<>
<Route path="inbox" element={<InboxRootRedirect />} />
<Route path="inbox/mine" element={<Inbox />} />
<Route path="inbox/recent" element={<Inbox />} />
<Route path="inbox/unread" element={<Inbox />} />
<Route path="inbox/blocked" element={<Inbox />} />
<Route path="inbox/all" element={<Inbox />} />
<Route path="inbox/new" element={<Navigate to="/inbox/mine" replace />} />
</>
)}
<Route path="inbox/requests" element={<JoinRequestQueue />} />
<Route path="u/:userSlug" element={<UserProfile />} />
<Route path="design-guide" element={<DesignGuide />} />
<Route path="instance/settings/adapters" element={<AdapterManager />} />
<Route path=":pluginRoutePath/*" element={<PluginPage />} />
<Route path="*" element={<NotFoundPage scope="board" />} />
</>
);
}
function AppsConnectEntryRoute({
credentialSource = "paperclip_vault",
}: {
credentialSource?: ToolConnectionCredentialSource;
} = {}) {
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const { enabled: chatConnectorsEnabled } = useChatConnectorsEnabled();
return canEnterAppsConnect(searchParams, { chatConnectorsEnabled })
? <AppsConnect credentialSource={credentialSource} />
: <Navigate to="/apps" replace />;
}
function InboxRootRedirect() {
const { enabled: streamlinedUiEnabled } = useStreamlinedUiEnabled();
const { enabled: combinedInboxTasksEnabled } = useCombinedInboxTasksEnabled();
return streamlinedUiEnabled && combinedInboxTasksEnabled
? <TaskViewRedirect view={taskViewForInboxTab(loadLastInboxTab())} />
: <Navigate to={`/inbox/${loadLastInboxTab()}`} replace />;
}
/** Sends a retired Inbox/Tasks URL to its view on the merged Tasks surface. */
function TaskViewRedirect({ view }: { view: TaskViewKey }) {
const location = useLocation();
const params = new URLSearchParams(location.search);
params.set(TASK_VIEW_PARAM, view);
return <Navigate to={`/issues?${params.toString()}${location.hash}`} replace />;
}
function LegacySkillStudioRedirect() {
const location = useLocation();
const { companies, selectedCompany, loading } = useCompany();
const { companyPrefix, skillId } = useParams<{ companyPrefix?: string; skillId?: string }>();
if (loading) return null;
const targetCompany =
(companyPrefix
? companies.find((company) => company.issuePrefix.toUpperCase() === companyPrefix.toUpperCase())
: null) ??
selectedCompany ??
companies[0] ??
null;
if (!targetCompany || !skillId) {
return <Navigate to="/skills/studio" replace />;
}
return (
<Navigate
to={`/${targetCompany.issuePrefix}/skills/studio/${encodeURIComponent(skillId)}${location.search}${location.hash}`}
replace
/>
);
}
function LegacySettingsRedirect() {
const location = useLocation();
const { companies, selectedCompany, loading } = useCompany();
const { companyPrefix } = useParams<{ companyPrefix?: string }>();
const { hidden: hiddenSettings } = useHiddenSettings();
if (loading) {
return <PaperclipLoading />;
}
const targetCompany =
(companyPrefix
? companies.find((company) => company.issuePrefix.toUpperCase() === companyPrefix.toUpperCase())
: null) ??
selectedCompany ??
companies[0] ??
null;
if (!targetCompany) {
if (
shouldRedirectCompanylessRouteToOnboarding({
pathname: location.pathname,
hasCompanies: false,
})
) {
return <Navigate to="/onboarding" replace />;
}
return <NoCompaniesStartPage />;
}
const normalizedPath = filterHiddenInstanceSettingsPath(
normalizeRememberedInstanceSettingsPath(
`${location.pathname}${location.search}${location.hash}`,
),
hiddenSettings,
);
return (
<Navigate
to={`/${targetCompany.issuePrefix}${normalizedPath}`}
replace
/>
);
}
function LegacyToolsSettingsRedirect() {
const { tab } = useParams<{ tab?: string }>();
return <Navigate to={legacyToolsRedirectTarget(tab)} replace />;
}
// The developer "Tools" surface moved under the Apps "Advanced setup" door
// (PAP-10862). `/tools` and `/tools/:tab` redirect to their new home.
function LegacyToolsRedirect() {
const { tab } = useParams<{ tab?: string }>();
return <Navigate to={legacyToolsRedirectTarget(tab)} replace />;
}
function legacyToolsRedirectTarget(tab?: string) {
if (!tab) return "/apps/advanced/profiles";
if (tab === "applications" || tab === "connections" || tab === "overview" || tab === "examples") return "/apps";
if (tab === "runtime" || tab === "audit") return "/apps";
if (tab === "policies") return "/apps/advanced/profiles";
return `/apps/advanced/${tab}`;
}
export function OnboardingRoutePage() {
const { companies } = useCompany();
const { openOnboarding } = useDialogActions();
const { t } = useTranslation();
const cloudInstance = useCloudInstance();
const createStackUrl = cloudStackCreateUrl(cloudInstance?.cloudBaseUrl ?? null);
const { onboardingOpen, onboardingRouteDismissed } = useDialogState();
const { companyPrefix } = useParams<{ companyPrefix?: string }>();
const matchedCompany = companyPrefix
? companies.find((company) => company.issuePrefix.toUpperCase() === companyPrefix.toUpperCase()) ?? null
: null;
// The OnboardingWizard auto-opens on this route (and can also be opened
// explicitly). While it is showing it covers the whole screen, so the
// launcher card below must not stay interactive behind it — otherwise users
// can tab/click through to the form behind the modal (PAP-52). The launcher
// only needs to render as a re-entry point once the wizard is dismissed.
if (isOnboardingWizardActive({ onboardingOpen, routeDismissed: onboardingRouteDismissed })) {
return null;
}
const title = matchedCompany
? `Add another agent to ${matchedCompany.name}`
: companies.length > 0
? "Create another organization"
: "Create your first organization";
const description = matchedCompany
? "Run onboarding again to add an agent and a starter task for this organization."
: companies.length > 0
? "Run onboarding again to create another organization and seed its first agent."
: "Get started by creating an organization and your first agent.";
return (
<div className="mx-auto max-w-xl py-10">
<div className="rounded-lg border border-border bg-card p-6">
<h1 className="text-xl font-semibold">{title}</h1>
<p className="mt-2 text-sm text-muted-foreground">{description}</p>
<div className="mt-4">
{/* On a managed stack whose Cloud origin is unknown there is nowhere
to send this click: creation lives on Cloud, and in-app creation
is a 403 floor. A button that does nothing is worse than none, so
say why instead of rendering an inert control. */}
{!matchedCompany && cloudInstance && !createStackUrl ? (
<p className="text-sm text-muted-foreground">
{t("app.cloudCreateUnavailable", {
defaultValue:
"Organizations are created in Paperclip Cloud. This instance can't reach it right now — try again from your Cloud portfolio.",
})}
</p>
) : (
<Button
onClick={() =>
matchedCompany
? openOnboarding({
// "Add another agent" to a company that already has its
// mission must not stop to ask for the mission again. An
// unsettled or failed lookup reads as "no mission" and
// costs the step, which the customer can pass - and the
// mission step now updates the existing goal rather than
// adding a second one.
initialStep: onboardingStepForCompany(),
companyId: matchedCompany.id,
})
: cloudInstance && createStackUrl
? navigateTopLevel(createStackUrl)
: openOnboarding()
}
>
{matchedCompany ? "Add Agent" : "Start Onboarding"}
</Button>
)}
</div>
</div>
</div>
);
}
function CompanyRootRedirect() {
const { companies, selectedCompany, loading } = useCompany();
const location = useLocation();
if (loading) {
return <PaperclipLoading />;
}
const targetCompany = selectedCompany ?? companies[0] ?? null;
if (!targetCompany) {
if (
shouldRedirectCompanylessRouteToOnboarding({
pathname: location.pathname,
hasCompanies: false,
})
) {
return <Navigate to="/onboarding" replace />;
}
return <NoCompaniesStartPage />;
}
return <Navigate to={`/${targetCompany.issuePrefix}/dashboard`} replace />;
}
function StatusCardsLegacyRedirect() {
const { cardId } = useParams<{ cardId?: string }>();
const prefix = useActiveCompanyPrefix();
const base = prefix ? `/${prefix}` : "";
return <Navigate to={`${base}/status${cardId ? `/${cardId}` : ""}`} replace />;
}
function AuditCompatibilityRedirect({
to,
forceAgentMode = false,
}: {
to: string;
forceAgentMode?: boolean;
}) {
const location = useLocation();
const searchParams = new URLSearchParams(location.search);
if (forceAgentMode) searchParams.set("mode", "agents");
const search = searchParams.toString();
return <Navigate to={`${to}${search ? `?${search}` : ""}${location.hash}`} replace />;
}
function UnprefixedBoardRedirect() {
const location = useLocation();
const { companies, selectedCompany, loading } = useCompany();
if (loading) {
return <PaperclipLoading />;
}
const targetCompany = selectedCompany ?? companies[0] ?? null;
if (!targetCompany) {
if (
shouldRedirectCompanylessRouteToOnboarding({
pathname: location.pathname,
hasCompanies: false,
})
) {
return <Navigate to="/onboarding" replace />;
}
return <NoCompaniesStartPage />;
}
return (
<Navigate
to={`/${targetCompany.issuePrefix}${location.pathname}${location.search}${location.hash}`}
replace
/>
);
}
function NoCompaniesStartPage() {
const { openOnboarding } = useDialogActions();
const { t } = useTranslation();
// A managed stack with no visible companies is a loading or error state, not
// an invitation to create one in-app — creation lives on Cloud (403 floor).
const cloudInstance = useCloudInstance();
const createStackUrl = cloudStackCreateUrl(cloudInstance?.cloudBaseUrl ?? null);
return (
<div className="mx-auto max-w-xl py-10">
<div className="rounded-lg border border-border bg-card p-6">
<h1 className="text-xl font-semibold">
{t("app.noCompanies.title", { defaultValue: "Create your first organization" })}
</h1>
<p className="mt-2 text-sm text-muted-foreground">
{t("app.noCompanies.description", { defaultValue: "Get started by creating an organization." })}
</p>
<div className="mt-4">
{/* Same as the onboarding route: no Cloud origin means nowhere to
send the click, and in-app creation is a 403 floor here. */}
{cloudInstance && !createStackUrl ? (
<p className="text-sm text-muted-foreground">
{t("app.cloudCreateUnavailable", {
defaultValue:
"Organizations are created in Paperclip Cloud. This instance can't reach it right now — try again from your Cloud portfolio.",
})}
</p>
) : (
<Button
onClick={() =>
cloudInstance && createStackUrl
? navigateTopLevel(createStackUrl)
: openOnboarding()
}
>
{t("app.noCompanies.newCompany", { defaultValue: "New Organization" })}
</Button>
)}
</div>
</div>
</div>
);
}
export function App() {
const { enabled: streamlinedUiEnabled, loaded: streamlinedUiLoaded } = useStreamlinedUiEnabled();
const { enabled: combinedInboxTasksEnabled } = useCombinedInboxTasksEnabled();
return (
<>
<Routes>
<Route path="oauth-handoff" element={<PaperclipCloudOAuthHandoffPage />} />
<Route path="auth" element={<AuthPage />} />
<Route path="board-claim/:token" element={<BoardClaimPage />} />
<Route path="mcp-connect/:id" element={<McpConnectPage />} />
<Route path="assistant-connections" element={<AssistantConnectionsPage />} />
<Route path="cli-auth/:id" element={<CliAuthPage />} />
<Route path="invite/:token" element={<InviteLandingPage />} />
<Route element={streamlinedUiLoaded ? <CloudAccessGate allowMembershipRequest /> : <PaperclipLoading />}>
{/* The identity APIs enforce the chat rollout flag. Nonmembers cannot
read experimental settings, but a private invitation may request membership. */}
<Route path="chat-identity/confirm" element={<ChatIdentityConfirm />} />
</Route>
<Route path="tests/perf/long-thread" element={<IssueChatLongThreadPerf />} />
<Route path="ux-lab/bootstrap-setup" element={<BootstrapSetupUxLab />} />
<Route path="ux-lab/responsible-user-denial" element={<ResponsibleUserDenialUxLab />} />
<Route path="ux-lab/cross-issue-collaboration" element={<CrossIssueCollaborationUxLab />} />
<Route element={streamlinedUiLoaded ? <CloudAccessGate /> : <PaperclipLoading />}>
<Route index element={<CompanyRootRedirect />} />
<Route path="onboarding" element={<OnboardingRoutePage />} />
<Route path="instance" element={<LegacySettingsRedirect />} />
<Route path="instance/settings" element={<LegacySettingsRedirect />} />
<Route path="instance/settings/*" element={<LegacySettingsRedirect />} />
<Route path="companies" element={<UnprefixedBoardRedirect />} />
<Route path="issues" element={<UnprefixedBoardRedirect />} />
<Route path="tasks" element={<UnprefixedBoardRedirect />} />
<Route path="issues/:issueId" element={<UnprefixedBoardRedirect />} />
<Route path="routines" element={<UnprefixedBoardRedirect />} />
<Route path="routines/:routineId" element={<UnprefixedBoardRedirect />} />
<Route path="review-queue" element={<UnprefixedBoardRedirect />} />
<Route path="learnings" element={<UnprefixedBoardRedirect />} />
<Route path="cases" element={<UnprefixedBoardRedirect />} />
<Route path="cases/:caseIdentifier" element={<UnprefixedBoardRedirect />} />
<Route path="status" element={<UnprefixedBoardRedirect />} />
<Route path="status/:cardId" element={<UnprefixedBoardRedirect />} />
<Route path="status-cards" element={<UnprefixedBoardRedirect />} />
<Route path="status-cards/:cardId" element={<UnprefixedBoardRedirect />} />
<Route path="pipelines" element={<UnprefixedBoardRedirect />} />
<Route path="pipelines/:pipelineId" element={<UnprefixedBoardRedirect />} />
<Route path="pipelines/:pipelineId/add" element={<UnprefixedBoardRedirect />} />
<Route path="pipelines/:pipelineId/settings" element={<UnprefixedBoardRedirect />} />
<Route path="pipelines/:pipelineId/items/:caseId" element={<UnprefixedBoardRedirect />} />
<Route path="pipelines/:pipelineId/cases/:caseId" element={<UnprefixedBoardRedirect />} />
<Route path="artifacts" element={<UnprefixedBoardRedirect />} />
<Route path="audit" element={<UnprefixedBoardRedirect />} />
{streamlinedUiEnabled ? (
<>
<Route path="audit/*" element={<UnprefixedBoardRedirect />} />
<Route path="activity" element={<UnprefixedBoardRedirect />} />
<Route path="activity/*" element={<UnprefixedBoardRedirect />} />
<Route path="runs" element={<UnprefixedBoardRedirect />} />
<Route path="costs" element={<UnprefixedBoardRedirect />} />
<Route path="budgets" element={<UnprefixedBoardRedirect />} />
</>
) : null}
<Route path="decisions" element={<UnprefixedBoardRedirect />} />
<Route path="u/:userSlug" element={<UnprefixedBoardRedirect />} />
<Route path="skills/studio" element={<UnprefixedBoardRedirect />} />
<Route path="skills/studio/new" element={<UnprefixedBoardRedirect />} />
<Route path="skills/studio/:skillId" element={<UnprefixedBoardRedirect />} />
<Route path="skills/:skillId/studio" element={<LegacySkillStudioRedirect />} />
<Route path="skills/*" element={<UnprefixedBoardRedirect />} />
<Route path="settings" element={<LegacySettingsRedirect />} />
<Route path="settings/*" element={<LegacySettingsRedirect />} />
<Route path="agents" element={<UnprefixedBoardRedirect />} />
{AGENT_FILTER_TABS.map((tab) => (
<Route key={tab} path={`agents/${tab}`} element={<UnprefixedBoardRedirect />} />
))}
<Route path="agents/new" element={<UnprefixedBoardRedirect />} />
<Route path="agents/:agentId" element={<UnprefixedBoardRedirect />} />
<Route path="agents/:agentId/:tab" element={<UnprefixedBoardRedirect />} />
<Route path="agents/:agentId/runs/:runId" element={<UnprefixedBoardRedirect />} />
<Route path="projects" element={<UnprefixedBoardRedirect />} />
<Route path="projects/:projectId" element={<UnprefixedBoardRedirect />} />
<Route path="projects/:projectId/overview" element={<UnprefixedBoardRedirect />} />
<Route path="projects/:projectId/issues" element={<UnprefixedBoardRedirect />} />
<Route path="projects/:projectId/issues/:filter" element={<UnprefixedBoardRedirect />} />
<Route path="projects/:projectId/workspaces" element={<UnprefixedBoardRedirect />} />
<Route path="projects/:projectId/workspaces/:workspaceId" element={<UnprefixedBoardRedirect />} />
<Route path="projects/:projectId/configuration" element={<UnprefixedBoardRedirect />} />
<Route path="workspaces" element={<UnprefixedBoardRedirect />} />
<Route path="execution-workspaces/:workspaceId" element={<UnprefixedExecutionWorkspaceRedirect />} />
<Route path="execution-workspaces/:workspaceId/services" element={<UnprefixedExecutionWorkspaceRedirect />} />
<Route path="execution-workspaces/:workspaceId/configuration" element={<UnprefixedExecutionWorkspaceRedirect />} />
<Route path="execution-workspaces/:workspaceId/runtime-logs" element={<UnprefixedExecutionWorkspaceRedirect />} />
<Route path="execution-workspaces/:workspaceId/issues" element={<UnprefixedExecutionWorkspaceRedirect />} />
<Route path="execution-workspaces/:workspaceId/routines" element={<UnprefixedExecutionWorkspaceRedirect />} />
<Route path=":companyPrefix" element={streamlinedUiEnabled ? <Layout /> : <ProductionLayout />}>
{boardRoutes(streamlinedUiEnabled, combinedInboxTasksEnabled)}
</Route>
<Route path="*" element={<NotFoundPage scope="global" />} />
</Route>
</Routes>
<OnboardingWizardVariant />
</>
);
}