feat: subagent selection from existing agents + UI cleanup - #23
Conversation
- Subagents can only be added by selecting from existing agents (agent_ref) - Add Description input in General section of agent config form - Remove Tools section from form, subagent editor, and viewer (MCP-only) - Remove Debug toggle from form and viewer - Fix modal overflow: add overflow-x-hidden + min-w-0 on inputs/grid children - Serialize description and agent_ref in YAML, preserve tools/debug silently - Fix AgentsPage is_builtin reference and appConfig mcpApiBaseUrl
The axios response interceptor converted errors into plain Error objects, stripping the .response property. storeApi.isNotFound checked error.response.status === 404, which always returned false on the unwrapped error — so getFile() rethrew on 404 instead of returning null, blocking skill and memory creation (the existence check aborted before PUT). Fix: attach HTTP status to the rejected Error in the interceptor; check error.status in isNotFound (with fallback to error.response.status).
The skill and memory grids use useStoreFilePreviews (query key 'store-file-previews'), but mutations only invalidated 'store-files'. The grids never refetched after create/delete/put, so new items did not appear until a manual page reload.
- Add MCP server registry page with CRUD dialogs and hooks - Add JsonBlock component for chat tool call/result rendering - Fix store-file-previews query invalidation on skill/memory mutations - Fix axios interceptor to preserve HTTP status on unwrapped errors - Fix fileConfigRepository and mcpAxiosInstance tests for mcpApiBaseUrl schema - Re-add MCP 'Add from registry' in agent config form - Format all files with prettier
Kaiohz
left a comment
There was a problem hiding this comment.
Code review — PR #23
Score: 7/10 — solid, well-tested refactor with good TDD discipline, but the PR is doing too much for a single change, the title is misleading, and there are a handful of dead-code/UX gaps worth fixing before merge.
1. PR scope vs. title (high impact)
The title says "subagent selection from existing agents + UI cleanup", but the diff also ships a full MCP Registry page (registry list/create/edit/delete, reveal endpoint, dedicated axios instance, 5 new hooks, new domain port + adapter, ~30+ new files). The first commit even rolls all of that up under a single feat commit (f8e41ed9), before the squash (28d081c6).
Suggestion: split the MCP registry work out into a separate PR. Two reasons:
- A reviewer can only meaningfully review ~300–500 LOC of new product surface at a time; this is ~3000 LOC mixed across two unrelated features.
- If the subagent refactor needs a hotfix, the registry goes with it on a revert.
- The "ci: make SonarQube non-blocking" commit is yet a third concern buried in the same PR.
The current squash + the description: "feat: subagent selection..." title help, but the title should mention the registry until the work is split.
2. SubAgent refactor — good
SubAgentEditorcorrectly drops the inline creation surface (instructions/model/tools/skills editors) and becomes a read-only reference card (name derived fromagent_ref, onlydescriptioneditable). That's the right direction — subagents should resolve at runtime from the referenced agent.AgentConfigForm.addFromExistingAgentcorrectly filters out self-reference (agent.name !== currentAgentName) and de-duplicates already-added refs. Nice touch.Badge"ref:" on the viewer is a good visual cue.- The
indexprop is now dead in the interface — it's still typed but no longer destructured (and no longer used in JSX):Tests still passinterface SubAgentEditorProps { value: SubAgentConfig; onChange: (value: SubAgentConfig) => void; onRemove: () => void; index: number; // ← not destructured, not used }
index={0}harmlessly. Fix: drop it from the interface (and from the tests).
3. MCP Registry page — UX gap
McpRegistryPage.openEdit calls mcpRegistryApi.reveal(server.name) on every click, unconditionally and on every keystroke of nothing (just opening the dialog). For a registry of N servers, the list view masks secrets and reveal returns them in plaintext. A few concerns:
- No prefetch/cache. If a user opens → close → re-open the same server, you re-call
/reveal. Consider keeping arevealCache: Map<name, RegisteredMcpServer>invalidated onuseUpdateMcpServermutation success. - On
revealfailure, you fall back to the maskedserverand open the dialog. The form fields are bound toeditServer(masked) — users will think the field "doesn't show anything" and submit emptyauth_token/ headers, silently wiping the secret on save. This is a data-loss footgun. Consider: don't open the dialog at all on failure, or show a clear "Could not load secrets — try again" state. - The "Add from registry" picker in
McpServersAccordionItem.addFromRegistryalso calls/revealand re-uses the secrets as the new localMcpServerConfig. That means any per-project override of those secrets is silently dropped on the way in (the local copy will track the registry value going forward). Worth a "Detach from registry?" affordance, or at least a docstring.
4. Minor code-quality nits
AgentConfigViewer.tsxremoves the Tools section from the viewer butAgentConfig.toolsis still in the schema and parsed. A user with an old YAML containingtools: [...]will load fine, but the viewer silently hides them. Either droptoolsfrom the schema/interface (matches the "MCP-only now" PR description) or keep showing them with a deprecation note.AgentConfigForm.tsx:151—addFromExistingAgentdoes a linearfindon every add. Trivial at the scale of agents a single human manages, but aMapkeyed by name built once in auseMemowould be cleaner.McpServersAccordionIteminlines a seconduseMcpRegistrycall. It's cheap (React Query dedupes), but co-locating the picker with the registry list page or lifting it to a provider would be more consistent.JsonBlock.tsxswallows clipboard errors silently (/* ignore */). On http:// (non-secure) or denied permission, the user clicks "Copy" and gets zero feedback. At least atoast.erroron failure.agentConfigSchema.ts—description: z.string().nullable().optional()is duplicated 1:1 withsystem_prompt/system_prompt_file. Consider.extend(...)or anullableOptionalStringhelper, the same way you presumably do for the other fields (e.g.instructions,model).appConfig.ts—mcpApiBaseUrlwas added as a build-time required field with.or(z.literal("")).optional().default(""). That's a soft default; consider whether the McpRegistry page should refuse to render when the URL is empty (better UX than letting axios hit""and produce a confusing 404).Continue-on-error: trueon SonarQube is fine pragmatically, but please file a follow-up to bring the scan back to a blocking status once the server is stable. Silently swallowing CI signals accumulates tech debt.
5. Tests
- 26 new unit tests, all passing. Good TDD coverage of: description field,
agent_refplumbing, tools/debug removal, overflow classes. The negative tests (queryByText(/^tools$/i)etc.) are explicit about what should not render — nice. - E2E scenarios 18/18 pass per the description.
- One small test smell:
SubAgentEditor.test.tsxstill passesindex={0}to a component whose interface no longer uses it. If you keepindexin the interface "for future use", leave a comment; otherwise remove it from the props + tests.
TL;DR
- Architecture direction: ✅ The subagent-as-ref model is the right call.
- MCP registry is a great addition but doesn't belong in this PR. Split it out.
- Fix the data-loss footgun in
openEdit(reveal failure → masked fallback → save wipes secret) before merging. - Remove the dead
indexprop. - Either remove
toolsfrom the schema or keep showing it in the viewer.
Verdict: approve-on-condition — split the registry out, fix the reveal-failure UX, and remove the dead index prop. Then ship.
- Add McpRegistryPage route in App.tsx - Add MCP Registry nav link in Sidebar - Revert continue-on-error on SonarQube (was hiding real issues)
Kaiohz
left a comment
There was a problem hiding this comment.
Code review — feat: subagent selection from existing agents + UI cleanup
Clean PR, 88 fichiers / +2982 / -349 / 6 commits, CI ✅, mergeable ✅, 6 commits (squash recommended). Build solide, tests complets (26 nouveaux, 725 au total), architecture hexagonale respectée.
👍 Points forts
- Architecture hexagonale exemplaire sur la nouvelle feature MCP registry :
McpRegistryPort(domain) →mcpRegistryApi(infra) → 5 hooks (application) → composants. Boundaries nettes, faible couplage. - Tests exhaustifs et bien découpés : un fichier de test par couche (entity, schema, hook, API, dialog, page), fixtures dédiées, hoisting propre. Les 26 nouveaux tests ciblent les vrais chemins de code (external/openapi switching, validation, reveal, encodeURI, invalidation query key, baseURL fallback).
- Masked vs revealed : la distinction
auth_token: null(list/get) vsauth_token: "secret"(reveal) est bien documentée dansRegisteredMcpServerJSDoc, et le testsupports a revealed entry where secrets are presentverrouille le contrat. - Axios interceptor fix (
statuspropagation dans l'Error) débloquestoreApi.isNotFoundaprès le unwrap duresponse. Patch minimal et bien testé (axiosInstance.test.ts+storeApi.test.ts). - Modal overflow fix propre :
min-w-0au niveauInput+overflow-x-hiddensur le scroll container. Les nouveaux testsinput.test.tsxet la modification deMcpServerEditormontrent que le fix est appliqué partout. - Subagent picker filtre correctement l'agent courant (anti self-ref) et les subagents déjà ajoutés. Bonne garde UX.
- README mis à jour pour refléter les changements UI (Description, Tools removed, Debug removed, subagent ref-only) — souvent oublié.
- Store-file-previews invalidation propagée à tous les hooks (create/delete/put) corrige un bug de cache stale post-mutation.
- Squash-ready : 6 commits propres, le dernier revert/fix est acceptable mais prêt pour squash.
⚠️ Concerns / suggestions
-
SubAgentEditor—agent_refundefined :id={sub-name-${value.agent_ref}}→ IDsub-name-undefinedsi jamais un subagent legacy sansagent_refarrive (data migration incomplète, ou code appelant qui passe un objet invalide).<Badge>ref: {value.agent_ref}</Badge>rendref: undefineden texte — laid.- Le
SubAgentEditorest désormais un composant purement "ref-mode" mais reste générique sur le type. Considérer un guard en haut du composant :if (!value.agent_ref) return null;(ou throw en dev), ou un type discriminantSubAgentConfig & { mode: "ref" }. Aujourd'hui, leuseFieldArray.append({...EMPTY_SUBAGENT, agent_ref: ...})est la seule voie d'entrée grâce à la PR, mais rien dans le composant ne le documente.
-
addFromRegistry— error swallowing :
DansAgentConfigForm:const addFromRegistry = useCallback( async (name: string) => { const revealed = await mcpRegistryApi.reveal(name); // <-- si reject, l'erreur remonte non capturée mcpServersArray.append(serverConfig); }, ... );
Comparer avec
McpRegistryPage.openEditqui wrappe danstry/catch+toast.error. Unerevealqui échoue (réseau, 500) fait juste crasher silencieusement laSelect.onValueChange. Recommandation :try/catch+toast.error(extractApiMessage(err))pour symétrie. -
mcpAxiosInstance.cachedMcpBaseURL— cache forever :let cachedMcpBaseURL: string | null = null; mcpApiClient.interceptors.request.use(async (config) => { if (!cachedMcpBaseURL) { ... cachedMcpBaseURL = appConfig.mcpApiBaseUrl || ... } config.baseURL = cachedMcpBaseURL; });
Le baseURL est mis en cache une fois pour toute la lifetime de l'app. Acceptable en SPA (config chargée au boot) mais aucun commentaire ne le dit. Soit documenter, soit exposer un
setMcpApiBaseUrl(url)si un hot-reload est envisagé. -
storeApi.isNotFound— double chemin :if ("status" in error && (error as { status?: number }).status === 404) return true; if ("response" in error) { const status = (error as { response?: { status?: number } }).response?.status; return status === 404; }
Les deux branches correspondent à : (a) erreur déjà wrappée par l'interceptor, (b) erreur axios brute (avant passage par l'interceptor — donc depuis un test ou un mock qui bypasse). Le test
returns null when the raw axios error has response.status 404couvre explicitement (b). C'est correct mais fragile : si un futur code appelleapiClient.getdirectement sans passer par l'interceptor (ex. un test), la branche (a) échouera. Une alternative : forcer systématiquement l'interceptor à toujours transformer (un seul format d'erreur). Aujourd'hui, on accepte les deux — c'est documenté implicitement par le commit99539278 fix: store file 404 status lost by axios interceptor unwrapmais pas dans le code lui-même. -
McpServerCard—server.url ?? "—":
LedisplayedUrlestopenapi_url ?? url ?? "—". Si un serveuropenapin'a pasopenapi_url(données corrompues), on tombe surserver.urlqui peut être""(empty string pour openapi), donc l'affichage devient—. C'est OK fonctionnellement, mais le rendu d'un tiret dans une card pour un cas marginal peut induire en erreur. Suggestion : afficher un placeholder explicite "No URL configured" plutôt que—. -
CreateMcpServerDialog—resetFormne reset pasisOpenapiviasourceType:function resetForm() { setSourceType("external"); // OK setName(""); setUrl(""); setOpenapiUrl(""); ... }
Le
setSourceType("external")est correct mais désactivé en mode edit (disabled={isEdit}), donc si l'utilisateur ouvre en mode edit (openapi) puis ferme puis rouvre en create, le state React est-il réinitialisé ? En réalité, le composant n'est pas démonté entre edit→close (leopen={createDialogOpen}est contrôlé par la page parente). Donc le state persiste. Mais leresetFormn'est appelé qu'àhandleClose, donc OK. À vérifier : si l'edit passe de openapi → external → create (nouveau), est-ce quesourceTypereset bien ? En lisant le flow, oui (le state est dans le composant, et l'editServerchange déclenche un useState initial). Mais c'est subtil — un test de régression "switch type in edit, close, reopen in create → defaults to external" serait utile. -
MCP API :
validatene suit pas le pattern CRUD :async validate(input: McpServerInput): Promise<McpServerValidationResult> { const response = await mcpApiClient.post<McpServerValidationResult>(`${BASE}/validate`, input);
L'endpoint est
/api/v1/mcp/servers/validate— un POST sur ce path pourrait entrer en conflit avec/api/v1/mcp/servers/{name}si jamaisname="validate"est créé. Risque faible (les noms sont validés côté UI), mais à garder en tête. Le testposts the input to POST /api/v1/mcp/servers/validatene le couvre pas explicitement (pas de test de régression "POST /api/v1/mcp/servers/validate ne capture pas l'input comme un create avec name='validate'"). -
Pas de test E2E pour le Picker : les tests unitaires de
AgentConfigFormcouvrent "expand Subagents → option appears → click → ref badge shows". Mais pas le scénario "user selects the same agent twice → second select is absent from picker" (filtrage des subagents déjà ajoutés). Le testclicking an existing agent in the picker adds a subagentne re-teste pas après un 1er add. Suggestion : un test "add same agent twice → second attempt is filtered out of options".
💡 Nitpicks
axiosInstance.ts:wrapped.status = error.response.status— TS pourrait se plaindre (Error n'a pas destatuspar défaut). Le castas Error & { status?: number }est OK, mais un petit commentaire "augmented Error with status for downstream consumers" aiderait.mcpApiClient.interceptors.response.usene wrappe pas avecstatusnon plus — incohérence entreapiClientetmcpApiClient: le 1er préservestatus, le 2nd non. Si demain un consumer de MCP veut distinguer 404 vs 500, il devra refaire la même gymnastique.McpServerInput.headersestRecord<string, string>mais l'UI permet de saisir des valeurs vides (vide → clé conservée avec valeur""). Le backend peut le rejeter mais aucun test ne couvre ce cas. Suggestion : trim ou filter les paires vides avant submit.- Le
data-od-id="json-block"surJsonBlockest un test-hook qui pollue un peu le DOM de prod — à confirmer que c'est la convention du projet (les autres composants l'utilisent :mcp-source-type,mcp-server-card-xxx,agent-count...). Si oui, c'est OK.
📊 Score : 8/10
Code de très bonne qualité, PR bien structurée, tests au rendez-vous. Les 8 points ci-dessus sont surtout de la robustesse/defensive programming, pas de bugs bloquants. Squash les 6 commits en 1, merge après avoir tranché sur les concerns #1, #2, #5.
✅ Ready to merge après :
- Décision sur le comportement
SubAgentEditorquandagent_refest absent (concern #1) - Harmonisation du error handling
addFromRegistryavecopenEdit(concern #2) - (Optionnel) Test de régression sur le filtrage anti-doublon du picker (concern #8)
Squash des commits + merge. Beau boulot 👏
The MCP registry page rendered an empty-state button in the middle of the content and had no create affordance in the populated grid, unlike the Agents/Skills/Memories pages. Extract McpServerGrid (mirrors SkillGrid/ MemoryGrid): a responsive grid of server cards followed by a dashed 'New MCP Server' card, also rendered when the registry is empty. The page keeps the top-right 'Add MCP Server' button and delegates loading/ error/grid rendering to the new component.
The Settings font-family change only reached body text because every component uses font-display/font-body/font-mono utilities, which resolved to the hardcoded 'Press Start 2P' tokens and bypassed inheritance. Derive --font-display/body/mono from --app-font-family so all utilities (68 files, sidebar + code + toasts included) follow the selected family. Replace the var(--font-body)/var(--font-mono) select values with concrete stacks to avoid CSS circular references, and migrate persisted legacy values from localStorage. Font size already scaled via --text-* tokens except four hardcoded text-[10px] spots (ToolResultBlock, ToolCallBadge, McpServerCard, SettingsPage) switched to text-xs so they scale with the slider.
MCP servers are now managed in their own registry screen, so the agent form's 'MCP Servers' section no longer needs an 'Add from registry' dropdown, editable server cards, or a custom 'Add MCP Server' button. Replaced with a PillMultiSelect mirroring the Skills/Memories sections: toggle a registry pill on to reveal+embed the full config (schema unchanged), toggle off to remove. Legacy non-registry entries saved in existing agents surface as removable '(custom)' pills so no form data is invisible. Deleted now-dead McpServerEditor.tsx + its test.
Changes
Subagent selection from existing agents
ref:, name read-only, description editableAgent config form cleanup
Modal overflow fix
overflow-x-hiddenon dialog scroll containersmin-w-0on Input base class and grid/flex childrenOther fixes
is_builtinreference (field was removed from metadata)mcpApiBaseUrlto AppConfigSchema (needed for build)Tests