@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.
- package/CLAUDE.md +20 -9
- 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 +4 -3
- package/framework/MORPH.md +10 -9
- package/framework/agents.json +43 -26
- package/framework/docs/CLI.md +7 -5
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/hooks/README.md +1 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
- package/framework/hooks/claude-code/core/worktree-create.js +22 -6
- package/framework/hooks/shared/git-paths.js +38 -5
- package/framework/mcp/registry.json +58 -66
- 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 +59 -25
- 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 +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +48 -24
- package/framework/skills/core/morph-media/SKILL.md +2 -2
- package/framework/skills/core/morph-plan/SKILL.md +10 -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 +7 -6
- package/framework/standards/PROVENANCE.json +4 -4
- package/framework/standards/STANDARDS.json +9 -9
- 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/dotnet/testing.md +4 -2
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
- package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
- package/framework/standards/frontend/nextjs/testing.md +4 -2
- package/framework/standards/integration/mcp/mcp-tools.md +64 -126
- 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 +12 -6
- 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/commands/project/worktree.js +60 -4
- package/src/commands/state/gate-check.js +5 -0
- package/src/commands/tasks/dag.js +27 -4
- package/src/commands/validation/verify.js +22 -38
- 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/risk-detector.js +13 -0
- package/src/lib/tasks/tasks-json-parser.js +21 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
- package/src/lib/worktree/env-allocator.js +9 -3
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +108 -13
- 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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import {
|
|
4
|
+
ChatCircleText, Question, BookOpenText, PaperPlaneTilt, ArrowBendDownLeft, CheckSquare, ShieldCheck, Flag, WarningOctagon,
|
|
5
|
+
PencilSimpleLine, Eye, TerminalWindow, Wrench,
|
|
6
|
+
} from '@phosphor-icons/react';
|
|
7
|
+
import { useMorphQuery } from '../hooks/useMorphQuery.js';
|
|
8
|
+
import EntityLink from './EntityLink.jsx';
|
|
9
|
+
import { apiBase, featureHref } from '../lib/project.js';
|
|
10
|
+
import { clock, dayLabel } from '../lib/format.js';
|
|
11
|
+
import { phaseMeta } from '../lib/phases.js';
|
|
12
|
+
|
|
13
|
+
export const KIND_META = {
|
|
14
|
+
prompt: { label: 'Você', icon: ChatCircleText },
|
|
15
|
+
question: { label: 'Perguntas', icon: Question },
|
|
16
|
+
skill: { label: 'Skills', icon: BookOpenText },
|
|
17
|
+
dispatch: { label: 'Despachos', icon: PaperPlaneTilt },
|
|
18
|
+
handback: { label: 'Retornos', icon: ArrowBendDownLeft },
|
|
19
|
+
task: { label: 'Tasks', icon: CheckSquare },
|
|
20
|
+
verify: { label: 'Verify', icon: ShieldCheck },
|
|
21
|
+
gate: { label: 'Gates', icon: Flag },
|
|
22
|
+
failure: { label: 'Falhas', icon: WarningOctagon },
|
|
23
|
+
write: { label: 'Escritas', icon: PencilSimpleLine },
|
|
24
|
+
read: { label: 'Leituras', icon: Eye },
|
|
25
|
+
command: { label: 'Comandos', icon: TerminalWindow },
|
|
26
|
+
tool: { label: 'Outras tools', icon: Wrench },
|
|
27
|
+
};
|
|
28
|
+
const DEFAULT = ['prompt', 'question', 'skill', 'dispatch', 'handback', 'task', 'verify', 'gate', 'failure'];
|
|
29
|
+
|
|
30
|
+
const NODE_TONE = { pass: 'text-[var(--color-ok)]', fail: 'text-[var(--color-bad)]', skip: 'text-[var(--color-faint)]' };
|
|
31
|
+
|
|
32
|
+
function Line({ item, showFeature, project }) {
|
|
33
|
+
const Icon = KIND_META[item.kind]?.icon || Wrench;
|
|
34
|
+
const tone = item.status === 'fail' ? 'text-[var(--color-bad)]' : item.status === 'ok' ? 'text-[var(--color-ok)]' : 'text-[var(--color-dim)]';
|
|
35
|
+
return (
|
|
36
|
+
<li className="grid grid-cols-[3rem_1.25rem_minmax(0,1fr)] gap-3 py-2.5">
|
|
37
|
+
<time className="pt-px font-mono text-xs tabular-nums text-[var(--color-faint)]" dateTime={item.ts}>{clock(item.ts)}</time>
|
|
38
|
+
<Icon weight="light" size={17} className={tone} aria-hidden="true" />
|
|
39
|
+
<div className="min-w-0">
|
|
40
|
+
<p className="text-sm text-[var(--color-fg)]">
|
|
41
|
+
{item.refs?.persona ? (
|
|
42
|
+
<>{item.title.replace(`morph-${item.refs.persona}`, '')}<EntityLink kind="persona" id={item.refs.persona}>morph-{item.refs.persona}</EntityLink></>
|
|
43
|
+
) : item.refs?.skill ? (
|
|
44
|
+
<>Carregou a skill <EntityLink kind="skill" id={item.refs.skill} /></>
|
|
45
|
+
) : item.title}
|
|
46
|
+
{showFeature && item.feature && (
|
|
47
|
+
<Link to={featureHref(project, item.feature)} className="ml-2 text-xs text-[var(--color-faint)] hover:text-[var(--color-dim)]">{item.feature}</Link>
|
|
48
|
+
)}
|
|
49
|
+
{item.phase && (
|
|
50
|
+
<span className="ml-2 text-xs" style={{ color: phaseMeta(item.phase).color }}>{phaseMeta(item.phase).label.toLowerCase()}</span>
|
|
51
|
+
)}
|
|
52
|
+
{item.agent && item.kind !== 'handback' && (
|
|
53
|
+
<span className="ml-2 text-xs text-[var(--color-faint)]">por {item.agent}</span>
|
|
54
|
+
)}
|
|
55
|
+
</p>
|
|
56
|
+
{item.detail && <p className="mt-0.5 line-clamp-2 break-words text-sm text-[var(--color-dim)]">{item.detail}</p>}
|
|
57
|
+
{item.nodes?.length > 0 && (
|
|
58
|
+
<p className="mt-1 flex flex-wrap gap-x-3 gap-y-1 font-mono text-xs">
|
|
59
|
+
{item.nodes.map((n) => (
|
|
60
|
+
<span key={n.name} className={NODE_TONE[n.status] || 'text-[var(--color-dim)]'}>{n.name} {n.status}</span>
|
|
61
|
+
))}
|
|
62
|
+
</p>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
</li>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Group consecutive items by calendar day, newest day first. */
|
|
70
|
+
function byDay(items) {
|
|
71
|
+
const groups = [];
|
|
72
|
+
for (const it of items) {
|
|
73
|
+
const key = (it.ts || '').slice(0, 10);
|
|
74
|
+
const last = groups[groups.length - 1];
|
|
75
|
+
if (last && last.key === key) last.items.push(it);
|
|
76
|
+
else groups.push({ key, ts: it.ts, items: [it] });
|
|
77
|
+
}
|
|
78
|
+
return groups;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function FeedView({ items, kinds, onToggleKind, showFeature, onMore, hasMore, total, project }) {
|
|
82
|
+
return (
|
|
83
|
+
<div>
|
|
84
|
+
<div className="mb-5 flex flex-wrap gap-2" role="group" aria-label="Tipos de evento">
|
|
85
|
+
{Object.entries(KIND_META).map(([kind, meta]) => {
|
|
86
|
+
const on = kinds.includes(kind);
|
|
87
|
+
return (
|
|
88
|
+
<button
|
|
89
|
+
key={kind}
|
|
90
|
+
type="button"
|
|
91
|
+
aria-pressed={on}
|
|
92
|
+
onClick={() => onToggleKind(kind)}
|
|
93
|
+
className={[
|
|
94
|
+
'fluid rounded-full px-3 py-1 text-xs transition-colors active:scale-[0.98]',
|
|
95
|
+
on ? 'bg-white/[0.09] text-[var(--color-fg)] shadow-[inset_0_0_0_1px_var(--color-hair-strong)]' : 'text-[var(--color-faint)] shadow-[inset_0_0_0_1px_var(--color-hair)] hover:text-[var(--color-dim)]',
|
|
96
|
+
].join(' ')}
|
|
97
|
+
>
|
|
98
|
+
{meta.label}
|
|
99
|
+
</button>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</div>
|
|
103
|
+
{items.length === 0 && (
|
|
104
|
+
<p className="text-sm text-[var(--color-dim)]">
|
|
105
|
+
Nada registrado com esses filtros. A telemetria vem de <code className="font-mono">.morph/logs/events.jsonl</code>, gravada pelos hooks enquanto o Claude trabalha.
|
|
106
|
+
</p>
|
|
107
|
+
)}
|
|
108
|
+
{byDay(items).map((g) => (
|
|
109
|
+
<section key={g.key} className="mb-6">
|
|
110
|
+
<h3 className="sticky top-0 z-10 -mx-1 bg-[var(--color-core)] px-1 py-1.5 text-xs font-medium text-[var(--color-dim)] first-letter:uppercase">{dayLabel(g.ts)}</h3>
|
|
111
|
+
<ul className="divide-y divide-[var(--color-hair)]">
|
|
112
|
+
{g.items.map((it) => <Line key={it.id} item={it} showFeature={showFeature} project={project} />)}
|
|
113
|
+
</ul>
|
|
114
|
+
</section>
|
|
115
|
+
))}
|
|
116
|
+
{hasMore && (
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={onMore}
|
|
120
|
+
className="fluid rounded-full px-4 py-2 text-sm text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)] transition-colors hover:text-[var(--color-fg)] active:scale-[0.98]"
|
|
121
|
+
>
|
|
122
|
+
Carregar anteriores
|
|
123
|
+
</button>
|
|
124
|
+
)}
|
|
125
|
+
{typeof total === 'number' && items.length > 0 && (
|
|
126
|
+
<p className="mt-3 text-xs text-[var(--color-faint)]">{items.length} de {total} eventos</p>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Live activity feed. The first page follows the server's `morph:changed`
|
|
134
|
+
* pushes; older pages are fetched on demand and appended.
|
|
135
|
+
*/
|
|
136
|
+
export default function ActivityFeed({ project, feature, showFeature = false }) {
|
|
137
|
+
const [kinds, setKinds] = useState(DEFAULT);
|
|
138
|
+
const [older, setOlder] = useState([]);
|
|
139
|
+
const [cursor, setCursor] = useState(null);
|
|
140
|
+
const params = new URLSearchParams({ kinds: kinds.join(','), limit: '80' });
|
|
141
|
+
if (feature) params.set('feature', feature);
|
|
142
|
+
const { data, error } = useMorphQuery(`${apiBase(project)}/activity?${params}`, { scope: 'activity', project });
|
|
143
|
+
|
|
144
|
+
useEffect(() => { setOlder([]); setCursor(null); }, [feature, kinds.join(',')]);
|
|
145
|
+
|
|
146
|
+
const next = cursor === null ? data?.next : cursor;
|
|
147
|
+
const loadMore = async () => {
|
|
148
|
+
if (!next) return;
|
|
149
|
+
const p = new URLSearchParams(params);
|
|
150
|
+
p.set('before', next);
|
|
151
|
+
const res = await fetch(`${apiBase(project)}/activity?${p}`);
|
|
152
|
+
const page = await res.json();
|
|
153
|
+
setOlder((o) => [...o, ...page.items]);
|
|
154
|
+
setCursor(page.next || '');
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (error) return <p className="text-sm text-[var(--color-bad)]">Não consegui ler a atividade: {error}</p>;
|
|
158
|
+
if (!data) return <p className="text-sm text-[var(--color-faint)]">Lendo a telemetria…</p>;
|
|
159
|
+
const toggle = (k) => setKinds((ks) => (ks.includes(k) ? ks.filter((x) => x !== k) : [...ks, k]));
|
|
160
|
+
return (
|
|
161
|
+
<FeedView
|
|
162
|
+
items={[...data.items, ...older]}
|
|
163
|
+
kinds={kinds}
|
|
164
|
+
onToggleKind={toggle}
|
|
165
|
+
showFeature={showFeature}
|
|
166
|
+
onMore={loadMore}
|
|
167
|
+
hasMore={Boolean(next)}
|
|
168
|
+
total={data.total}
|
|
169
|
+
project={project}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import morphData from 'virtual:morph-data';
|
|
4
|
+
import { MagnifyingGlass } from '@phosphor-icons/react';
|
|
5
|
+
import { entityHref } from './EntityLink.jsx';
|
|
6
|
+
import { tocOf } from '../pages/Guide.jsx';
|
|
7
|
+
import { useMorphQuery } from '../hooks/useMorphQuery.js';
|
|
8
|
+
import { apiBase, projectHref, featureHref } from '../lib/project.js';
|
|
9
|
+
|
|
10
|
+
const KIND_LABEL = {
|
|
11
|
+
page: 'Página', project: 'Projeto', feature: 'Feature', task: 'Task', skill: 'Skill', persona: 'Persona',
|
|
12
|
+
standard: 'Standard', rule: 'Rule', hook: 'Hook', guide: 'Guia', rubric: 'Rubrica',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const PAGES = [
|
|
16
|
+
['Portfólio', '/'], ['Guia', '/guide'], ['Fases', '/phases'], ['Skills', '/skills'],
|
|
17
|
+
['Personas', '/agents'], ['Standards', '/standards'], ['Rules', '/rules'], ['Hooks', '/hooks'], ['Rubricas', '/rubrics'],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/** Everything the framework knows that has a page — built once. */
|
|
21
|
+
function knowledgeIndex() {
|
|
22
|
+
const out = PAGES.map(([title, to]) => ({ kind: 'page', title, to, hay: title }));
|
|
23
|
+
for (const s of morphData.skills || []) out.push({ kind: 'skill', title: s.id, hint: s.description, to: entityHref('skill', s.id), hay: `${s.id} ${s.description}` });
|
|
24
|
+
for (const a of morphData.agents?.list || []) out.push({ kind: 'persona', title: `morph-${a.id}`, hint: a.title, to: entityHref('persona', a.id), hay: `${a.id} ${a.title} ${(a.keywords || []).join(' ')}` });
|
|
25
|
+
for (const s of morphData.standards?.list || []) out.push({ kind: 'standard', title: s.name, hint: s.id, to: entityHref('standard', s.id), hay: `${s.name} ${s.id} ${(s.aliases || []).join(' ')} ${(s.tags || []).join(' ')}` });
|
|
26
|
+
for (const r of morphData.rules || []) out.push({ kind: 'rule', title: r.name, hint: (r.paths || []).join(', '), to: entityHref('rule', r.name), hay: `${r.name} ${(r.paths || []).join(' ')}` });
|
|
27
|
+
const hooks = new Set();
|
|
28
|
+
for (const h of morphData.hooks || []) {
|
|
29
|
+
if (hooks.has(h.name)) continue;
|
|
30
|
+
hooks.add(h.name);
|
|
31
|
+
out.push({ kind: 'hook', title: h.name, hint: h.description, to: entityHref('hook', h.name), hay: `${h.name} ${h.description || ''}` });
|
|
32
|
+
}
|
|
33
|
+
for (const d of morphData.docs?.guide || []) {
|
|
34
|
+
for (const h of tocOf(d).filter((x) => x.depth >= 2)) {
|
|
35
|
+
out.push({ kind: 'guide', title: h.text, hint: d.title, to: `/guide/${d.id}#${h.slug}`, hay: `${h.text} ${d.title}` });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const r of morphData.docs?.rubrics || []) out.push({ kind: 'rubric', title: r.title, to: `/rubrics?sel=${encodeURIComponent(`dim:${r.id}`)}`, hay: r.title });
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function projectIndex(portfolio) {
|
|
43
|
+
return (portfolio?.projects || []).map((p) => ({ kind: 'project', title: p.name, hint: p.dir, to: projectHref(p.id), hay: `${p.name} ${p.dir}` }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Features and their tasks. `projectName` is set for another project's
|
|
48
|
+
* features, so a hit says where it lives.
|
|
49
|
+
*/
|
|
50
|
+
function featureIndex(project, features, projectName = null) {
|
|
51
|
+
const out = [];
|
|
52
|
+
const where = (text) => (projectName ? (text ? `${projectName}: ${text}` : projectName) : text);
|
|
53
|
+
for (const f of features || []) {
|
|
54
|
+
out.push({ kind: 'feature', title: f.name, hint: where(f.description), to: featureHref(project, f.name), hay: `${f.name} ${f.description || ''} ${projectName || ''}` });
|
|
55
|
+
for (const t of f.taskIndex || []) {
|
|
56
|
+
out.push({ kind: 'task', title: `${t.id} ${t.title}`, hint: where(f.name), to: featureHref(project, f.name, '?tab=tasks'), hay: `${t.id} ${t.title} ${f.name} ${projectName || ''}` });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The active features of the other projects, from the portfolio's summaries. */
|
|
63
|
+
function otherProjectsIndex(portfolio, project) {
|
|
64
|
+
return (portfolio?.projects || [])
|
|
65
|
+
.filter((p) => p.id !== project && p.summary)
|
|
66
|
+
.flatMap((p) => featureIndex(p.id, p.summary.activeFeatures, p.name));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Every query word must appear; a title that starts with the query ranks first. */
|
|
70
|
+
export function searchIndex(index, query, limit = 40) {
|
|
71
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
72
|
+
if (!words.length) return [];
|
|
73
|
+
const scored = [];
|
|
74
|
+
for (const item of index) {
|
|
75
|
+
const hay = item.hay.toLowerCase();
|
|
76
|
+
if (!words.every((w) => hay.includes(w))) continue;
|
|
77
|
+
const title = item.title.toLowerCase();
|
|
78
|
+
const score = (title.startsWith(words[0]) ? 0 : title.includes(words[0]) ? 1 : 2) + (item.kind === 'page' ? -1 : 0);
|
|
79
|
+
scored.push({ item, score });
|
|
80
|
+
}
|
|
81
|
+
return scored.sort((a, b) => a.score - b.score || a.item.title.length - b.item.title.length).slice(0, limit).map((s) => s.item);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default function CommandPalette({ open, onClose, portfolio, project }) {
|
|
85
|
+
const navigate = useNavigate();
|
|
86
|
+
const [query, setQuery] = useState('');
|
|
87
|
+
const [active, setActive] = useState(0);
|
|
88
|
+
const inputRef = useRef(null);
|
|
89
|
+
const knowledge = useMemo(knowledgeIndex, []);
|
|
90
|
+
const { data } = useMorphQuery(open && project ? `${apiBase(project)}/features` : null, { project });
|
|
91
|
+
const index = useMemo(
|
|
92
|
+
() => [...featureIndex(project, data?.features), ...otherProjectsIndex(portfolio, project), ...projectIndex(portfolio), ...knowledge],
|
|
93
|
+
[data, knowledge, portfolio, project],
|
|
94
|
+
);
|
|
95
|
+
const results = useMemo(() => searchIndex(index, query), [index, query]);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (open) { setQuery(''); setActive(0); setTimeout(() => inputRef.current?.focus(), 0); }
|
|
99
|
+
}, [open]);
|
|
100
|
+
useEffect(() => { setActive(0); }, [query]);
|
|
101
|
+
|
|
102
|
+
if (!open) return null;
|
|
103
|
+
const go = (item) => { onClose(); navigate(item.to); };
|
|
104
|
+
const onKey = (e) => {
|
|
105
|
+
if (e.key === 'Escape') onClose();
|
|
106
|
+
else if (e.key === 'ArrowDown') { e.preventDefault(); setActive((i) => Math.min(i + 1, results.length - 1)); }
|
|
107
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((i) => Math.max(i - 1, 0)); }
|
|
108
|
+
else if (e.key === 'Enter' && results[active]) go(results[active]);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center bg-black/60 px-4 pt-[12vh] backdrop-blur-sm" onMouseDown={onClose} role="presentation">
|
|
113
|
+
<div role="dialog" aria-modal="true" aria-label="Busca" className="bezel w-full max-w-2xl" onMouseDown={(e) => e.stopPropagation()}>
|
|
114
|
+
<div className="bezel-core overflow-hidden">
|
|
115
|
+
<label className="flex items-center gap-3 border-b border-[var(--color-hair)] px-5 py-4">
|
|
116
|
+
<MagnifyingGlass weight="light" size={18} className="text-[var(--color-faint)]" />
|
|
117
|
+
<span className="sr-only">Buscar em tudo</span>
|
|
118
|
+
<input
|
|
119
|
+
ref={inputRef}
|
|
120
|
+
value={query}
|
|
121
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
122
|
+
onKeyDown={onKey}
|
|
123
|
+
placeholder="Buscar features, tasks, skills, personas, standards, seções do guia…"
|
|
124
|
+
className="w-full bg-transparent text-base text-[var(--color-fg)] outline-none focus-visible:outline-none placeholder:text-[var(--color-faint)]"
|
|
125
|
+
aria-controls="palette-results"
|
|
126
|
+
aria-activedescendant={results[active] ? `palette-${active}` : undefined}
|
|
127
|
+
/>
|
|
128
|
+
<kbd className="rounded-md px-1.5 font-mono text-[11px] text-[var(--color-faint)] shadow-[inset_0_0_0_1px_var(--color-hair)]">Esc</kbd>
|
|
129
|
+
</label>
|
|
130
|
+
<ul id="palette-results" role="listbox" className="max-h-[55vh] overflow-y-auto p-2">
|
|
131
|
+
{query && results.length === 0 && <li className="px-3 py-6 text-center text-sm text-[var(--color-dim)]">Nada encontrado para “{query}”.</li>}
|
|
132
|
+
{!query && <li className="px-3 py-6 text-center text-sm text-[var(--color-faint)]">Digite para buscar. Setas navegam, Enter abre.</li>}
|
|
133
|
+
{results.map((r, i) => (
|
|
134
|
+
<li
|
|
135
|
+
key={`${r.kind}:${r.to}:${r.title}`}
|
|
136
|
+
id={`palette-${i}`}
|
|
137
|
+
role="option"
|
|
138
|
+
aria-selected={i === active}
|
|
139
|
+
onMouseEnter={() => setActive(i)}
|
|
140
|
+
onClick={() => go(r)}
|
|
141
|
+
className={['flex cursor-pointer items-baseline gap-3 rounded-lg px-3 py-2', i === active ? 'bg-white/[0.07]' : ''].join(' ')}
|
|
142
|
+
>
|
|
143
|
+
<span className="w-16 shrink-0 text-xs text-[var(--color-faint)]">{KIND_LABEL[r.kind]}</span>
|
|
144
|
+
<span className="min-w-0 flex-1">
|
|
145
|
+
<span className="block truncate text-sm text-[var(--color-fg)]">{r.title}</span>
|
|
146
|
+
{r.hint && <span className="block truncate text-xs text-[var(--color-dim)]">{r.hint}</span>}
|
|
147
|
+
</span>
|
|
148
|
+
</li>
|
|
149
|
+
))}
|
|
150
|
+
</ul>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -1,11 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
)
|
|
11
|
-
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { X } from '@phosphor-icons/react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The reading drawer of the knowledge pages. Wide enough for rendered markdown,
|
|
6
|
+
* closes on Escape, and sits over the graph without a backdrop so the graph
|
|
7
|
+
* stays visible beside it.
|
|
8
|
+
*/
|
|
9
|
+
export default function DetailPanel({ open, title, subtitle, onClose, children }) {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!open) return undefined;
|
|
12
|
+
const onKey = (e) => { if (e.key === 'Escape') onClose?.(); };
|
|
13
|
+
window.addEventListener('keydown', onKey);
|
|
14
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
15
|
+
}, [open, onClose]);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<aside
|
|
19
|
+
aria-hidden={!open}
|
|
20
|
+
aria-label={title}
|
|
21
|
+
className={[
|
|
22
|
+
'fluid fixed inset-y-0 right-0 z-40 flex w-full max-w-[44rem] flex-col p-3 transition-transform',
|
|
23
|
+
open ? 'translate-x-0' : 'pointer-events-none translate-x-full',
|
|
24
|
+
].join(' ')}
|
|
25
|
+
>
|
|
26
|
+
<div className="bezel flex min-h-0 flex-1 flex-col bg-[var(--color-ink)]/80 backdrop-blur-xl">
|
|
27
|
+
<div className="bezel-core flex min-h-0 flex-1 flex-col">
|
|
28
|
+
<header className="flex items-start justify-between gap-4 border-b border-[var(--color-hair)] px-6 py-4">
|
|
29
|
+
<div className="min-w-0">
|
|
30
|
+
<h2 className="truncate text-lg font-semibold tracking-tight">{title}</h2>
|
|
31
|
+
{subtitle && <p className="mt-0.5 truncate text-sm text-[var(--color-dim)]">{subtitle}</p>}
|
|
32
|
+
</div>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
onClick={onClose}
|
|
36
|
+
aria-label="Fechar painel"
|
|
37
|
+
className="fluid flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white/[0.05] text-[var(--color-dim)] transition-colors hover:bg-white/[0.1] hover:text-[var(--color-fg)]"
|
|
38
|
+
>
|
|
39
|
+
<X weight="light" size={16} />
|
|
40
|
+
</button>
|
|
41
|
+
</header>
|
|
42
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-5">{children}</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</aside>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Link, useSearchParams } from 'react-router-dom';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where each kind of thing lives in the app. The ONE map of entity → URL, so a
|
|
6
|
+
* persona, standard or skill reads as a link everywhere it is mentioned.
|
|
7
|
+
* A feature belongs to a project: its links come from `featureHref` (lib/project.js).
|
|
8
|
+
*/
|
|
9
|
+
export function entityHref(kind, id) {
|
|
10
|
+
const q = (page) => `/${page}?sel=${encodeURIComponent(id)}`;
|
|
11
|
+
switch (kind) {
|
|
12
|
+
case 'persona': return q('agents');
|
|
13
|
+
case 'skill': return q('skills');
|
|
14
|
+
case 'standard': return q('standards');
|
|
15
|
+
case 'rule': return q('rules');
|
|
16
|
+
case 'hook': return q('hooks');
|
|
17
|
+
case 'phase': return q('phases');
|
|
18
|
+
case 'guide': return `/guide/${encodeURIComponent(id)}`;
|
|
19
|
+
case 'rubric': return q('rubrics');
|
|
20
|
+
default: return '#';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `standards/backend/dotnet/testing.md` (as tasks.json cites it) → the registry `path`. */
|
|
25
|
+
export function standardKey(ref) {
|
|
26
|
+
return String(ref).replace(/^\.?\/?(\.morph\/framework\/)?standards\//, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TONE = 'text-[var(--color-phase-proposal)] hover:underline underline-offset-2';
|
|
30
|
+
|
|
31
|
+
export default function EntityLink({ kind, id, children, className = '', chip = false, plain = false, title }) {
|
|
32
|
+
const cls = chip
|
|
33
|
+
? `inline-flex items-center rounded-full px-2 py-0.5 font-mono text-[11px] text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)] transition-colors hover:text-[var(--color-fg)] hover:shadow-[inset_0_0_0_1px_var(--color-hair-strong)] ${className}`
|
|
34
|
+
: plain ? className : `${TONE} ${className}`;
|
|
35
|
+
return <Link to={entityHref(kind, id)} className={cls} title={title}>{children ?? id}</Link>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The selected item of a knowledge page, kept in `?sel=` so a panel can be
|
|
40
|
+
* linked, bookmarked and reached from anywhere with EntityLink.
|
|
41
|
+
*
|
|
42
|
+
* @returns {[string|null, (id: string|null) => void]}
|
|
43
|
+
*/
|
|
44
|
+
export function useSelection() {
|
|
45
|
+
const [params, setParams] = useSearchParams();
|
|
46
|
+
const sel = params.get('sel');
|
|
47
|
+
const setSel = useCallback((id) => {
|
|
48
|
+
setParams((p) => {
|
|
49
|
+
const n = new URLSearchParams(p);
|
|
50
|
+
if (id) n.set('sel', id); else n.delete('sel');
|
|
51
|
+
return n;
|
|
52
|
+
}, { replace: false });
|
|
53
|
+
}, [setParams]);
|
|
54
|
+
return [sel, setSel];
|
|
55
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Component } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Catches a page that throws while rendering and shows the error in place of
|
|
5
|
+
* the page, so the rest of the layout (the sidebar) keeps working.
|
|
6
|
+
*
|
|
7
|
+
* Without it one broken page unmounts the whole tree: 8.38.0 opened on
|
|
8
|
+
* /agents, which threw a ReferenceError, and the dashboard was a black screen.
|
|
9
|
+
*
|
|
10
|
+
* Mount it with `key={location.pathname}`: a boundary that already caught an
|
|
11
|
+
* error stays in fallback until it remounts, and the key remounts it when the
|
|
12
|
+
* sidebar navigates to another page.
|
|
13
|
+
*/
|
|
14
|
+
export default class ErrorBoundary extends Component {
|
|
15
|
+
constructor(props) {
|
|
16
|
+
super(props);
|
|
17
|
+
this.state = { error: null };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static getDerivedStateFromError(error) {
|
|
21
|
+
return { error };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
componentDidCatch(error, info) {
|
|
25
|
+
console.error(`[dashboard] ${this.props.page || 'page'} failed to render:`, error, info?.componentStack);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render() {
|
|
29
|
+
const { error } = this.state;
|
|
30
|
+
if (!error) return this.props.children;
|
|
31
|
+
return (
|
|
32
|
+
<div role="alert" className="p-6 max-w-3xl">
|
|
33
|
+
<h2 className="text-red-400 text-sm font-semibold uppercase tracking-wide mb-2">
|
|
34
|
+
This page failed to render
|
|
35
|
+
</h2>
|
|
36
|
+
<p className="text-gray-300 text-sm mb-3">
|
|
37
|
+
<span className="font-mono text-gray-400">{this.props.page}</span> threw while rendering.
|
|
38
|
+
The other pages still work — pick one in the sidebar.
|
|
39
|
+
</p>
|
|
40
|
+
<pre className="p-3 bg-gray-900 border border-red-900 rounded text-xs text-red-300 whitespace-pre-wrap font-mono overflow-x-auto">
|
|
41
|
+
{error.name}: {error.message}
|
|
42
|
+
</pre>
|
|
43
|
+
{error.stack && (
|
|
44
|
+
<details className="mt-2">
|
|
45
|
+
<summary className="text-gray-500 text-xs cursor-pointer">Stack trace</summary>
|
|
46
|
+
<pre className="mt-1 p-3 bg-gray-950 border border-gray-800 rounded text-[10px] text-gray-500 whitespace-pre-wrap font-mono overflow-x-auto">
|
|
47
|
+
{error.stack}
|
|
48
|
+
</pre>
|
|
49
|
+
</details>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { ReactFlow, Controls, MiniMap, Background } from '@xyflow/react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { ReactFlow, Controls, MiniMap, Background } from '@xyflow/react';
|
|
2
|
+
|
|
3
|
+
// The React Flow attribution stays: hiding it is licensed to Pro subscribers only.
|
|
4
|
+
export default function FlowGraph({ nodes, edges, nodeTypes, onNodeClick, fitView = true, className = '' }) {
|
|
5
|
+
return (
|
|
6
|
+
<div className={`w-full h-full ${className}`} style={{ minHeight: '500px' }}>
|
|
7
|
+
<ReactFlow
|
|
8
|
+
nodes={nodes} edges={edges} nodeTypes={nodeTypes} onNodeClick={onNodeClick}
|
|
9
|
+
fitView={fitView} fitViewOptions={{ padding: 0.15, maxZoom: 1.1 }} minZoom={0.2}
|
|
10
|
+
|
|
11
|
+
defaultEdgeOptions={{ type: 'smoothstep', animated: false, style: { stroke: '#3a4250', strokeWidth: 1.5 } }}
|
|
12
|
+
>
|
|
13
|
+
<Background color="rgb(255 255 255 / 0.05)" gap={22} />
|
|
14
|
+
<Controls position="bottom-right" />
|
|
15
|
+
<MiniMap nodeColor={(node) => node.data?.color || '#4a5260'} maskColor="rgba(5, 5, 5, 0.72)" position="bottom-left" pannable zoomable />
|
|
16
|
+
</ReactFlow>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import morphData from 'virtual:morph-data';
|
|
2
|
+
import Markdown from './Markdown.jsx';
|
|
3
|
+
import EntityLink, { standardKey } from './EntityLink.jsx';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Names of things that have a page. Built once from the knowledge module.
|
|
7
|
+
*/
|
|
8
|
+
const skillIds = new Set((morphData.skills || []).map((s) => s.id));
|
|
9
|
+
const personaIds = new Set((morphData.agents?.list || []).map((a) => a.id));
|
|
10
|
+
const standardByKey = new Map();
|
|
11
|
+
for (const s of morphData.standards?.list || []) {
|
|
12
|
+
standardByKey.set(s.id, s.id);
|
|
13
|
+
standardByKey.set(s.path, s.id);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What an inline code span names, if it names something with a page:
|
|
18
|
+
* `morph-plan` or `/morph-plan` → skill, `morph-dotnet-senior` → persona,
|
|
19
|
+
* a standard id or path → standard.
|
|
20
|
+
*
|
|
21
|
+
* @returns {{ kind: string, id: string } | null}
|
|
22
|
+
*/
|
|
23
|
+
export function resolveMention(text) {
|
|
24
|
+
const t = String(text).trim();
|
|
25
|
+
const bare = t.replace(/^\//, '');
|
|
26
|
+
if (skillIds.has(bare)) return { kind: 'skill', id: bare };
|
|
27
|
+
const persona = /^morph-(.+)$/.exec(bare);
|
|
28
|
+
if (persona && personaIds.has(persona[1])) return { kind: 'persona', id: persona[1] };
|
|
29
|
+
const std = standardByKey.get(standardKey(t));
|
|
30
|
+
if (std) return { kind: 'standard', id: std };
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function InlineCode({ className, children, node, ...rest }) {
|
|
35
|
+
const text = Array.isArray(children) ? children.join('') : String(children ?? '');
|
|
36
|
+
const block = Boolean(className) || text.includes('\n');
|
|
37
|
+
if (!block) {
|
|
38
|
+
const hit = resolveMention(text);
|
|
39
|
+
if (hit) {
|
|
40
|
+
return (
|
|
41
|
+
<EntityLink kind={hit.kind} id={hit.id} className="no-underline">
|
|
42
|
+
<code>{text}</code>
|
|
43
|
+
</EntityLink>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return <code className={className} {...rest}>{children}</code>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Markdown for framework and feature text: code spans that name a skill, persona or standard become links. */
|
|
51
|
+
export default function KnowledgeMarkdown({ components, ...props }) {
|
|
52
|
+
return <Markdown {...props} components={{ code: InlineCode, ...components }} />;
|
|
53
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import ReactMarkdown from 'react-markdown';
|
|
2
|
+
import remarkGfm from 'remark-gfm';
|
|
3
|
+
import rehypeSlug from 'rehype-slug';
|
|
4
|
+
import { splitFrontmatter } from '../lib/format.js';
|
|
5
|
+
|
|
6
|
+
const REMARK = [remarkGfm];
|
|
7
|
+
const REHYPE = [rehypeSlug];
|
|
8
|
+
|
|
9
|
+
/** Frontmatter keys worth showing, in the order a reader wants them. */
|
|
10
|
+
const META_ORDER = ['name', 'description', 'when-to-invoke', 'user-invokable', 'stacks', 'version', 'cliVersion'];
|
|
11
|
+
|
|
12
|
+
function MetaBlock({ meta }) {
|
|
13
|
+
const keys = Object.keys(meta).sort((a, b) => {
|
|
14
|
+
const ia = META_ORDER.indexOf(a); const ib = META_ORDER.indexOf(b);
|
|
15
|
+
return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
|
|
16
|
+
});
|
|
17
|
+
return (
|
|
18
|
+
<dl className="mb-6 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1.5 rounded-xl bg-white/[0.03] px-4 py-3 text-sm shadow-[inset_0_0_0_1px_var(--color-hair)]">
|
|
19
|
+
{keys.map((k) => (
|
|
20
|
+
<div key={k} className="contents">
|
|
21
|
+
<dt className="font-mono text-xs leading-6 text-[var(--color-faint)]">{k}</dt>
|
|
22
|
+
<dd className="leading-6 text-[var(--color-dim)]">{meta[k] || '—'}</dd>
|
|
23
|
+
</div>
|
|
24
|
+
))}
|
|
25
|
+
</dl>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Rendered markdown for every long text in the app: feature artifacts, skills,
|
|
31
|
+
* standards, the charter. Frontmatter is lifted into a metadata block instead
|
|
32
|
+
* of being printed as text.
|
|
33
|
+
*
|
|
34
|
+
* @param {{ text: string, components?: object, className?: string, size?: 'md'|'sm' }} props
|
|
35
|
+
* `sm` is for side panels, where the reading column is narrower.
|
|
36
|
+
*/
|
|
37
|
+
export default function Markdown({ text, components, className = '', size = 'md' }) {
|
|
38
|
+
const { meta, body } = splitFrontmatter(text || '');
|
|
39
|
+
return (
|
|
40
|
+
<article className={`prose-morph ${size === 'sm' ? 'prose-morph-sm' : ''} ${className}`}>
|
|
41
|
+
{meta && Object.keys(meta).length > 0 && <MetaBlock meta={meta} />}
|
|
42
|
+
<ReactMarkdown remarkPlugins={REMARK} rehypePlugins={REHYPE} components={components}>
|
|
43
|
+
{body}
|
|
44
|
+
</ReactMarkdown>
|
|
45
|
+
</article>
|
|
46
|
+
);
|
|
47
|
+
}
|