Imported from OpenThrottle/monorepo (
applications/openthrottle-developer/AGENTS.md). Install upstream withnpx skills add OpenThrottle/monorepo --skill openthrottle-developer. Copyright stays with the author.
openthrottle-developer — agent notes
The flagship developer portal (React Router v8 + Vite): plans, tasks, projects, notes,
generators, prompts, pull requests, the in-app IDE, and repo-skills discovery. Talks only to
openthrottle-server over GraphQL. See README.md for the feature tour and
docs/monorepo/openthrottle-developer-vite-devtools.md
for devtools/profiling/troubleshooting.
Consumed by: nothing — deployable app (dev port 6020; env in .env.default).
Commands
pnpm nx run openthrottle-developer:dev— Vite dev server on 6020; needsopenthrottle-serverreachable atAPI_URL_INTERNAL/API_URL_EXTERNAL(default 6021).- Codegen is two divergent passes: this app's
codegen-graphql(from codegen.ts,enumsAsTypes: true→ type-only enums intoapp/__generated__/) and a separatepackages/openthrottle-developer-codegenpass that omits the flag to emit runtime enum objects (e.g.CustomPromptType.Agents, imported byPlansTable/PromptsTable). Read both headers before touching either — do not "consolidate" them.
Layout
app/routes/—flatRoutes(dotted flat filenames, e.g.plans.$planId._index.tsx) with a co-located<route>.tsx.graphqldocument per route. Route modules stay thin.app/routing/<area>/— feature folders (components/,data/,hooks/,utils/,config/) behind the~/*alias.plans/is the largest and the reference decomposition (see gotchas).app/entry.server.tsx+app/global/config/csp.ts— CSP nonce minting and the shared streaming render (see gotchas).app/testing/route-fixtures.tsx— isolated route-UI harness (replaces Storybook); prioritized modules in docs/routing-modules-debug-harness.md.docs/— per-feature design docs (repo-skills discovery, commander/search entrypoints, settings Debug/Logs tabs). Read the matching one before touching that area.
Invariants & gotchas
-
Plans route decomposition is the pattern to follow.
plans.$planId._index.tsxwas a god route (#136); loader/subscription-teardown logic now lives in extracted hooks (app/routing/plans/hooks/usePlanRunConfigEditor,usePlanLifecycleRevalidation) and utils, with presentational pieces likePlanWorkflowConfigHookRowsplit intoapp/routing/plans/components/. Keep new work in that shape; do not grow the route file back up. -
Fresh worktrees fail Vitest at collection until codegen runs — suites import from
app/__generated__/(graphql.ts,gql.ts, and this app's uniquetesting.tsmock factory). Runpnpm nx run openthrottle-developer:codegen-graphql(or the workspace-wide codegen) first; the oldVITEST_FAILURE_BASELINE.mdis obsolete. -
CSP is nonce-based report-only (#143).
entry.server.tsxmints a per-request nonce, calls the sharedbuildCsp(nonce, getCspOptions()), sets the header, and threads the same nonce intoroot.tsxso inline bootstrap scripts match. Only per-app origins and thereportOnlyflag live inapp/global/config/csp.ts; the builder forces report-only outsideNODE_ENV=production, so flippingreportOnlyonly affects prod. Add third-party origins via theadditional*Srcarrays, never by loosening the shared builder. -
Skills route reads the filesystem, not a TS registry.
/skillsand the agents area discover skills from.agents/skills(the SSOT view;.claude/skillsfan-out deduped in) at request time viaapp/routing/agents/data/resolve-monorepo-root.server.ts. Root resolution:WORKSPACE_ROOTenv (same var as the server — never inventMONOREPO_ROOT), else walk up ≤12 levels for a dir with bothnx.jsonandpnpm-workspace.yaml, elsenull(empty list, no throw — deployed builds without a checkout). Design: docs/repo-skills-discovery-design.md. -
Voice input (#142): push-to-talk on the home commander via
app/routing/home/hooks/useVoiceInput.tsxuseTranscriptionStream.tsx, streaming to a local WhisperLive instance — a dev-only external dependency; the feature degrades when it is unreachable.
-
tests/setup.tskeeps app-local jsdom shims (no-op WebGL2 context + avisualViewportstand-in for@paper-design/shaderson the auth screen); these stay here, not in the shared testing package. -
Prompts: routes live at
/prompts/*(legacy/custom-prompts/*redirect), but GraphQL still uses thecustomPrompt/customPromptsfields — don't rename the operations to match the URLs. -
Agent-CLI setup (
/settings/agents): read-only status of the agent CLIs (installed/version fromdiscoverAgentClis) always renders; the Install/Update controls are gated by the server-computedagentCliSetupConfig { canManage, installEnabled }— the client has no permission data, so the loader must fetch those booleans and pass them down (never infer from client identity). Runs stream overagentSetupChunkAddedviauseAgentSetupStream. The feature is default-off on the server (OT_AGENT_CLI_INSTALL_ENABLED) — a local-dev tool, not for hosted deploys. CLI auth/login is out of scope. -
Route actions validate
FormDatathrough the generated Zod schema, not hand-rolled field reads. UseparseFormDatafrom@openthrottle/react-router-graphql+ a~/__generated__/schemasinput schema (.omit()route-param ids, then assemble). See packages/graphql-codegen/README.md. Don't add newformData.get+typeof === 'string'+.trim()+ empty-check helpers. -
Surface
parsed.errordirectly — don't hard-code per-field copy.parseFormDatahumanizes messages centrally ("Tag is required.", "Status must be one of: …"), soreturn { error: parsed.error }is the default. Only reach foroptions.labels(swap a field's label, e.g.{ gitUrl: 'Git repository URL' }) oroptions.messages(replace the whole message) when the humanized default reads poorly; a schema's own.refine/.min(N,'msg')copy is preserved untouched. Keep genuinely bespoke guidance (route-param guards, the plan-run git-branch hint) as-is.
Page titles
- Format:
<Leaf> | <Section> | OpenThrottle. Leaf-first — it matcheshandle.breadcrumborder and keeps the distinguishing word visible in a narrow tab. Top-level pages omit the section (Plans | OpenThrottle). - At most one section segment — the nearest ancestor group that has its own index route.
Nested settings detail pages use the sub-group, not
Settings:Repositories | Settings, but${repo.name} | Repositoriesand${flag.key} | Rollout. - Sections in use:
Settings,Skills,Queues,Generators,Personas,IDE,Calendar,Repositories,Rollout. Groups whose leaves already read unambiguously on their own (/legal/*,/notes/*,/docs/*) take no section segment. - Sentence case (
Create prompt, notCreate Prompt). Preserved verbatim:OpenThrottle,MCP,IDE,FAQ,PR,API, and proper nouns coming from data. - Detail routes prefer the entity name from loader data, guarded (
args.loaderData?.plan?.title) because loader data is undefined during error renders, and fall back to a generic singular label with noDetailssuffix —Plan | Plans, neverPlan Details. - Every user-visible route sets a title; resource routes deliberately do not (
resources.*,robots[.]txt,auth.logout,auth.ws-token,skills.autocomplete). - Where a route builds its title from a
*_COPY.pageTitleconstant, fix the constant in itsdata.copy.ts— the on-page heading shares it, and moving both together is intended. - Enforced by
app/routes/__tests__/route-titles.test.ts.
Don't
- Don't hand-edit
app/__generated__/*— regenerate via codegen. - Don't add a
buildtarget debate here:dev/build/start/typecheckare the plugin-inferred targets (root/area docs); the inferredtypecheckis renamed__NOT_USED__typecheckand the real one comes from the local nx plugin.
Pointers
- README.md — feature tour, Skills page requirements, Prompts UI.
- docs/ — repo-skills discovery, commander/search entrypoints, settings tab specs.
- tests/e2e/README.md — canonical Maestro-against-prod-build write-up.