@polymorphism-tech/morph-spec 8.38.0 → 8.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/CLAUDE.md +20 -9
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +60 -43
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +4 -3
  8. package/framework/MORPH.md +10 -9
  9. package/framework/agents.json +43 -26
  10. package/framework/docs/CLI.md +7 -5
  11. package/framework/evals/rubrics/code-quality.md +4 -4
  12. package/framework/evals/rubrics/condensed.md +2 -2
  13. package/framework/hooks/README.md +1 -1
  14. package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
  15. package/framework/hooks/claude-code/core/worktree-create.js +22 -6
  16. package/framework/hooks/shared/git-paths.js +38 -5
  17. package/framework/mcp/registry.json +58 -66
  18. package/framework/rules/infrastructure-standards.md +0 -1
  19. package/framework/rules/morph-workflow.md +9 -4
  20. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  21. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  22. package/framework/skills/core/morph-apply/SKILL.md +59 -25
  23. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  24. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  25. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  26. package/framework/skills/core/morph-implement/SKILL.md +48 -24
  27. package/framework/skills/core/morph-media/SKILL.md +2 -2
  28. package/framework/skills/core/morph-plan/SKILL.md +10 -3
  29. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  30. package/framework/skills/core/morph-review/SKILL.md +1 -1
  31. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  32. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  33. package/framework/skills/core/morph-uiux/SKILL.md +7 -6
  34. package/framework/standards/PROVENANCE.json +4 -4
  35. package/framework/standards/STANDARDS.json +9 -9
  36. package/framework/standards/ai-agents/context-providers.md +16 -26
  37. package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
  38. package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
  39. package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
  40. package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
  41. package/framework/standards/ai-agents/modalities-vision.md +1 -18
  42. package/framework/standards/ai-agents/prompt-sources.md +0 -4
  43. package/framework/standards/architecture/code-comments.md +52 -16
  44. package/framework/standards/backend/dotnet/testing.md +4 -2
  45. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
  46. package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
  47. package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
  48. package/framework/standards/frontend/nextjs/testing.md +4 -2
  49. package/framework/standards/integration/mcp/mcp-tools.md +64 -126
  50. package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
  51. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
  52. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
  53. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
  54. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
  55. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
  56. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
  57. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
  58. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
  59. package/framework/templates/code/dotnet/database/migration.cs +49 -83
  60. package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
  61. package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
  62. package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
  63. package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
  64. package/framework/templates/code/dotnet/jobs/job.cs +2 -97
  65. package/framework/templates/code/dotnet/test.cs +2 -87
  66. package/framework/templates/code/typescript/contracts.ts +140 -167
  67. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
  68. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
  69. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
  70. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
  71. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
  72. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
  73. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
  74. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
  75. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
  76. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
  77. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
  78. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
  79. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
  80. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
  81. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
  82. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
  83. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
  84. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
  85. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
  86. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
  87. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
  88. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
  89. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
  90. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
  91. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
  92. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
  93. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
  94. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
  95. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
  96. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
  97. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
  98. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
  99. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
  100. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
  101. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
  102. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
  103. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
  110. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
  111. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
  112. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
  113. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
  114. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
  115. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
  116. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
  117. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
  118. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
  119. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
  120. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
  121. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
  122. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
  123. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
  124. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
  127. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
  128. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
  129. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
  130. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
  131. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
  132. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
  134. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
  135. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
  136. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
  137. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
  138. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
  139. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
  140. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
  141. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
  142. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
  143. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
  144. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
  145. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
  146. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
  147. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
  148. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
  149. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
  150. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
  151. package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
  152. package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
  153. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
  154. package/framework/templates/integrations/asaas-client.cs +332 -387
  155. package/framework/templates/integrations/asaas-webhook.cs +290 -351
  156. package/framework/templates/integrations/neon-auth-config.cs +9 -105
  157. package/framework/templates/project/validate.js +97 -120
  158. package/package.json +2 -2
  159. package/src/commands/dashboard/dashboard.js +80 -53
  160. package/src/commands/mcp/mcp-setup.js +37 -9
  161. package/src/commands/project/doctor.js +12 -6
  162. package/src/commands/project/init.js +6 -3
  163. package/src/commands/project/install-plugin-cmd.js +1 -1
  164. package/src/commands/project/update.js +5 -0
  165. package/src/commands/project/worktree.js +60 -4
  166. package/src/commands/state/gate-check.js +5 -0
  167. package/src/commands/tasks/dag.js +27 -4
  168. package/src/commands/validation/verify.js +22 -38
  169. package/src/dashboard/app/App.jsx +80 -16
  170. package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
  171. package/src/dashboard/app/components/CommandPalette.jsx +155 -0
  172. package/src/dashboard/app/components/DetailPanel.jsx +47 -11
  173. package/src/dashboard/app/components/EntityLink.jsx +55 -0
  174. package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
  175. package/src/dashboard/app/components/FlowGraph.jsx +19 -17
  176. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
  177. package/src/dashboard/app/components/Markdown.jsx +47 -0
  178. package/src/dashboard/app/components/PhaseRail.jsx +111 -0
  179. package/src/dashboard/app/components/SearchFilter.jsx +40 -22
  180. package/src/dashboard/app/components/Sidebar.jsx +146 -37
  181. package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
  182. package/src/dashboard/app/hooks/usePref.js +38 -0
  183. package/src/dashboard/app/lib/format.js +69 -0
  184. package/src/dashboard/app/lib/phases.js +56 -0
  185. package/src/dashboard/app/lib/project.js +50 -0
  186. package/src/dashboard/app/pages/Activity.jsx +24 -0
  187. package/src/dashboard/app/pages/AgentMap.jsx +371 -370
  188. package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
  189. package/src/dashboard/app/pages/Features.jsx +400 -0
  190. package/src/dashboard/app/pages/Guide.jsx +146 -0
  191. package/src/dashboard/app/pages/HookInspector.jsx +357 -322
  192. package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
  193. package/src/dashboard/app/pages/Portfolio.jsx +156 -0
  194. package/src/dashboard/app/pages/Rubrics.jsx +101 -0
  195. package/src/dashboard/app/pages/RulesMap.jsx +300 -273
  196. package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
  197. package/src/dashboard/app/pages/Standards.jsx +407 -332
  198. package/src/dashboard/app/styles/index.css +187 -47
  199. package/src/dashboard/index.html +14 -12
  200. package/src/dashboard/package.json +32 -24
  201. package/src/dashboard/scanner/activity-scanner.js +351 -0
  202. package/src/dashboard/scanner/agents-scanner.js +59 -1
  203. package/src/dashboard/scanner/docs-scanner.js +59 -0
  204. package/src/dashboard/scanner/feature-status.js +10 -0
  205. package/src/dashboard/scanner/features-scanner.js +360 -0
  206. package/src/dashboard/scanner/index.js +14 -7
  207. package/src/dashboard/scanner/phases-scanner.js +14 -15
  208. package/src/dashboard/scanner/portfolio.js +153 -0
  209. package/src/dashboard/scanner/project-api.js +195 -0
  210. package/src/dashboard/scanner/rules-scanner.js +42 -37
  211. package/src/dashboard/scanner/skills-scanner.js +105 -43
  212. package/src/dashboard/scanner/standards-scanner.js +2 -0
  213. package/src/dashboard/vite-plugin-morph-data.js +134 -17
  214. package/src/lib/detectors/claude-config-detector.js +2 -7
  215. package/src/lib/detectors/index.js +5 -1
  216. package/src/lib/evaluator-report.js +22 -0
  217. package/src/lib/generators/settings-generator.js +208 -209
  218. package/src/lib/installers/mcp-installer.js +724 -719
  219. package/src/lib/installers/memory-installer.js +1 -2
  220. package/src/lib/projects/registry.js +154 -0
  221. package/src/lib/risk-detector.js +13 -0
  222. package/src/lib/tasks/tasks-json-parser.js +21 -0
  223. package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
  224. package/src/lib/worktree/env-allocator.js +9 -3
  225. package/src/scripts/install-plugin.js +1 -1
  226. package/src/scripts/setup-infra.js +2 -4
  227. package/src/utils/agents-installer.js +108 -13
  228. package/src/utils/banner.js +1 -1
  229. package/src/commands/index.js +0 -13
  230. package/src/commands/project/checkpoint.js +0 -209
  231. package/src/core/state/phase-state-machine.js +0 -197
  232. package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
  233. package/src/dashboard/app/pages/LiveState.jsx +0 -206
  234. package/src/dashboard/scanner/squads-scanner.js +0 -13
  235. package/src/dashboard/scanner/state-scanner.js +0 -85
  236. package/src/lib/generators/index.js +0 -10
  237. package/src/lib/hooks/hook-executor.js +0 -167
  238. package/src/lib/validators/checkpoint-persistence.js +0 -39
  239. package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
  240. package/src/lib/validators/shared/index.js +0 -12
  241. package/src/lib/validators/shared/issue-counter.js +0 -14
  242. package/src/lib/validators/shared/result-formatter.js +0 -124
