@polymorphism-tech/morph-spec 8.38.0 → 8.40.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.
- package/CLAUDE.md +19 -8
- package/README.md +2 -2
- package/bin/morph-spec.js +60 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -2
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +36 -25
- package/framework/docs/CLI.md +2 -2
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/mcp/registry.json +33 -2
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +9 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +5 -1
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +6 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +4 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +6 -5
- package/framework/standards/STANDARDS.json +3 -3
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/architecture/code-comments.md +52 -16
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +48 -117
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/project/doctor.js +3 -5
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +5 -0
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +59 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +2 -0
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +22 -0
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +57 -12
- package/src/utils/banner.js +1 -1
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- 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 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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,
|
|
28
|
-
const
|
|
100
|
+
const { eventKey, hooks, onSelectHook, selectedKey } = data;
|
|
101
|
+
const meta = eventMeta(eventKey);
|
|
29
102
|
|
|
30
103
|
return (
|
|
31
104
|
<div
|
|
32
|
-
className="
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
{
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
function ToolNode({ data }) {
|
|
151
|
+
function ToolNode() {
|
|
73
152
|
return (
|
|
74
|
-
<div
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
addEventNode('
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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',
|
|
181
|
-
edges.push(solid('e-up-ptu', 'user-prompt', 'pre-tool-use',
|
|
182
|
-
edges.push(solid('e-ptu-tool', 'pre-tool-use', '__tool__', '#
|
|
183
|
-
edges.push(solid('e-tool-pou', '__tool__', 'post-tool-use',
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
edges.push(solid('e-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
// ───
|
|
207
|
-
|
|
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
|
|
261
|
+
const matcher = matcherText(hook);
|
|
210
262
|
|
|
211
263
|
return (
|
|
212
|
-
<div className="text-sm text-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
<
|
|
226
|
-
<p className="
|
|
227
|
-
|
|
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
|
-
<
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
{imp
|
|
241
|
-
|
|
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
|
-
</
|
|
298
|
+
</Section>
|
|
245
299
|
)}
|
|
246
300
|
|
|
247
|
-
|
|
248
|
-
<div>
|
|
301
|
+
<Section title="Código">
|
|
249
302
|
<button
|
|
303
|
+
type="button"
|
|
250
304
|
onClick={() => setShowSource((v) => !v)}
|
|
251
|
-
className="w-full
|
|
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 ?
|
|
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
|
|
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
|
-
</
|
|
317
|
+
</Section>
|
|
261
318
|
</div>
|
|
262
319
|
);
|
|
263
320
|
}
|
|
264
321
|
|
|
265
|
-
|
|
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-
|
|
272
|
-
{
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
className="
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
</
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
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
|
-
|
|
330
|
-
const
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
|
364
|
-
|
|
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-
|
|
368
|
-
<p className="
|
|
369
|
-
|
|
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
|
|
373
|
-
{
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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="
|
|
415
|
-
<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={
|
|
426
|
-
{
|
|
460
|
+
<DetailPanel open={Boolean(content)} title={title} subtitle={subtitle} onClose={closePanel}>
|
|
461
|
+
{content}
|
|
427
462
|
</DetailPanel>
|
|
428
463
|
</div>
|
|
429
464
|
);
|