@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
@@ -1,429 +1,464 @@
1
1
  import { useState, useMemo, useCallback } from 'react';
2
2
  import { Handle, Position, MarkerType } from '@xyflow/react';
3
+ import { CaretDown, CaretRight } from '@phosphor-icons/react';
3
4
  import morphData from 'virtual:morph-data';
4
5
  import FlowGraph from '../components/FlowGraph.jsx';
5
6
  import DetailPanel from '../components/DetailPanel.jsx';
6
- import { useDetailPanel } from '../hooks/useDetailPanel.js';
7
-
8
- // Event color palette
9
- const EVENT_COLORS = {
10
- 'session-start': { color: '#34d399', bg: '#064e3b', label: 'Session Start' },
11
- 'user-prompt': { color: '#fbbf24', bg: '#78350f', label: 'User Prompt' },
12
- 'pre-tool-use': { color: '#60a5fa', bg: '#1e3a5f', label: 'Pre-Tool-Use' },
13
- 'post-tool-use': { color: '#f472b6', bg: '#500724', label: 'Post-Tool-Use' },
14
- 'pre-compact': { color: '#a78bfa', bg: '#2e1065', label: 'Pre-Compact' },
15
- 'stop': { color: '#ef4444', bg: '#450a0a', label: 'Stop' },
16
- 'teammate-idle': { color: '#94a3b8', bg: '#1e293b', label: 'Teammate Idle' },
17
- 'worktree-create': { color: '#2dd4bf', bg: '#134e4a', label: 'Worktree Create' },
18
- 'worktree-remove': { color: '#fb923c', bg: '#431407', label: 'Worktree Remove' },
7
+ import EntityLink, { useSelection } from '../components/EntityLink.jsx';
8
+
9
+ /**
10
+ * Every event the scanner can report. `when` and `power` are what the panel
11
+ * explains; `canBlock` is the badge on the graph (the tool-call veto).
12
+ * Colors are the dashboard palette as hex, because the MiniMap cannot read
13
+ * CSS variables.
14
+ */
15
+ const EVENTS = {
16
+ 'session-start': {
17
+ label: 'Início da sessão', color: '#73daca',
18
+ when: 'Dispara quando uma sessão do Claude Code começa, é retomada ou limpa.',
19
+ power: 'Observa: não impede nada, mas pode injetar contexto na sessão.',
20
+ },
21
+ 'user-prompt': {
22
+ label: 'Prompt do usuário', color: '#e0af68',
23
+ when: 'Dispara quando o usuário envia uma mensagem, antes de o Claude processá-la.',
24
+ power: 'Pode acrescentar contexto ao prompt ou barrá-lo.',
25
+ },
26
+ 'pre-tool-use': {
27
+ label: 'Antes da ferramenta', color: '#7aa2f7', canBlock: true, toolEvent: true,
28
+ when: 'Dispara antes de cada chamada de ferramenta que casa com o matcher (Bash, Write, Edit…).',
29
+ power: 'Pode bloquear a chamada: a ferramenta não roda e o Claude recebe o motivo.',
30
+ },
31
+ 'post-tool-use': {
32
+ label: 'Depois da ferramenta', color: '#bb9af7', toolEvent: true,
33
+ when: 'Dispara depois que a ferramenta roda (inclui falhas e lotes de chamadas).',
34
+ power: 'Observa o resultado: não desfaz a chamada, mas pode devolver um aviso ao Claude.',
35
+ },
36
+ stop: {
37
+ label: 'Fim da resposta', color: '#f7768e',
38
+ when: 'Dispara quando o Claude (ou um sub-agent) termina de responder.',
39
+ power: 'Observa e valida; pode pedir que o Claude continue em vez de parar.',
40
+ },
41
+ 'pre-compact': {
42
+ label: 'Antes de compactar', color: '#ff9e64',
43
+ when: 'Dispara antes de o Claude Code compactar o histórico da conversa.',
44
+ power: 'Observa: não impede a compactação.',
45
+ },
46
+ 'teammate-idle': {
47
+ label: 'Colega ocioso', color: '#8d97a5',
48
+ when: 'Dispara quando um colega de uma equipe de agentes fica ocioso.',
49
+ power: 'Observa.',
50
+ },
51
+ 'worktree-create': {
52
+ label: 'Criar worktree', color: '#9ece6a',
53
+ when: 'Dispara quando o Claude Code cria uma worktree (claude -w, EnterWorktree, agente isolado).',
54
+ power: 'Assume a criação: o morph-spec a delega ao próprio CLI.',
55
+ },
56
+ 'worktree-remove': {
57
+ label: 'Remover worktree', color: '#ff9e64',
58
+ when: 'Dispara quando o Claude Code remove uma worktree que ele criou.',
59
+ power: 'Assume a remoção: o morph-spec a delega ao próprio CLI.',
60
+ },
61
+ unregistered: {
62
+ label: 'Não registrados', color: '#5c6573',
63
+ when: 'Arquivos de hook que o instalador não registra em nenhum evento.',
64
+ power: 'Nunca disparam: estão no framework, mas fora da lista de hooks instalados.',
65
+ },
19
66
  };
20
67
 
21
- // Node dimensions
22
- const NODE_W = 200;
23
- const NODE_H = 120; // base; expands with hooks
68
+ function eventMeta(key) {
69
+ return EVENTS[key] || { label: key, color: '#8d97a5', when: 'Evento sem descrição no dashboard.', power: '' };
70
+ }
71
+
72
+ /**
73
+ * The URL key of one registration. A hook file can register more than once
74
+ * under the same event with different matchers, so the name alone is not
75
+ * unique: `?sel=` holds `event:name:matcher`. A bare hook name also works and
76
+ * opens that hook's first registration.
77
+ */
78
+ export function hookKey(hook) {
79
+ return `${hook.event}:${hook.name}:${hook.matcher || ''}`;
80
+ }
81
+
82
+ function findHook(hooks, sel) {
83
+ if (!sel) return null;
84
+ return hooks.find((h) => hookKey(h) === sel) || hooks.find((h) => h.name === sel) || null;
85
+ }
86
+
87
+ function matcherText(hook) {
88
+ if (hook.matcher) return hook.matcher;
89
+ return eventMeta(hook.event).toolEvent ? 'todas as ferramentas' : null;
90
+ }
91
+
92
+ // ─── Nodes ────────────────────────────────────────────────────────────────────
93
+
94
+ const NODE_W = 280;
95
+ const ROW_H = 26;
96
+ const HEAD_H = 58;
97
+ const nodeHeight = (count) => HEAD_H + Math.max(count, 1) * ROW_H;
24
98
 
25
- // ─── Custom: EventGroupNode ───────────────────────────────────────────────────
26
99
  function EventGroupNode({ data }) {
27
- const { eventKey, hooks, canBlock } = data;
28
- const palette = EVENT_COLORS[eventKey] || { color: '#94a3b8', bg: '#1e293b', label: eventKey };
100
+ const { eventKey, hooks, onSelectHook, selectedKey } = data;
101
+ const meta = eventMeta(eventKey);
29
102
 
30
103
  return (
31
104
  <div
32
- className="rounded-xl px-4 py-3 min-w-[190px] cursor-pointer select-none"
33
- style={{
34
- background: palette.bg,
35
- border: `2px solid ${palette.color}`,
36
- boxShadow: `0 0 12px ${palette.color}33`,
37
- }}
105
+ className="cursor-pointer select-none rounded-xl bg-[var(--color-raised)] px-3.5 py-3"
106
+ style={{ width: NODE_W, boxShadow: `inset 0 0 0 1px var(--color-hair-strong), inset 3px 0 0 ${meta.color}` }}
38
107
  >
39
- <Handle type="target" position={Position.Left}
40
- style={{ background: palette.color, width: 8, height: 8, border: 'none' }} />
41
-
42
- {/* Header */}
43
- <div className="flex items-center justify-between mb-2">
44
- <span className="text-white text-xs font-bold tracking-wide uppercase">{palette.label}</span>
45
- {canBlock && (
46
- <span className="text-[9px] font-bold px-1.5 py-0.5 rounded"
47
- style={{ background: '#1e40af', color: '#93c5fd', border: '1px solid #3b82f6' }}>
48
- can BLOCK
108
+ <Handle type="target" position={Position.Left} style={{ background: meta.color, width: 8, height: 8, border: 'none' }} />
109
+
110
+ <div className="mb-2 flex items-center justify-between gap-2">
111
+ <span className="text-[13px] font-medium text-[var(--color-fg)]">{meta.label}</span>
112
+ {meta.canBlock ? (
113
+ <span className="rounded-full px-2 py-0.5 text-xs text-[var(--color-warn)] shadow-[inset_0_0_0_1px_var(--color-hair-strong)]">
114
+ pode bloquear
49
115
  </span>
116
+ ) : (
117
+ <span className="text-xs text-[var(--color-faint)]">{hooks.length}</span>
50
118
  )}
51
119
  </div>
52
120
 
53
- {/* Hook list */}
54
- <ul className="space-y-0.5">
55
- {hooks.map((h) => (
56
- <li key={h.name}
57
- className="text-[10px] font-mono truncate"
58
- style={{ color: palette.color }}
59
- title={h.name}>
60
- · {h.name}
61
- </li>
62
- ))}
63
- </ul>
64
-
65
- <Handle type="source" position={Position.Right}
66
- style={{ background: palette.color, width: 8, height: 8, border: 'none' }} />
121
+ {hooks.length === 0 ? (
122
+ <p className="text-xs text-[var(--color-faint)]">Nenhum hook do morph-spec aqui.</p>
123
+ ) : (
124
+ <ul className="space-y-0.5">
125
+ {hooks.map((h) => {
126
+ const key = hookKey(h);
127
+ const matcher = matcherText(h);
128
+ return (
129
+ <li key={key}>
130
+ <button
131
+ type="button"
132
+ className="nodrag flex w-full items-center justify-between gap-2 rounded-md px-1.5 py-0.5 text-left text-xs transition-colors hover:bg-white/[0.06]"
133
+ style={{ background: key === selectedKey ? 'rgb(255 255 255 / 0.08)' : undefined }}
134
+ onClick={(e) => { e.stopPropagation(); onSelectHook(key); }}
135
+ title={h.description || h.name}
136
+ >
137
+ <span className="truncate font-mono text-[var(--color-fg)]">{h.name}</span>
138
+ {matcher && <span className="shrink-0 truncate font-mono text-[var(--color-dim)]" style={{ maxWidth: 110 }}>{matcher}</span>}
139
+ </button>
140
+ </li>
141
+ );
142
+ })}
143
+ </ul>
144
+ )}
145
+
146
+ <Handle type="source" position={Position.Right} style={{ background: meta.color, width: 8, height: 8, border: 'none' }} />
67
147
  </div>
68
148
  );
69
149
  }
70
150
 
71
- // ─── Custom: ToolNode ─────────────────────────────────────────────────────────
72
- function ToolNode({ data }) {
151
+ function ToolNode() {
73
152
  return (
74
- <div
75
- className="rounded-xl px-5 py-3 select-none text-center min-w-[140px]"
76
- style={{
77
- background: '#374151',
78
- border: '2px solid #6b7280',
79
- boxShadow: '0 0 10px #6b728033',
80
- }}
81
- >
82
- <Handle type="target" position={Position.Left}
83
- style={{ background: '#9ca3af', width: 8, height: 8, border: 'none' }} />
84
- <div className="text-gray-100 text-sm font-bold">TOOL</div>
85
- <div className="text-gray-400 text-[10px] mt-0.5">executes</div>
86
- <Handle type="source" position={Position.Right}
87
- style={{ background: '#9ca3af', width: 8, height: 8, border: 'none' }} />
153
+ <div className="cursor-pointer select-none rounded-xl bg-[var(--color-core)] px-5 py-3 text-center shadow-[inset_0_0_0_1px_var(--color-hair-strong)]" style={{ width: 150 }}>
154
+ <Handle type="target" position={Position.Left} style={{ background: '#8d97a5', width: 8, height: 8, border: 'none' }} />
155
+ <div className="text-sm font-medium text-[var(--color-fg)]">Ferramenta</div>
156
+ <div className="mt-0.5 text-xs text-[var(--color-dim)]">executa</div>
157
+ <Handle type="source" position={Position.Right} style={{ background: '#8d97a5', width: 8, height: 8, border: 'none' }} />
88
158
  </div>
89
159
  );
90
160
  }
91
161
 
92
- const nodeTypes = {
93
- eventGroupNode: EventGroupNode,
94
- toolNode: ToolNode,
95
- };
162
+ const nodeTypes = { eventGroupNode: EventGroupNode, toolNode: ToolNode };
163
+
164
+ // ─── Layout ───────────────────────────────────────────────────────────────────
165
+ // Main row: session-start → user-prompt → pre-tool-use → tool → post-tool-use → stop / pre-compact
166
+ // Second row: events outside the tool-call flow (worktree pair, teammate-idle,
167
+ // unregistered hooks and any event this page does not know).
96
168
 
97
- // ─── Layout: manual horizontal positions ─────────────────────────────────────
98
- // Flow: session-start → user-prompt → pre-tool-use → TOOL → post-tool-use → stop
99
- // → pre-compact
100
- // teammate-idle is disconnected at bottom
101
- // Loop-back: post-tool-use → user-prompt (dashed)
102
-
103
- const COL_GAP = 260;
104
- const ROW_Y = 100;
105
- const BRANCH_Y_STOP = 80;
106
- const BRANCH_Y_COMPACT = 240;
107
-
108
- function buildNodesAndEdges(hooksByEvent) {
109
- // Main flow columns (x positions)
110
- const cols = {
111
- 'session-start': 0,
112
- 'user-prompt': COL_GAP,
113
- 'pre-tool-use': COL_GAP * 2,
114
- '__tool__': COL_GAP * 3,
115
- 'post-tool-use': COL_GAP * 4,
116
- 'stop': COL_GAP * 5,
117
- 'pre-compact': COL_GAP * 5,
118
- 'teammate-idle': COL_GAP * 2,
119
- // Worktree lifecycle — outside the tool-call flow, drawn as a pair below it.
120
- 'worktree-create': COL_GAP * 0,
121
- 'worktree-remove': COL_GAP * 1,
122
- };
169
+ const COL_GAP = NODE_W + 90;
170
+ const MAIN_ROW = ['session-start', 'user-prompt', 'pre-tool-use', '__tool__', 'post-tool-use'];
171
+ const SIDE_ROW = ['worktree-create', 'worktree-remove', 'teammate-idle', 'unregistered'];
123
172
 
173
+ function buildNodesAndEdges(hooksByEvent, onSelectHook, selectedKey) {
124
174
  const nodes = [];
125
175
  const edges = [];
176
+ const hooksOf = (k) => hooksByEvent[k] || [];
126
177
 
127
- // Helper to build an EventGroupNode
128
- function addEventNode(eventKey, x, y, extra = {}) {
129
- const hooks = hooksByEvent[eventKey] || [];
178
+ function addEventNode(eventKey, x, y) {
130
179
  nodes.push({
131
180
  id: eventKey,
132
181
  type: 'eventGroupNode',
133
182
  position: { x, y },
134
- data: {
135
- eventKey,
136
- hooks,
137
- canBlock: eventKey === 'pre-tool-use',
138
- color: (EVENT_COLORS[eventKey] || {}).color || '#94a3b8',
139
- ...extra,
140
- },
183
+ data: { eventKey, hooks: hooksOf(eventKey), onSelectHook, selectedKey, color: eventMeta(eventKey).color },
141
184
  });
142
185
  }
143
186
 
144
- // Main flow nodes
145
- addEventNode('session-start', cols['session-start'], ROW_Y);
146
- addEventNode('user-prompt', cols['user-prompt'], ROW_Y);
147
- addEventNode('pre-tool-use', cols['pre-tool-use'], ROW_Y);
148
-
149
- // TOOL node
150
- nodes.push({
151
- id: '__tool__',
152
- type: 'toolNode',
153
- position: { x: cols['__tool__'], y: ROW_Y + 30 },
154
- data: { color: '#6b7280' },
187
+ MAIN_ROW.forEach((key, i) => {
188
+ if (key === '__tool__') {
189
+ nodes.push({ id: '__tool__', type: 'toolNode', position: { x: i * COL_GAP + (NODE_W - 150) / 2, y: 20 }, data: { color: '#8d97a5' } });
190
+ } else {
191
+ addEventNode(key, i * COL_GAP, 0);
192
+ }
155
193
  });
156
194
 
157
- addEventNode('post-tool-use', cols['post-tool-use'], ROW_Y);
158
-
159
- // Branches: stop (top branch) and pre-compact (bottom branch)
160
- addEventNode('stop', cols['stop'], BRANCH_Y_STOP);
161
- addEventNode('pre-compact', cols['pre-compact'], BRANCH_Y_COMPACT);
195
+ const branchX = MAIN_ROW.length * COL_GAP;
196
+ const stopH = nodeHeight(hooksOf('stop').length);
197
+ addEventNode('stop', branchX, 0);
198
+ addEventNode('pre-compact', branchX, stopH + 50);
162
199
 
163
- // teammate-idle disconnected below main flow
164
- addEventNode('teammate-idle', cols['teammate-idle'] - COL_GAP * 0.5, ROW_Y + 280);
165
-
166
- // Worktree lifecycle (WorktreeCreate → WorktreeRemove) — its own row below
167
- // the main flow: these fire on `claude -w` / EnterWorktree / Agent isolation,
168
- // not on a tool call.
169
- addEventNode('worktree-create', cols['worktree-create'], ROW_Y + 280);
170
- addEventNode('worktree-remove', cols['worktree-remove'], ROW_Y + 280);
171
-
172
- // ─── Edges ────────────────────────────────────────────────────────────
173
- const solid = (id, source, target, color = '#475569') => ({
174
- id, source, target,
175
- type: 'smoothstep',
176
- style: { stroke: color, strokeWidth: 2 },
200
+ const mainBottom = Math.max(
201
+ ...MAIN_ROW.filter((k) => k !== '__tool__').map((k) => nodeHeight(hooksOf(k).length)),
202
+ stopH + 50 + nodeHeight(hooksOf('pre-compact').length),
203
+ );
204
+ const sideY = mainBottom + 110;
205
+ const known = new Set([...MAIN_ROW, 'stop', 'pre-compact', ...SIDE_ROW]);
206
+ const extra = Object.keys(hooksByEvent).filter((k) => !known.has(k));
207
+ [...SIDE_ROW, ...extra].forEach((key, i) => addEventNode(key, i * COL_GAP, sideY));
208
+
209
+ const solid = (id, source, target, color) => ({
210
+ id, source, target, type: 'smoothstep',
211
+ style: { stroke: color, strokeWidth: 1.8, opacity: 0.75 },
177
212
  markerEnd: { type: MarkerType.ArrowClosed, color },
178
213
  });
179
214
 
180
- edges.push(solid('e-ss-up', 'session-start', 'user-prompt', EVENT_COLORS['user-prompt'].color));
181
- edges.push(solid('e-up-ptu', 'user-prompt', 'pre-tool-use', EVENT_COLORS['pre-tool-use'].color));
182
- edges.push(solid('e-ptu-tool', 'pre-tool-use', '__tool__', '#6b7280'));
183
- edges.push(solid('e-tool-pou', '__tool__', 'post-tool-use', EVENT_COLORS['post-tool-use'].color));
184
-
185
- // Post-tool-use branches to stop and pre-compact
186
- edges.push(solid('e-pou-stop', 'post-tool-use', 'stop', EVENT_COLORS['stop'].color));
187
- edges.push(solid('e-pou-compact', 'post-tool-use', 'pre-compact', EVENT_COLORS['pre-compact'].color));
188
- edges.push(solid('e-wt-create-remove', 'worktree-create', 'worktree-remove', EVENT_COLORS['worktree-remove'].color));
189
-
190
- // Loop-back dashed: post-tool-use → user-prompt
215
+ edges.push(solid('e-ss-up', 'session-start', 'user-prompt', EVENTS['user-prompt'].color));
216
+ edges.push(solid('e-up-ptu', 'user-prompt', 'pre-tool-use', EVENTS['pre-tool-use'].color));
217
+ edges.push(solid('e-ptu-tool', 'pre-tool-use', '__tool__', '#8d97a5'));
218
+ edges.push(solid('e-tool-pou', '__tool__', 'post-tool-use', EVENTS['post-tool-use'].color));
219
+ edges.push(solid('e-pou-stop', 'post-tool-use', 'stop', EVENTS.stop.color));
220
+ edges.push(solid('e-pou-compact', 'post-tool-use', 'pre-compact', EVENTS['pre-compact'].color));
221
+ edges.push(solid('e-wt', 'worktree-create', 'worktree-remove', EVENTS['worktree-remove'].color));
191
222
  edges.push({
192
- id: 'e-loop',
193
- source: 'post-tool-use',
194
- target: 'user-prompt',
195
- type: 'smoothstep',
196
- style: { stroke: '#fbbf24', strokeWidth: 1.5, strokeDasharray: '6 4' },
197
- label: 'conversation loop',
198
- labelStyle: { fill: '#fbbf24', fontSize: 10, fontFamily: 'monospace' },
199
- labelBgStyle: { fill: '#1c1917', fillOpacity: 0.85 },
200
- markerEnd: { type: MarkerType.ArrowClosed, color: '#fbbf24' },
223
+ id: 'e-loop', source: 'post-tool-use', target: 'user-prompt', type: 'smoothstep',
224
+ style: { stroke: '#e0af68', strokeWidth: 1.4, strokeDasharray: '6 4', opacity: 0.7 },
225
+ label: 'a conversa continua',
226
+ labelStyle: { fill: '#e0af68', fontSize: 12 },
227
+ labelBgStyle: { fill: '#12151b', fillOpacity: 0.9 },
228
+ markerEnd: { type: MarkerType.ArrowClosed, color: '#e0af68' },
201
229
  });
202
230
 
203
231
  return { nodes, edges };
204
232
  }
205
233
 
206
- // ─── HookDetail: detail panel content ────────────────────────────────────────
207
- function HookDetail({ hook }) {
234
+ // ─── Panels ───────────────────────────────────────────────────────────────────
235
+
236
+ function Section({ title, children }) {
237
+ return (
238
+ <section>
239
+ <h3 className="mb-2 text-xs font-medium text-[var(--color-faint)]">{title}</h3>
240
+ {children}
241
+ </section>
242
+ );
243
+ }
244
+
245
+ function EventExplainer({ eventKey }) {
246
+ const meta = eventMeta(eventKey);
247
+ return (
248
+ <div className="space-y-1.5 rounded-xl bg-white/[0.03] px-4 py-3 text-sm leading-relaxed shadow-[inset_0_0_0_1px_var(--color-hair)]">
249
+ <p className="flex items-center gap-2 text-[var(--color-fg)]">
250
+ <span className="inline-block h-2 w-2 shrink-0 rounded-full" style={{ background: meta.color }} />
251
+ {meta.label}
252
+ </p>
253
+ <p className="text-[var(--color-dim)]">{meta.when}</p>
254
+ {meta.power && <p className="text-[var(--color-dim)]">{meta.power}</p>}
255
+ </div>
256
+ );
257
+ }
258
+
259
+ function HookDetail({ hook, hookNames }) {
208
260
  const [showSource, setShowSource] = useState(false);
209
- const palette = EVENT_COLORS[hook.event] || { color: '#94a3b8' };
261
+ const matcher = matcherText(hook);
210
262
 
211
263
  return (
212
- <div className="text-sm text-gray-200 space-y-3">
213
- {/* Meta */}
214
- <div>
215
- <p className="text-gray-400 text-xs">
216
- Event: <span className="font-mono" style={{ color: palette.color }}>{hook.event}</span>
217
- </p>
218
- <p className="text-gray-400 text-xs mt-0.5">
219
- File: <span className="text-gray-300 font-mono">{hook.filename}</span>
220
- </p>
221
- </div>
264
+ <div className="space-y-6 text-sm text-[var(--color-dim)]">
265
+ <Section title="Quando roda">
266
+ <EventExplainer eventKey={hook.event} />
267
+ {hook.rawEvent && (
268
+ <p className="mt-2 text-xs">Evento do Claude Code: <span className="font-mono text-[var(--color-fg)]">{hook.rawEvent}</span></p>
269
+ )}
270
+ </Section>
271
+
272
+ {matcher && (
273
+ <Section title="Ferramentas observadas">
274
+ <code className="inline-block rounded-lg bg-white/[0.03] px-2.5 py-1 font-mono text-xs text-[var(--color-fg)] shadow-[inset_0_0_0_1px_var(--color-hair)]">{matcher}</code>
275
+ </Section>
276
+ )}
222
277
 
223
- {/* Description */}
224
278
  {hook.description && (
225
- <div>
226
- <p className="text-gray-500 text-[10px] uppercase tracking-wide mb-1">Description</p>
227
- <p className="text-gray-300 text-xs leading-relaxed">{hook.description}</p>
228
- </div>
279
+ <Section title="O que faz">
280
+ <p className="whitespace-pre-line leading-relaxed text-[var(--color-fg)]">{hook.description}</p>
281
+ </Section>
229
282
  )}
230
283
 
231
- {/* Imports */}
232
284
  {hook.imports?.length > 0 && (
233
- <div>
234
- <p className="text-gray-500 text-[10px] uppercase tracking-wide mb-1.5">Imports</p>
235
- <div className="flex flex-wrap gap-1">
236
- {hook.imports.map((imp) => (
237
- <span key={imp}
238
- className="px-2 py-0.5 rounded text-[10px] font-mono bg-gray-800 border border-gray-700 text-gray-400"
239
- title={imp}>
240
- {imp.length > 28 ? '…' + imp.slice(-22) : imp}
241
- </span>
242
- ))}
285
+ <Section title="Importa">
286
+ <div className="flex flex-wrap gap-1.5">
287
+ {hook.imports.map((imp) => {
288
+ const base = imp.split('/').pop().replace(/\.m?js$/, '');
289
+ return hookNames.has(base) && base !== hook.name ? (
290
+ <EntityLink key={imp} kind="hook" id={base} chip title={imp}>{base}</EntityLink>
291
+ ) : (
292
+ <span key={imp} title={imp} className="rounded-full px-2 py-0.5 font-mono text-[11px] text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)]">
293
+ {imp}
294
+ </span>
295
+ );
296
+ })}
243
297
  </div>
244
- </div>
298
+ </Section>
245
299
  )}
246
300
 
247
- {/* Source toggle */}
248
- <div>
301
+ <Section title="Código">
249
302
  <button
303
+ type="button"
250
304
  onClick={() => setShowSource((v) => !v)}
251
- className="w-full text-left px-3 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded text-xs text-gray-300 transition-colors"
305
+ className="fluid flex w-full items-center gap-2 rounded-lg bg-white/[0.04] px-3 py-2 text-left text-xs text-[var(--color-fg)] shadow-[inset_0_0_0_1px_var(--color-hair)] transition-colors hover:bg-white/[0.07]"
306
+ aria-expanded={showSource}
252
307
  >
253
- {showSource ? 'Hide Source' : 'Show Source'}
308
+ {showSource ? <CaretDown weight="light" size={14} /> : <CaretRight weight="light" size={14} />}
309
+ <span className="font-mono">{hook.filename}</span>
310
+ <span className="ml-auto text-[var(--color-faint)]">{showSource ? 'ocultar' : 'mostrar'}</span>
254
311
  </button>
255
312
  {showSource && (
256
- <pre className="mt-2 p-3 bg-gray-950 border border-gray-800 rounded text-[9px] text-gray-400 overflow-x-auto whitespace-pre font-mono leading-relaxed max-h-80 overflow-y-auto">
313
+ <pre className="mt-2 max-h-[28rem] overflow-auto rounded-lg bg-[var(--color-ink)] p-3 font-mono text-xs leading-relaxed text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)]">
257
314
  {hook.source}
258
315
  </pre>
259
316
  )}
260
- </div>
317
+ </Section>
261
318
  </div>
262
319
  );
263
320
  }
264
321
 
265
- // ─── EventGroupDetail: panel for clicking a group node ───────────────────────
266
- function EventGroupDetail({ eventKey, hooks, openHook }) {
267
- const palette = EVENT_COLORS[eventKey] || { color: '#94a3b8', label: eventKey };
268
- const isBlockable = eventKey === 'pre-tool-use';
269
-
322
+ function EventGroupDetail({ eventKey, hooks, onSelectHook }) {
270
323
  return (
271
- <div className="space-y-3">
272
- {/* Event header */}
273
- <div className="flex items-center gap-2">
274
- <span
275
- className="inline-block w-2.5 h-2.5 rounded-full flex-shrink-0"
276
- style={{ background: palette.color }}
277
- />
278
- <span className="text-gray-300 text-sm font-semibold">{palette.label}</span>
279
- {isBlockable && (
280
- <span className="text-[9px] font-bold px-1.5 py-0.5 rounded"
281
- style={{ background: '#1e40af', color: '#93c5fd', border: '1px solid #3b82f6' }}>
282
- can BLOCK
283
- </span>
324
+ <div className="space-y-5 text-sm text-[var(--color-dim)]">
325
+ <EventExplainer eventKey={eventKey} />
326
+ <Section title={`${hooks.length} ${hooks.length === 1 ? 'hook registrado' : 'hooks registrados'}`}>
327
+ {hooks.length === 0 ? (
328
+ <p className="text-xs">O morph-spec não instala nenhum hook neste evento.</p>
329
+ ) : (
330
+ <ul className="space-y-2">
331
+ {hooks.map((hook) => {
332
+ const matcher = matcherText(hook);
333
+ return (
334
+ <li key={hookKey(hook)}>
335
+ <button
336
+ type="button"
337
+ onClick={() => onSelectHook(hookKey(hook))}
338
+ className="fluid w-full rounded-xl bg-white/[0.03] px-3.5 py-2.5 text-left shadow-[inset_0_0_0_1px_var(--color-hair)] transition-colors hover:bg-white/[0.06]"
339
+ >
340
+ <span className="flex items-center justify-between gap-2">
341
+ <span className="font-mono text-xs text-[var(--color-fg)]">{hook.name}</span>
342
+ {matcher && <span className="font-mono text-xs text-[var(--color-dim)]">{matcher}</span>}
343
+ </span>
344
+ {hook.description && (
345
+ <span className="mt-1 line-clamp-2 block text-xs leading-snug">{hook.description}</span>
346
+ )}
347
+ </button>
348
+ </li>
349
+ );
350
+ })}
351
+ </ul>
284
352
  )}
285
- </div>
286
- <p className="text-gray-500 text-xs">{hooks.length} hook{hooks.length !== 1 ? 's' : ''} registered</p>
287
-
288
- {/* Hook list */}
289
- <ul className="space-y-2">
290
- {hooks.map((hook) => (
291
- <li key={hook.name}>
292
- <button
293
- onClick={() => openHook(hook)}
294
- className="w-full text-left px-3 py-2 rounded-lg border transition-colors hover:opacity-90"
295
- style={{
296
- background: '#111827',
297
- borderColor: palette.color + '55',
298
- }}
299
- >
300
- <p className="font-mono text-xs" style={{ color: palette.color }}>{hook.name}</p>
301
- {hook.description && (
302
- <p className="text-gray-400 text-[10px] mt-0.5 line-clamp-2 leading-snug">
303
- {hook.description}
304
- </p>
305
- )}
306
- </button>
307
- </li>
308
- ))}
309
- </ul>
353
+ </Section>
354
+ </div>
355
+ );
356
+ }
357
+
358
+ function ToolDetail() {
359
+ return (
360
+ <div className="space-y-3 text-sm leading-relaxed text-[var(--color-dim)]">
361
+ <p>
362
+ A chamada de ferramenta que o Claude Code faz de fato: <span className="font-mono text-[var(--color-fg)]">Bash</span>,{' '}
363
+ <span className="font-mono text-[var(--color-fg)]">Read</span>, <span className="font-mono text-[var(--color-fg)]">Edit</span> e as demais.
364
+ </p>
365
+ <p>
366
+ Os hooks de antes da ferramenta rodam primeiro e podem bloqueá-la. Os de depois rodam com o resultado em mãos e só observam.
367
+ </p>
310
368
  </div>
311
369
  );
312
370
  }
313
371
 
314
372
  // ─── Main Page ────────────────────────────────────────────────────────────────
373
+
315
374
  export default function HookInspector() {
316
375
  const { hooks } = morphData;
317
- const { panel, openPanel, closePanel } = useDetailPanel();
376
+ const [sel, setSel] = useSelection();
377
+ // Event group and tool panels are local: only a hook has a shareable URL.
378
+ const [aside, setAside] = useState(null);
318
379
 
319
- // Group hooks by event
320
380
  const hooksByEvent = useMemo(() => {
321
381
  const map = {};
322
- for (const hook of hooks) {
323
- if (!map[hook.event]) map[hook.event] = [];
324
- map[hook.event].push(hook);
325
- }
382
+ for (const hook of hooks) (map[hook.event] ||= []).push(hook);
326
383
  return map;
327
384
  }, [hooks]);
328
385
 
329
- // Build graph
330
- const { nodes, edges } = useMemo(() => buildNodesAndEdges(hooksByEvent), [hooksByEvent]);
386
+ const hookNames = useMemo(() => new Set(hooks.map((h) => h.name)), [hooks]);
387
+ const selectedHook = findHook(hooks, sel);
388
+ const selectedKey = selectedHook ? hookKey(selectedHook) : null;
331
389
 
332
- // Open panel: click on an event group → show its hooks list
333
- // From that list, user can click a hook → show hook detail
334
- const openHookDetail = useCallback((hook) => {
335
- const palette = EVENT_COLORS[hook.event] || { label: hook.event };
336
- openPanel(`${hook.name}`, <HookDetail hook={hook} />);
337
- }, [openPanel]);
390
+ const { nodes, edges } = useMemo(
391
+ () => buildNodesAndEdges(hooksByEvent, setSel, selectedKey),
392
+ [hooksByEvent, setSel, selectedKey]
393
+ );
338
394
 
339
395
  const handleNodeClick = useCallback((_event, node) => {
340
- if (node.type === 'toolNode') {
341
- openPanel('TOOL executes', (
342
- <div className="text-gray-300 text-sm space-y-2">
343
- <p>This represents the actual tool call Claude Code performs (e.g., <span className="font-mono text-blue-300">Bash</span>, <span className="font-mono text-blue-300">Read</span>, <span className="font-mono text-blue-300">Edit</span>).</p>
344
- <p className="text-gray-400 text-xs">Pre-Tool-Use hooks run before this and can block execution by returning a non-empty decision. Post-Tool-Use hooks run after and receive the tool result.</p>
345
- </div>
346
- ));
347
- return;
348
- }
349
- if (node.type === 'eventGroupNode') {
350
- const { eventKey, hooks: hooksInGroup } = node.data;
351
- const palette = EVENT_COLORS[eventKey] || { label: eventKey };
352
- openPanel(
353
- palette.label,
354
- <EventGroupDetail eventKey={eventKey} hooks={hooksInGroup} openHook={openHookDetail} />
355
- );
356
- }
357
- }, [openPanel, openHookDetail]);
358
-
359
- const totalHooks = hooks.length;
360
- const eventCount = Object.keys(hooksByEvent).length;
396
+ if (node.type === 'toolNode') setAside({ type: 'tool' });
397
+ else if (node.type === 'eventGroupNode') setAside({ type: 'event', key: node.data.eventKey });
398
+ }, []);
399
+
400
+ const closePanel = useCallback(() => {
401
+ if (selectedHook) setSel(null);
402
+ else setAside(null);
403
+ }, [selectedHook, setSel]);
404
+
405
+ const fileCount = hookNames.size;
406
+ const registered = hooks.filter((h) => h.event !== 'unregistered').length;
407
+ const unregistered = hooks.length - registered;
408
+
409
+ let title = '';
410
+ let subtitle;
411
+ let content = null;
412
+ if (selectedHook) {
413
+ title = selectedHook.name;
414
+ subtitle = [eventMeta(selectedHook.event).label, matcherText(selectedHook)].filter(Boolean).join(' · ');
415
+ content = <HookDetail key={selectedKey} hook={selectedHook} hookNames={hookNames} />;
416
+ } else if (aside?.type === 'event') {
417
+ title = eventMeta(aside.key).label;
418
+ subtitle = 'Evento do Claude Code';
419
+ content = <EventGroupDetail eventKey={aside.key} hooks={hooksByEvent[aside.key] || []} onSelectHook={setSel} />;
420
+ } else if (aside?.type === 'tool') {
421
+ title = 'Ferramenta';
422
+ subtitle = 'A chamada que os hooks cercam';
423
+ content = <ToolDetail />;
424
+ }
361
425
 
362
426
  return (
363
- <div className="flex flex-col h-full">
364
- {/* Header bar */}
365
- <div className="flex items-center justify-between px-5 py-3 bg-gray-900 border-b border-gray-800 flex-shrink-0">
427
+ <div className="flex h-full flex-col">
428
+ <div className="flex shrink-0 flex-wrap items-center justify-between gap-3 border-b border-[var(--color-hair)] bg-[var(--color-ink)]/70 px-5 py-3 backdrop-blur">
366
429
  <div>
367
- <h1 className="text-white font-bold text-base leading-tight">Hook Inspector</h1>
368
- <p className="text-gray-400 text-xs mt-0.5">
369
- Lifecycle timeline — {totalHooks} hooks across {eventCount} event types. Click a node to inspect.
430
+ <h1 className="text-base font-semibold leading-tight text-[var(--color-fg)]">Hooks</h1>
431
+ <p className="mt-0.5 text-sm text-[var(--color-dim)]">
432
+ {fileCount} {fileCount === 1 ? 'arquivo' : 'arquivos'}, {registered} {registered === 1 ? 'registro' : 'registros'} em eventos
433
+ {unregistered > 0 ? `, ${unregistered} não ${unregistered === 1 ? 'registrado' : 'registrados'}` : ''}. Clique num hook para ler.
370
434
  </p>
371
435
  </div>
372
- <div className="flex items-center gap-3">
373
- {/* Legend */}
374
- <div className="flex flex-wrap gap-2">
375
- {Object.entries(EVENT_COLORS).map(([key, { color, label }]) => (
376
- <div key={key} className="flex items-center gap-1.5">
377
- <span className="inline-block w-2.5 h-2.5 rounded-full" style={{ background: color }} />
378
- <span className="text-gray-400 text-[10px] hidden lg:inline">{label}</span>
379
- </div>
380
- ))}
381
- </div>
436
+ <div className="flex flex-wrap gap-x-3 gap-y-1">
437
+ {Object.entries(EVENTS).map(([key, { color, label }]) => (
438
+ <span key={key} className="flex items-center gap-1.5 text-xs text-[var(--color-dim)]">
439
+ <span className="inline-block h-2 w-2 rounded-full" style={{ background: color }} />
440
+ {label}
441
+ </span>
442
+ ))}
382
443
  </div>
383
444
  </div>
384
445
 
385
- {/* Flow graph */}
386
- <div className="flex-1 relative">
387
- <FlowGraph
388
- nodes={nodes}
389
- edges={edges}
390
- nodeTypes={nodeTypes}
391
- onNodeClick={handleNodeClick}
392
- fitView
393
- />
394
-
395
- {/* Floating legend / instructions */}
396
- <div className="absolute top-3 left-3 bg-gray-900/90 border border-gray-700 rounded-lg p-3 text-xs space-y-2 z-10 max-w-[200px]">
397
- <p className="text-gray-400 font-semibold uppercase tracking-wide text-[10px] mb-1">Flow Key</p>
398
- <div className="flex items-center gap-2">
399
- <span className="inline-block w-6 border-t-2 border-yellow-400 border-dashed" />
400
- <span className="text-gray-400 text-[10px]">conversation loop</span>
401
- </div>
402
- <div className="flex items-center gap-2">
403
- <span className="inline-block w-2 h-2 rounded" style={{ background: '#374151', border: '1.5px solid #6b7280' }} />
404
- <span className="text-gray-400 text-[10px]">tool execution</span>
405
- </div>
406
- <div className="flex items-center gap-2">
407
- <span className="px-1.5 py-0.5 rounded text-[8px] font-bold"
408
- style={{ background: '#1e40af', color: '#93c5fd', border: '1px solid #3b82f6' }}>
409
- can BLOCK
410
- </span>
411
- <span className="text-gray-400 text-[10px]">pre-tool only</span>
412
- </div>
446
+ <div className="relative flex-1">
447
+ <FlowGraph nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodeClick={handleNodeClick} fitView />
448
+
449
+ <div className="absolute left-3 top-3 z-10 max-w-[240px] space-y-2 rounded-xl bg-[var(--color-ink)]/85 p-3 text-xs text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)] backdrop-blur">
450
+ <p className="text-[var(--color-faint)]">Como ler</p>
451
+ <p>A linha de cima é a ordem de uma rodada: prompt, ferramenta, fim. A de baixo são eventos fora dessa rodada.</p>
452
+ <p>À direita de cada hook, as ferramentas que ele observa.</p>
413
453
  <div className="flex items-center gap-2">
414
- <span className="text-gray-500 text-[10px] italic">teammate-idle</span>
415
- <span className="text-gray-500 text-[10px]">= disconnected</span>
454
+ <span className="inline-block w-5 border-t border-dashed" style={{ borderColor: '#e0af68' }} />
455
+ <span>a conversa continua</span>
416
456
  </div>
417
457
  </div>
418
-
419
- {/* Hook count badge */}
420
- <div className="absolute top-3 right-3 bg-gray-900/90 border border-gray-700 rounded-lg px-3 py-1.5 text-xs text-gray-400 z-10">
421
- {totalHooks} hooks · {eventCount} events
422
- </div>
423
458
  </div>
424
459
 
425
- <DetailPanel open={panel.open} title={panel.title} onClose={closePanel}>
426
- {panel.content}
460
+ <DetailPanel open={Boolean(content)} title={title} subtitle={subtitle} onClose={closePanel}>
461
+ {content}
427
462
  </DetailPanel>
428
463
  </div>
429
464
  );