@@ -0,0 +1,172 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import {
4
+ ChatCircleText, Question, BookOpenText, PaperPlaneTilt, ArrowBendDownLeft, CheckSquare, ShieldCheck, Flag, WarningOctagon,
5
+ PencilSimpleLine, Eye, TerminalWindow, Wrench,
6
+ } from '@phosphor-icons/react';
7
+ import { useMorphQuery } from '../hooks/useMorphQuery.js';
8
+ import EntityLink from './EntityLink.jsx';
9
+ import { apiBase, featureHref } from '../lib/project.js';
10
+ import { clock, dayLabel } from '../lib/format.js';
11
+ import { phaseMeta } from '../lib/phases.js';
12
+
13
+ export const KIND_META = {
14
+ prompt: { label: 'Você', icon: ChatCircleText },
15
+ question: { label: 'Perguntas', icon: Question },
16
+ skill: { label: 'Skills', icon: BookOpenText },
17
+ dispatch: { label: 'Despachos', icon: PaperPlaneTilt },
18
+ handback: { label: 'Retornos', icon: ArrowBendDownLeft },
19
+ task: { label: 'Tasks', icon: CheckSquare },
20
+ verify: { label: 'Verify', icon: ShieldCheck },
21
+ gate: { label: 'Gates', icon: Flag },
22
+ failure: { label: 'Falhas', icon: WarningOctagon },
23
+ write: { label: 'Escritas', icon: PencilSimpleLine },
24
+ read: { label: 'Leituras', icon: Eye },
25
+ command: { label: 'Comandos', icon: TerminalWindow },
26
+ tool: { label: 'Outras tools', icon: Wrench },
27
+ };
28
+ const DEFAULT = ['prompt', 'question', 'skill', 'dispatch', 'handback', 'task', 'verify', 'gate', 'failure'];
29
+
30
+ const NODE_TONE = { pass: 'text-[var(--color-ok)]', fail: 'text-[var(--color-bad)]', skip: 'text-[var(--color-faint)]' };
31
+
32
+ function Line({ item, showFeature, project }) {
33
+ const Icon = KIND_META[item.kind]?.icon || Wrench;
34
+ const tone = item.status === 'fail' ? 'text-[var(--color-bad)]' : item.status === 'ok' ? 'text-[var(--color-ok)]' : 'text-[var(--color-dim)]';
35
+ return (
36
+ <li className="grid grid-cols-[3rem_1.25rem_minmax(0,1fr)] gap-3 py-2.5">
37
+ <time className="pt-px font-mono text-xs tabular-nums text-[var(--color-faint)]" dateTime={item.ts}>{clock(item.ts)}</time>
38
+ <Icon weight="light" size={17} className={tone} aria-hidden="true" />
39
+ <div className="min-w-0">
40
+ <p className="text-sm text-[var(--color-fg)]">
41
+ {item.refs?.persona ? (
42
+ <>{item.title.replace(`morph-${item.refs.persona}`, '')}<EntityLink kind="persona" id={item.refs.persona}>morph-{item.refs.persona}</EntityLink></>
43
+ ) : item.refs?.skill ? (
44
+ <>Carregou a skill <EntityLink kind="skill" id={item.refs.skill} /></>
45
+ ) : item.title}
46
+ {showFeature && item.feature && (
47
+ <Link to={featureHref(project, item.feature)} className="ml-2 text-xs text-[var(--color-faint)] hover:text-[var(--color-dim)]">{item.feature}</Link>
48
+ )}
49
+ {item.phase && (
50
+ <span className="ml-2 text-xs" style={{ color: phaseMeta(item.phase).color }}>{phaseMeta(item.phase).label.toLowerCase()}</span>
51
+ )}
52
+ {item.agent && item.kind !== 'handback' && (
53
+ <span className="ml-2 text-xs text-[var(--color-faint)]">por {item.agent}</span>
54
+ )}
55
+ </p>
56
+ {item.detail && <p className="mt-0.5 line-clamp-2 break-words text-sm text-[var(--color-dim)]">{item.detail}</p>}
57
+ {item.nodes?.length > 0 && (
58
+ <p className="mt-1 flex flex-wrap gap-x-3 gap-y-1 font-mono text-xs">
59
+ {item.nodes.map((n) => (
60
+ <span key={n.name} className={NODE_TONE[n.status] || 'text-[var(--color-dim)]'}>{n.name} {n.status}</span>
61
+ ))}
62
+ </p>
63
+ )}
64
+ </div>
65
+ </li>
66
+ );
67
+ }
68
+
69
+ /** Group consecutive items by calendar day, newest day first. */
70
+ function byDay(items) {
71
+ const groups = [];
72
+ for (const it of items) {
73
+ const key = (it.ts || '').slice(0, 10);
74
+ const last = groups[groups.length - 1];
75
+ if (last && last.key === key) last.items.push(it);
76
+ else groups.push({ key, ts: it.ts, items: [it] });
77
+ }
78
+ return groups;
79
+ }
80
+
81
+ export function FeedView({ items, kinds, onToggleKind, showFeature, onMore, hasMore, total, project }) {
82
+ return (
83
+ <div>
84
+ <div className="mb-5 flex flex-wrap gap-2" role="group" aria-label="Tipos de evento">
85
+ {Object.entries(KIND_META).map(([kind, meta]) => {
86
+ const on = kinds.includes(kind);
87
+ return (
88
+ <button
89
+ key={kind}
90
+ type="button"
91
+ aria-pressed={on}
92
+ onClick={() => onToggleKind(kind)}
93
+ className={[
94
+ 'fluid rounded-full px-3 py-1 text-xs transition-colors active:scale-[0.98]',
95
+ on ? 'bg-white/[0.09] text-[var(--color-fg)] shadow-[inset_0_0_0_1px_var(--color-hair-strong)]' : 'text-[var(--color-faint)] shadow-[inset_0_0_0_1px_var(--color-hair)] hover:text-[var(--color-dim)]',
96
+ ].join(' ')}
97
+ >
98
+ {meta.label}
99
+ </button>
100
+ );
101
+ })}
102
+ </div>
103
+ {items.length === 0 && (
104
+ <p className="text-sm text-[var(--color-dim)]">
105
+ Nada registrado com esses filtros. A telemetria vem de <code className="font-mono">.morph/logs/events.jsonl</code>, gravada pelos hooks enquanto o Claude trabalha.
106
+ </p>
107
+ )}
108
+ {byDay(items).map((g) => (
109
+ <section key={g.key} className="mb-6">
110
+ <h3 className="sticky top-0 z-10 -mx-1 bg-[var(--color-core)] px-1 py-1.5 text-xs font-medium text-[var(--color-dim)] first-letter:uppercase">{dayLabel(g.ts)}</h3>
111
+ <ul className="divide-y divide-[var(--color-hair)]">
112
+ {g.items.map((it) => <Line key={it.id} item={it} showFeature={showFeature} project={project} />)}
113
+ </ul>
114
+ </section>
115
+ ))}
116
+ {hasMore && (
117
+ <button
118
+ type="button"
119
+ onClick={onMore}
120
+ className="fluid rounded-full px-4 py-2 text-sm text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)] transition-colors hover:text-[var(--color-fg)] active:scale-[0.98]"
121
+ >
122
+ Carregar anteriores
123
+ </button>
124
+ )}
125
+ {typeof total === 'number' && items.length > 0 && (
126
+ <p className="mt-3 text-xs text-[var(--color-faint)]">{items.length} de {total} eventos</p>
127
+ )}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ /**
133
+ * Live activity feed. The first page follows the server's `morph:changed`
134
+ * pushes; older pages are fetched on demand and appended.
135
+ */
136
+ export default function ActivityFeed({ project, feature, showFeature = false }) {
137
+ const [kinds, setKinds] = useState(DEFAULT);
138
+ const [older, setOlder] = useState([]);
139
+ const [cursor, setCursor] = useState(null);
140
+ const params = new URLSearchParams({ kinds: kinds.join(','), limit: '80' });
141
+ if (feature) params.set('feature', feature);
142
+ const { data, error } = useMorphQuery(`${apiBase(project)}/activity?${params}`, { scope: 'activity', project });
143
+
144
+ useEffect(() => { setOlder([]); setCursor(null); }, [feature, kinds.join(',')]);
145
+
146
+ const next = cursor === null ? data?.next : cursor;
147
+ const loadMore = async () => {
148
+ if (!next) return;
149
+ const p = new URLSearchParams(params);
150
+ p.set('before', next);
151
+ const res = await fetch(`${apiBase(project)}/activity?${p}`);
152
+ const page = await res.json();
153
+ setOlder((o) => [...o, ...page.items]);
154
+ setCursor(page.next || '');
155
+ };
156
+
157
+ if (error) return <p className="text-sm text-[var(--color-bad)]">Não consegui ler a atividade: {error}</p>;
158
+ if (!data) return <p className="text-sm text-[var(--color-faint)]">Lendo a telemetria…</p>;
159
+ const toggle = (k) => setKinds((ks) => (ks.includes(k) ? ks.filter((x) => x !== k) : [...ks, k]));
160
+ return (
161
+ <FeedView
162
+ items={[...data.items, ...older]}
163
+ kinds={kinds}
164
+ onToggleKind={toggle}
165
+ showFeature={showFeature}
166
+ onMore={loadMore}
167
+ hasMore={Boolean(next)}
168
+ total={data.total}
169
+ project={project}
170
+ />
171
+ );
172
+ }
@@ -0,0 +1,155 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import morphData from 'virtual:morph-data';
4
+ import { MagnifyingGlass } from '@phosphor-icons/react';
5
+ import { entityHref } from './EntityLink.jsx';
6
+ import { tocOf } from '../pages/Guide.jsx';
7
+ import { useMorphQuery } from '../hooks/useMorphQuery.js';
8
+ import { apiBase, projectHref, featureHref } from '../lib/project.js';
9
+
10
+ const KIND_LABEL = {
11
+ page: 'Página', project: 'Projeto', feature: 'Feature', task: 'Task', skill: 'Skill', persona: 'Persona',
12
+ standard: 'Standard', rule: 'Rule', hook: 'Hook', guide: 'Guia', rubric: 'Rubrica',
13
+ };
14
+
15
+ const PAGES = [
16
+ ['Portfólio', '/'], ['Guia', '/guide'], ['Fases', '/phases'], ['Skills', '/skills'],
17
+ ['Personas', '/agents'], ['Standards', '/standards'], ['Rules', '/rules'], ['Hooks', '/hooks'], ['Rubricas', '/rubrics'],
18
+ ];
19
+
20
+ /** Everything the framework knows that has a page — built once. */
21
+ function knowledgeIndex() {
22
+ const out = PAGES.map(([title, to]) => ({ kind: 'page', title, to, hay: title }));
23
+ for (const s of morphData.skills || []) out.push({ kind: 'skill', title: s.id, hint: s.description, to: entityHref('skill', s.id), hay: `${s.id} ${s.description}` });
24
+ for (const a of morphData.agents?.list || []) out.push({ kind: 'persona', title: `morph-${a.id}`, hint: a.title, to: entityHref('persona', a.id), hay: `${a.id} ${a.title} ${(a.keywords || []).join(' ')}` });
25
+ for (const s of morphData.standards?.list || []) out.push({ kind: 'standard', title: s.name, hint: s.id, to: entityHref('standard', s.id), hay: `${s.name} ${s.id} ${(s.aliases || []).join(' ')} ${(s.tags || []).join(' ')}` });
26
+ for (const r of morphData.rules || []) out.push({ kind: 'rule', title: r.name, hint: (r.paths || []).join(', '), to: entityHref('rule', r.name), hay: `${r.name} ${(r.paths || []).join(' ')}` });
27
+ const hooks = new Set();
28
+ for (const h of morphData.hooks || []) {
29
+ if (hooks.has(h.name)) continue;
30
+ hooks.add(h.name);
31
+ out.push({ kind: 'hook', title: h.name, hint: h.description, to: entityHref('hook', h.name), hay: `${h.name} ${h.description || ''}` });
32
+ }
33
+ for (const d of morphData.docs?.guide || []) {
34
+ for (const h of tocOf(d).filter((x) => x.depth >= 2)) {
35
+ out.push({ kind: 'guide', title: h.text, hint: d.title, to: `/guide/${d.id}#${h.slug}`, hay: `${h.text} ${d.title}` });
36
+ }
37
+ }
38
+ for (const r of morphData.docs?.rubrics || []) out.push({ kind: 'rubric', title: r.title, to: `/rubrics?sel=${encodeURIComponent(`dim:${r.id}`)}`, hay: r.title });
39
+ return out;
40
+ }
41
+
42
+ function projectIndex(portfolio) {
43
+ return (portfolio?.projects || []).map((p) => ({ kind: 'project', title: p.name, hint: p.dir, to: projectHref(p.id), hay: `${p.name} ${p.dir}` }));
44
+ }
45
+
46
+ /**
47
+ * Features and their tasks. `projectName` is set for another project's
48
+ * features, so a hit says where it lives.
49
+ */
50
+ function featureIndex(project, features, projectName = null) {
51
+ const out = [];
52
+ const where = (text) => (projectName ? (text ? `${projectName}: ${text}` : projectName) : text);
53
+ for (const f of features || []) {
54
+ out.push({ kind: 'feature', title: f.name, hint: where(f.description), to: featureHref(project, f.name), hay: `${f.name} ${f.description || ''} ${projectName || ''}` });
55
+ for (const t of f.taskIndex || []) {
56
+ out.push({ kind: 'task', title: `${t.id} ${t.title}`, hint: where(f.name), to: featureHref(project, f.name, '?tab=tasks'), hay: `${t.id} ${t.title} ${f.name} ${projectName || ''}` });
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /** The active features of the other projects, from the portfolio's summaries. */
63
+ function otherProjectsIndex(portfolio, project) {
64
+ return (portfolio?.projects || [])
65
+ .filter((p) => p.id !== project && p.summary)
66
+ .flatMap((p) => featureIndex(p.id, p.summary.activeFeatures, p.name));
67
+ }
68
+
69
+ /** Every query word must appear; a title that starts with the query ranks first. */
70
+ export function searchIndex(index, query, limit = 40) {
71
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
72
+ if (!words.length) return [];
73
+ const scored = [];
74
+ for (const item of index) {
75
+ const hay = item.hay.toLowerCase();
76
+ if (!words.every((w) => hay.includes(w))) continue;
77
+ const title = item.title.toLowerCase();
78
+ const score = (title.startsWith(words[0]) ? 0 : title.includes(words[0]) ? 1 : 2) + (item.kind === 'page' ? -1 : 0);
79
+ scored.push({ item, score });
80
+ }
81
+ return scored.sort((a, b) => a.score - b.score || a.item.title.length - b.item.title.length).slice(0, limit).map((s) => s.item);
82
+ }
83
+
84
+ export default function CommandPalette({ open, onClose, portfolio, project }) {
85
+ const navigate = useNavigate();
86
+ const [query, setQuery] = useState('');
87
+ const [active, setActive] = useState(0);
88
+ const inputRef = useRef(null);
89
+ const knowledge = useMemo(knowledgeIndex, []);
90
+ const { data } = useMorphQuery(open && project ? `${apiBase(project)}/features` : null, { project });
91
+ const index = useMemo(
92
+ () => [...featureIndex(project, data?.features), ...otherProjectsIndex(portfolio, project), ...projectIndex(portfolio), ...knowledge],
93
+ [data, knowledge, portfolio, project],
94
+ );
95
+ const results = useMemo(() => searchIndex(index, query), [index, query]);
96
+
97
+ useEffect(() => {
98
+ if (open) { setQuery(''); setActive(0); setTimeout(() => inputRef.current?.focus(), 0); }
99
+ }, [open]);
100
+ useEffect(() => { setActive(0); }, [query]);
101
+
102
+ if (!open) return null;
103
+ const go = (item) => { onClose(); navigate(item.to); };
104
+ const onKey = (e) => {
105
+ if (e.key === 'Escape') onClose();
106
+ else if (e.key === 'ArrowDown') { e.preventDefault(); setActive((i) => Math.min(i + 1, results.length - 1)); }
107
+ else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((i) => Math.max(i - 1, 0)); }
108
+ else if (e.key === 'Enter' && results[active]) go(results[active]);
109
+ };
110
+
111
+ return (
112
+ <div className="fixed inset-0 z-50 flex items-start justify-center bg-black/60 px-4 pt-[12vh] backdrop-blur-sm" onMouseDown={onClose} role="presentation">
113
+ <div role="dialog" aria-modal="true" aria-label="Busca" className="bezel w-full max-w-2xl" onMouseDown={(e) => e.stopPropagation()}>
114
+ <div className="bezel-core overflow-hidden">
115
+ <label className="flex items-center gap-3 border-b border-[var(--color-hair)] px-5 py-4">
116
+ <MagnifyingGlass weight="light" size={18} className="text-[var(--color-faint)]" />
117
+ <span className="sr-only">Buscar em tudo</span>
118
+ <input
119
+ ref={inputRef}
120
+ value={query}
121
+ onChange={(e) => setQuery(e.target.value)}
122
+ onKeyDown={onKey}
123
+ placeholder="Buscar features, tasks, skills, personas, standards, seções do guia…"
124
+ className="w-full bg-transparent text-base text-[var(--color-fg)] outline-none focus-visible:outline-none placeholder:text-[var(--color-faint)]"
125
+ aria-controls="palette-results"
126
+ aria-activedescendant={results[active] ? `palette-${active}` : undefined}
127
+ />
128
+ <kbd className="rounded-md px-1.5 font-mono text-[11px] text-[var(--color-faint)] shadow-[inset_0_0_0_1px_var(--color-hair)]">Esc</kbd>
129
+ </label>
130
+ <ul id="palette-results" role="listbox" className="max-h-[55vh] overflow-y-auto p-2">
131
+ {query && results.length === 0 && <li className="px-3 py-6 text-center text-sm text-[var(--color-dim)]">Nada encontrado para “{query}”.</li>}
132
+ {!query && <li className="px-3 py-6 text-center text-sm text-[var(--color-faint)]">Digite para buscar. Setas navegam, Enter abre.</li>}
133
+ {results.map((r, i) => (
134
+ <li
135
+ key={`${r.kind}:${r.to}:${r.title}`}
136
+ id={`palette-${i}`}
137
+ role="option"
138
+ aria-selected={i === active}
139
+ onMouseEnter={() => setActive(i)}
140
+ onClick={() => go(r)}
141
+ className={['flex cursor-pointer items-baseline gap-3 rounded-lg px-3 py-2', i === active ? 'bg-white/[0.07]' : ''].join(' ')}
142
+ >
143
+ <span className="w-16 shrink-0 text-xs text-[var(--color-faint)]">{KIND_LABEL[r.kind]}</span>
144
+ <span className="min-w-0 flex-1">
145
+ <span className="block truncate text-sm text-[var(--color-fg)]">{r.title}</span>
146
+ {r.hint && <span className="block truncate text-xs text-[var(--color-dim)]">{r.hint}</span>}
147
+ </span>
148
+ </li>
149
+ ))}
150
+ </ul>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ );
155
+ }
@@ -1,11 +1,47 @@
1
- export default function DetailPanel({ open, title, onClose, children }) {
2
- return (
3
- <div className={`fixed top-0 right-0 h-full w-96 bg-gray-900 border-l border-gray-700 shadow-2xl transform transition-transform duration-200 ease-in-out z-50 ${open ? 'translate-x-0' : 'translate-x-full'}`}>
4
- <div className="flex items-center justify-between p-4 border-b border-gray-700">
5
- <h2 className="text-white font-semibold text-lg truncate">{title}</h2>
6
- <button onClick={onClose} className="text-gray-400 hover:text-white text-xl leading-none">×</button>
7
- </div>
8
- <div className="p-4 overflow-y-auto h-[calc(100%-57px)]">{children}</div>
9
- </div>
10
- );
11
- }
1
+ import { useEffect } from 'react';
2
+ import { X } from '@phosphor-icons/react';
3
+
4
+ /**
5
+ * The reading drawer of the knowledge pages. Wide enough for rendered markdown,
6
+ * closes on Escape, and sits over the graph without a backdrop so the graph
7
+ * stays visible beside it.
8
+ */
9
+ export default function DetailPanel({ open, title, subtitle, onClose, children }) {
10
+ useEffect(() => {
11
+ if (!open) return undefined;
12
+ const onKey = (e) => { if (e.key === 'Escape') onClose?.(); };
13
+ window.addEventListener('keydown', onKey);
14
+ return () => window.removeEventListener('keydown', onKey);
15
+ }, [open, onClose]);
16
+
17
+ return (
18
+ <aside
19
+ aria-hidden={!open}
20
+ aria-label={title}
21
+ className={[
22
+ 'fluid fixed inset-y-0 right-0 z-40 flex w-full max-w-[44rem] flex-col p-3 transition-transform',
23
+ open ? 'translate-x-0' : 'pointer-events-none translate-x-full',
24
+ ].join(' ')}
25
+ >
26
+ <div className="bezel flex min-h-0 flex-1 flex-col bg-[var(--color-ink)]/80 backdrop-blur-xl">
27
+ <div className="bezel-core flex min-h-0 flex-1 flex-col">
28
+ <header className="flex items-start justify-between gap-4 border-b border-[var(--color-hair)] px-6 py-4">
29
+ <div className="min-w-0">
30
+ <h2 className="truncate text-lg font-semibold tracking-tight">{title}</h2>
31
+ {subtitle && <p className="mt-0.5 truncate text-sm text-[var(--color-dim)]">{subtitle}</p>}
32
+ </div>
33
+ <button
34
+ type="button"
35
+ onClick={onClose}
36
+ aria-label="Fechar painel"
37
+ className="fluid flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white/[0.05] text-[var(--color-dim)] transition-colors hover:bg-white/[0.1] hover:text-[var(--color-fg)]"
38
+ >
39
+ <X weight="light" size={16} />
40
+ </button>
41
+ </header>
42
+ <div className="min-h-0 flex-1 overflow-y-auto px-6 py-5">{children}</div>
43
+ </div>
44
+ </div>
45
+ </aside>
46
+ );
47
+ }
@@ -0,0 +1,55 @@
1
+ import { Link, useSearchParams } from 'react-router-dom';
2
+ import { useCallback } from 'react';
3
+
4
+ /**
5
+ * Where each kind of thing lives in the app. The ONE map of entity → URL, so a
6
+ * persona, standard or skill reads as a link everywhere it is mentioned.
7
+ * A feature belongs to a project: its links come from `featureHref` (lib/project.js).
8
+ */
9
+ export function entityHref(kind, id) {
10
+ const q = (page) => `/${page}?sel=${encodeURIComponent(id)}`;
11
+ switch (kind) {
12
+ case 'persona': return q('agents');
13
+ case 'skill': return q('skills');
14
+ case 'standard': return q('standards');
15
+ case 'rule': return q('rules');
16
+ case 'hook': return q('hooks');
17
+ case 'phase': return q('phases');
18
+ case 'guide': return `/guide/${encodeURIComponent(id)}`;
19
+ case 'rubric': return q('rubrics');
20
+ default: return '#';
21
+ }
22
+ }
23
+
24
+ /** `standards/backend/dotnet/testing.md` (as tasks.json cites it) → the registry `path`. */
25
+ export function standardKey(ref) {
26
+ return String(ref).replace(/^\.?\/?(\.morph\/framework\/)?standards\//, '');
27
+ }
28
+
29
+ const TONE = 'text-[var(--color-phase-proposal)] hover:underline underline-offset-2';
30
+
31
+ export default function EntityLink({ kind, id, children, className = '', chip = false, plain = false, title }) {
32
+ const cls = chip
33
+ ? `inline-flex items-center rounded-full px-2 py-0.5 font-mono text-[11px] text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)] transition-colors hover:text-[var(--color-fg)] hover:shadow-[inset_0_0_0_1px_var(--color-hair-strong)] ${className}`
34
+ : plain ? className : `${TONE} ${className}`;
35
+ return <Link to={entityHref(kind, id)} className={cls} title={title}>{children ?? id}</Link>;
36
+ }
37
+
38
+ /**
39
+ * The selected item of a knowledge page, kept in `?sel=` so a panel can be
40
+ * linked, bookmarked and reached from anywhere with EntityLink.
41
+ *
42
+ * @returns {[string|null, (id: string|null) => void]}
43
+ */
44
+ export function useSelection() {
45
+ const [params, setParams] = useSearchParams();
46
+ const sel = params.get('sel');
47
+ const setSel = useCallback((id) => {
48
+ setParams((p) => {
49
+ const n = new URLSearchParams(p);
50
+ if (id) n.set('sel', id); else n.delete('sel');
51
+ return n;
52
+ }, { replace: false });
53
+ }, [setParams]);
54
+ return [sel, setSel];
55
+ }
@@ -0,0 +1,54 @@
1
+ import { Component } from 'react';
2
+
3
+ /**
4
+ * Catches a page that throws while rendering and shows the error in place of
5
+ * the page, so the rest of the layout (the sidebar) keeps working.
6
+ *
7
+ * Without it one broken page unmounts the whole tree: 8.38.0 opened on
8
+ * /agents, which threw a ReferenceError, and the dashboard was a black screen.
9
+ *
10
+ * Mount it with `key={location.pathname}`: a boundary that already caught an
11
+ * error stays in fallback until it remounts, and the key remounts it when the
12
+ * sidebar navigates to another page.
13
+ */
14
+ export default class ErrorBoundary extends Component {
15
+ constructor(props) {
16
+ super(props);
17
+ this.state = { error: null };
18
+ }
19
+
20
+ static getDerivedStateFromError(error) {
21
+ return { error };
22
+ }
23
+
24
+ componentDidCatch(error, info) {
25
+ console.error(`[dashboard] ${this.props.page || 'page'} failed to render:`, error, info?.componentStack);
26
+ }
27
+
28
+ render() {
29
+ const { error } = this.state;
30
+ if (!error) return this.props.children;
31
+ return (
32
+ <div role="alert" className="p-6 max-w-3xl">
33
+ <h2 className="text-red-400 text-sm font-semibold uppercase tracking-wide mb-2">
34
+ This page failed to render
35
+ </h2>
36
+ <p className="text-gray-300 text-sm mb-3">
37
+ <span className="font-mono text-gray-400">{this.props.page}</span> threw while rendering.
38
+ The other pages still work — pick one in the sidebar.
39
+ </p>
40
+ <pre className="p-3 bg-gray-900 border border-red-900 rounded text-xs text-red-300 whitespace-pre-wrap font-mono overflow-x-auto">
41
+ {error.name}: {error.message}
42
+ </pre>
43
+ {error.stack && (
44
+ <details className="mt-2">
45
+ <summary className="text-gray-500 text-xs cursor-pointer">Stack trace</summary>
46
+ <pre className="mt-1 p-3 bg-gray-950 border border-gray-800 rounded text-[10px] text-gray-500 whitespace-pre-wrap font-mono overflow-x-auto">
47
+ {error.stack}
48
+ </pre>
49
+ </details>
50
+ )}
51
+ </div>
52
+ );
53
+ }
54
+ }
@@ -1,17 +1,19 @@
1
- import { ReactFlow, Controls, MiniMap, Background } from '@xyflow/react';
2
-
3
- export default function FlowGraph({ nodes, edges, nodeTypes, onNodeClick, fitView = true, className = '' }) {
4
- return (
5
- <div className={`w-full h-full ${className}`} style={{ minHeight: '500px' }}>
6
- <ReactFlow
7
- nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodeClick={onNodeClick}
8
- fitView={fitView} proOptions={{ hideAttribution: true }}
9
- defaultEdgeOptions={{ type: 'smoothstep', animated: false, style: { stroke: '#475569', strokeWidth: 1.5 } }}
10
- >
11
- <Background color="#1e293b" gap={20} />
12
- <Controls position="bottom-right" />
13
- <MiniMap nodeColor={(node) => node.data?.color || '#475569'} maskColor="rgba(0, 0, 0, 0.7)" position="bottom-left" />
14
- </ReactFlow>
15
- </div>
16
- );
17
- }
1
+ import { ReactFlow, Controls, MiniMap, Background } from '@xyflow/react';
2
+
3
+ // The React Flow attribution stays: hiding it is licensed to Pro subscribers only.
4
+ export default function FlowGraph({ nodes, edges, nodeTypes, onNodeClick, fitView = true, className = '' }) {
5
+ return (
6
+ <div className={`w-full h-full ${className}`} style={{ minHeight: '500px' }}>
7
+ <ReactFlow
8
+ nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodeClick={onNodeClick}
9
+ fitView={fitView} fitViewOptions={{ padding: 0.15, maxZoom: 1.1 }} minZoom={0.2}
10
+
11
+ defaultEdgeOptions={{ type: 'smoothstep', animated: false, style: { stroke: '#3a4250', strokeWidth: 1.5 } }}
12
+ >
13
+ <Background color="rgb(255 255 255 / 0.05)" gap={22} />
14
+ <Controls position="bottom-right" />
15
+ <MiniMap nodeColor={(node) => node.data?.color || '#4a5260'} maskColor="rgba(5, 5, 5, 0.72)" position="bottom-left" pannable zoomable />
16
+ </ReactFlow>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,53 @@
1
+ import morphData from 'virtual:morph-data';
2
+ import Markdown from './Markdown.jsx';
3
+ import EntityLink, { standardKey } from './EntityLink.jsx';
4
+
5
+ /**
6
+ * Names of things that have a page. Built once from the knowledge module.
7
+ */
8
+ const skillIds = new Set((morphData.skills || []).map((s) => s.id));
9
+ const personaIds = new Set((morphData.agents?.list || []).map((a) => a.id));
10
+ const standardByKey = new Map();
11
+ for (const s of morphData.standards?.list || []) {
12
+ standardByKey.set(s.id, s.id);
13
+ standardByKey.set(s.path, s.id);
14
+ }
15
+
16
+ /**
17
+ * What an inline code span names, if it names something with a page:
18
+ * `morph-plan` or `/morph-plan` → skill, `morph-dotnet-senior` → persona,
19
+ * a standard id or path → standard.
20
+ *
21
+ * @returns {{ kind: string, id: string } | null}
22
+ */
23
+ export function resolveMention(text) {
24
+ const t = String(text).trim();
25
+ const bare = t.replace(/^\//, '');
26
+ if (skillIds.has(bare)) return { kind: 'skill', id: bare };
27
+ const persona = /^morph-(.+)$/.exec(bare);
28
+ if (persona && personaIds.has(persona[1])) return { kind: 'persona', id: persona[1] };
29
+ const std = standardByKey.get(standardKey(t));
30
+ if (std) return { kind: 'standard', id: std };
31
+ return null;
32
+ }
33
+
34
+ function InlineCode({ className, children, node, ...rest }) {
35
+ const text = Array.isArray(children) ? children.join('') : String(children ?? '');
36
+ const block = Boolean(className) || text.includes('\n');
37
+ if (!block) {
38
+ const hit = resolveMention(text);
39
+ if (hit) {
40
+ return (
41
+ <EntityLink kind={hit.kind} id={hit.id} className="no-underline">
42
+ <code>{text}</code>
43
+ </EntityLink>
44
+ );
45
+ }
46
+ }
47
+ return <code className={className} {...rest}>{children}</code>;
48
+ }
49
+
50
+ /** Markdown for framework and feature text: code spans that name a skill, persona or standard become links. */
51
+ export default function KnowledgeMarkdown({ components, ...props }) {
52
+ return <Markdown {...props} components={{ code: InlineCode, ...components }} />;
53
+ }
@@ -0,0 +1,47 @@
1
+ import ReactMarkdown from 'react-markdown';
2
+ import remarkGfm from 'remark-gfm';
3
+ import rehypeSlug from 'rehype-slug';
4
+ import { splitFrontmatter } from '../lib/format.js';
5
+
6
+ const REMARK = [remarkGfm];
7
+ const REHYPE = [rehypeSlug];
8
+
9
+ /** Frontmatter keys worth showing, in the order a reader wants them. */
10
+ const META_ORDER = ['name', 'description', 'when-to-invoke', 'user-invokable', 'stacks', 'version', 'cliVersion'];
11
+
12
+ function MetaBlock({ meta }) {
13
+ const keys = Object.keys(meta).sort((a, b) => {
14
+ const ia = META_ORDER.indexOf(a); const ib = META_ORDER.indexOf(b);
15
+ return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
16
+ });
17
+ return (
18
+ <dl className="mb-6 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1.5 rounded-xl bg-white/[0.03] px-4 py-3 text-sm shadow-[inset_0_0_0_1px_var(--color-hair)]">
19
+ {keys.map((k) => (
20
+ <div key={k} className="contents">
21
+ <dt className="font-mono text-xs leading-6 text-[var(--color-faint)]">{k}</dt>
22
+ <dd className="leading-6 text-[var(--color-dim)]">{meta[k] || '—'}</dd>
23
+ </div>
24
+ ))}
25
+ </dl>
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Rendered markdown for every long text in the app: feature artifacts, skills,
31
+ * standards, the charter. Frontmatter is lifted into a metadata block instead
32
+ * of being printed as text.
33
+ *
34
+ * @param {{ text: string, components?: object, className?: string, size?: 'md'|'sm' }} props
35
+ * `sm` is for side panels, where the reading column is narrower.
36
+ */
37
+ export default function Markdown({ text, components, className = '', size = 'md' }) {
38
+ const { meta, body } = splitFrontmatter(text || '');
39
+ return (
40
+ <article className={`prose-morph ${size === 'sm' ? 'prose-morph-sm' : ''} ${className}`}>
41
+ {meta && Object.keys(meta).length > 0 && <MetaBlock meta={meta} />}
42
+ <ReactMarkdown remarkPlugins={REMARK} rehypePlugins={REHYPE} components={components}>
43
+ {body}
44
+ </ReactMarkdown>
45
+ </article>
46
+ );
47
+ }