@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.
Files changed (227) hide show
  1. package/CLAUDE.md +19 -8
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +60 -43
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +3 -2
  8. package/framework/MORPH.md +1 -1
  9. package/framework/agents.json +36 -25
  10. package/framework/docs/CLI.md +2 -2
  11. package/framework/evals/rubrics/code-quality.md +4 -4
  12. package/framework/evals/rubrics/condensed.md +2 -2
  13. package/framework/mcp/registry.json +33 -2
  14. package/framework/rules/infrastructure-standards.md +0 -1
  15. package/framework/rules/morph-workflow.md +9 -4
  16. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  17. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  18. package/framework/skills/core/morph-apply/SKILL.md +5 -1
  19. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  20. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  21. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  22. package/framework/skills/core/morph-implement/SKILL.md +6 -1
  23. package/framework/skills/core/morph-media/SKILL.md +1 -1
  24. package/framework/skills/core/morph-plan/SKILL.md +4 -3
  25. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  26. package/framework/skills/core/morph-review/SKILL.md +1 -1
  27. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  28. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  29. package/framework/skills/core/morph-uiux/SKILL.md +6 -5
  30. package/framework/standards/STANDARDS.json +3 -3
  31. package/framework/standards/ai-agents/context-providers.md +16 -26
  32. package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
  33. package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
  34. package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
  35. package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
  36. package/framework/standards/ai-agents/modalities-vision.md +1 -18
  37. package/framework/standards/ai-agents/prompt-sources.md +0 -4
  38. package/framework/standards/architecture/code-comments.md +52 -16
  39. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
  40. package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
  41. package/framework/standards/integration/mcp/mcp-tools.md +48 -117
  42. package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
  43. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
  44. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
  45. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
  46. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
  47. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
  48. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
  49. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
  50. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
  51. package/framework/templates/code/dotnet/database/migration.cs +49 -83
  52. package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
  53. package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
  54. package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
  55. package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
  56. package/framework/templates/code/dotnet/jobs/job.cs +2 -97
  57. package/framework/templates/code/dotnet/test.cs +2 -87
  58. package/framework/templates/code/typescript/contracts.ts +140 -167
  59. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
  60. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
  61. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
  62. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
  63. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
  64. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
  65. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
  66. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
  67. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
  68. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
  69. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
  70. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
  71. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
  72. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
  73. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
  74. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
  75. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
  76. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
  77. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
  78. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
  79. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
  80. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
  81. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
  82. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
  83. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
  84. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
  85. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
  86. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
  87. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
  88. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
  89. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
  90. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
  91. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
  92. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
  93. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
  94. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
  95. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
  96. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
  97. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
  98. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
  99. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
  100. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
  101. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
  102. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
  103. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
  110. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
  111. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
  112. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
  113. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
  114. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
  115. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
  116. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
  117. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
  118. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
  119. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
  120. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
  121. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
  122. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
  123. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
  124. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
  127. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
  128. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
  129. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
  130. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
  131. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
  132. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
  134. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
  135. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
  136. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
  137. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
  138. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
  139. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
  140. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
  141. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
  142. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
  143. package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
  144. package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
  145. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
  146. package/framework/templates/integrations/asaas-client.cs +332 -387
  147. package/framework/templates/integrations/asaas-webhook.cs +290 -351
  148. package/framework/templates/integrations/neon-auth-config.cs +9 -105
  149. package/framework/templates/project/validate.js +97 -120
  150. package/package.json +2 -2
  151. package/src/commands/dashboard/dashboard.js +80 -53
  152. package/src/commands/mcp/mcp-setup.js +37 -9
  153. package/src/commands/project/doctor.js +3 -5
  154. package/src/commands/project/init.js +6 -3
  155. package/src/commands/project/install-plugin-cmd.js +1 -1
  156. package/src/commands/project/update.js +5 -0
  157. package/src/dashboard/app/App.jsx +80 -16
  158. package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
  159. package/src/dashboard/app/components/CommandPalette.jsx +155 -0
  160. package/src/dashboard/app/components/DetailPanel.jsx +47 -11
  161. package/src/dashboard/app/components/EntityLink.jsx +55 -0
  162. package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
  163. package/src/dashboard/app/components/FlowGraph.jsx +19 -17
  164. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
  165. package/src/dashboard/app/components/Markdown.jsx +47 -0
  166. package/src/dashboard/app/components/PhaseRail.jsx +111 -0
  167. package/src/dashboard/app/components/SearchFilter.jsx +40 -22
  168. package/src/dashboard/app/components/Sidebar.jsx +146 -37
  169. package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
  170. package/src/dashboard/app/hooks/usePref.js +38 -0
  171. package/src/dashboard/app/lib/format.js +69 -0
  172. package/src/dashboard/app/lib/phases.js +56 -0
  173. package/src/dashboard/app/lib/project.js +50 -0
  174. package/src/dashboard/app/pages/Activity.jsx +24 -0
  175. package/src/dashboard/app/pages/AgentMap.jsx +371 -370
  176. package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
  177. package/src/dashboard/app/pages/Features.jsx +400 -0
  178. package/src/dashboard/app/pages/Guide.jsx +146 -0
  179. package/src/dashboard/app/pages/HookInspector.jsx +357 -322
  180. package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
  181. package/src/dashboard/app/pages/Portfolio.jsx +156 -0
  182. package/src/dashboard/app/pages/Rubrics.jsx +101 -0
  183. package/src/dashboard/app/pages/RulesMap.jsx +300 -273
  184. package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
  185. package/src/dashboard/app/pages/Standards.jsx +407 -332
  186. package/src/dashboard/app/styles/index.css +187 -47
  187. package/src/dashboard/index.html +14 -12
  188. package/src/dashboard/package.json +32 -24
  189. package/src/dashboard/scanner/activity-scanner.js +351 -0
  190. package/src/dashboard/scanner/agents-scanner.js +59 -1
  191. package/src/dashboard/scanner/docs-scanner.js +59 -0
  192. package/src/dashboard/scanner/feature-status.js +10 -0
  193. package/src/dashboard/scanner/features-scanner.js +360 -0
  194. package/src/dashboard/scanner/index.js +14 -7
  195. package/src/dashboard/scanner/phases-scanner.js +14 -15
  196. package/src/dashboard/scanner/portfolio.js +153 -0
  197. package/src/dashboard/scanner/project-api.js +195 -0
  198. package/src/dashboard/scanner/rules-scanner.js +42 -37
  199. package/src/dashboard/scanner/skills-scanner.js +105 -43
  200. package/src/dashboard/scanner/standards-scanner.js +2 -0
  201. package/src/dashboard/vite-plugin-morph-data.js +134 -17
  202. package/src/lib/detectors/claude-config-detector.js +2 -7
  203. package/src/lib/detectors/index.js +5 -1
  204. package/src/lib/evaluator-report.js +22 -0
  205. package/src/lib/generators/settings-generator.js +208 -209
  206. package/src/lib/installers/mcp-installer.js +724 -719
  207. package/src/lib/installers/memory-installer.js +1 -2
  208. package/src/lib/projects/registry.js +154 -0
  209. package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
  210. package/src/scripts/install-plugin.js +1 -1
  211. package/src/scripts/setup-infra.js +2 -4
  212. package/src/utils/agents-installer.js +57 -12
  213. package/src/utils/banner.js +1 -1
  214. package/src/commands/index.js +0 -13
  215. package/src/commands/project/checkpoint.js +0 -209
  216. package/src/core/state/phase-state-machine.js +0 -197
  217. package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
  218. package/src/dashboard/app/pages/LiveState.jsx +0 -206
  219. package/src/dashboard/scanner/squads-scanner.js +0 -13
  220. package/src/dashboard/scanner/state-scanner.js +0 -85
  221. package/src/lib/generators/index.js +0 -10
  222. package/src/lib/hooks/hook-executor.js +0 -167
  223. package/src/lib/validators/checkpoint-persistence.js +0 -39
  224. package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
  225. package/src/lib/validators/shared/index.js +0 -12
  226. package/src/lib/validators/shared/issue-counter.js +0 -14
  227. package/src/lib/validators/shared/result-formatter.js +0 -124
@@ -0,0 +1,604 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { Link, useParams, useSearchParams } from 'react-router-dom';
3
+ import {
4
+ ArrowLeft, CheckCircle, CircleDashed, PauseCircle, Circle, WarningCircle, FileText, FileCode, CaretDown, CaretRight,
5
+ Rows, Kanban, Copy, Check, ArrowSquareOut,
6
+ } from '@phosphor-icons/react';
7
+ import PhaseRail, { segmentStates } from '../components/PhaseRail.jsx';
8
+ import KnowledgeMarkdown from '../components/KnowledgeMarkdown.jsx';
9
+ import EntityLink, { standardKey } from '../components/EntityLink.jsx';
10
+ import ActivityFeed from '../components/ActivityFeed.jsx';
11
+ import { useMorphQuery } from '../hooks/useMorphQuery.js';
12
+ import { usePref, useNow } from '../hooks/usePref.js';
13
+ import { Segmented, isWorking } from './Features.jsx';
14
+ import { apiBase, projectHref } from '../lib/project.js';
15
+ import { PHASES, GATE_LABEL, STATUS_LABEL, LOCATION_LABEL, phaseMeta } from '../lib/phases.js';
16
+ import { timeAgo, dateTime, num, duration, bytes } from '../lib/format.js';
17
+
18
+ const TABS = [
19
+ { id: 'overview', label: 'Visão geral' },
20
+ { id: 'tasks', label: 'Tasks' },
21
+ { id: 'docs', label: 'Documentos' },
22
+ { id: 'activity', label: 'Atividade' },
23
+ ];
24
+
25
+ const TASK_STATUS = {
26
+ done: { label: 'feita', tone: 'text-[var(--color-ok)]', icon: CheckCircle },
27
+ completed: { label: 'feita', tone: 'text-[var(--color-ok)]', icon: CheckCircle },
28
+ in_progress: { label: 'em andamento', tone: 'text-[var(--color-phase-implement)]', icon: CircleDashed },
29
+ blocked: { label: 'bloqueada', tone: 'text-[var(--color-warn)]', icon: PauseCircle },
30
+ skipped: { label: 'pulada', tone: 'text-[var(--color-faint)]', icon: Circle },
31
+ pending: { label: 'pendente', tone: 'text-[var(--color-faint)]', icon: Circle },
32
+ };
33
+
34
+ /** Duration of each task from the telemetry (TaskStart → TaskEnd), newest segment wins. */
35
+ function taskDurations(cost) {
36
+ const out = {};
37
+ for (const t of cost?.tasks || []) if (t.task && t.durationSeconds) out[t.task] = t.durationSeconds;
38
+ return out;
39
+ }
40
+
41
+ /** A pending task whose dependencies are all done can start now. */
42
+ function isReady(t, byId) {
43
+ if (t.status !== 'pending') return false;
44
+ return (t.dependencies || []).every((d) => ['done', 'completed', 'skipped'].includes(byId[d]?.status));
45
+ }
46
+
47
+ /** `vscode://file/…` opens the file in the editor from the browser. */
48
+ export function editorLink(absPath) {
49
+ return `vscode://file/${encodeURI(String(absPath).replace(/\\/g, '/'))}`;
50
+ }
51
+
52
+ function CopyButton({ text, label = 'Copiar caminho' }) {
53
+ const [done, setDone] = useState(false);
54
+ const copy = async () => {
55
+ try { await navigator.clipboard.writeText(text); setDone(true); setTimeout(() => setDone(false), 1500); } catch { /* clipboard blocked */ }
56
+ };
57
+ return (
58
+ <button type="button" onClick={copy} title={text}
59
+ className="fluid inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 font-sans text-xs text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)] transition-colors hover:text-[var(--color-fg)] active:scale-[0.98]">
60
+ {done ? <Check weight="light" size={13} /> : <Copy weight="light" size={13} />}{done ? 'Copiado' : label}
61
+ </button>
62
+ );
63
+ }
64
+
65
+ function EditorButton({ path, label = 'Abrir no VS Code' }) {
66
+ return (
67
+ <a href={editorLink(path)}
68
+ className="fluid inline-flex items-center gap-1.5 rounded-full bg-white/[0.06] px-2.5 py-1 font-sans text-xs text-[var(--color-fg)] transition-colors hover:bg-white/[0.1] active:scale-[0.98]">
69
+ <ArrowSquareOut weight="light" size={13} />{label}
70
+ </a>
71
+ );
72
+ }
73
+
74
+ /** What a person should expect next, in one sentence. */
75
+ export function nextAction(f) {
76
+ if (f.status === 'completed') return `Concluída${f.archivedAt ? ` ${timeAgo(f.archivedAt)}` : ''}.`;
77
+ if (f.status === 'abandoned') return 'Abandonada.';
78
+ const paused = f.gates.find((g) => g.paused);
79
+ if (paused) return `O ${GATE_LABEL[paused.gate] || paused.gate} pausou${paused.reason ? `: ${paused.reason}` : ''}. A decisão é sua, no Claude Code.`;
80
+ if (f.inEvaluation) return 'O avaliador independente entregou o relatório. O Gate 3 decide pelo score.';
81
+ if (f.phase === 'implement' && f.tasksList?.length) {
82
+ const open = f.tasksList.find((t) => t.status === 'in_progress') || f.tasksList.find((t) => t.status === 'pending');
83
+ if (open) return `${f.tasks.done} de ${f.tasks.total} tasks feitas. Próxima: ${open.id}, ${open.title}.`;
84
+ return 'Todas as tasks feitas. Falta o verify da feature e a revisão.';
85
+ }
86
+ const text = {
87
+ proposal: 'Proposta em construção. O Gate 1 aprova o escopo.',
88
+ uiux: 'Desenho de UI/UX em andamento.',
89
+ plan: 'Plano sendo escrito: spec, use cases e tasks. O Gate 2 aprova.',
90
+ implement: 'Implementação em andamento.',
91
+ review: 'Em revisão.',
92
+ };
93
+ return text[f.phase] || '';
94
+ }
95
+
96
+ function Panel({ title, children, className = '' }) {
97
+ return (
98
+ <section className={`bezel ${className}`}>
99
+ <div className="bezel-core px-5 py-5">
100
+ {title && <h2 className="mb-4 text-sm font-medium text-[var(--color-fg)]">{title}</h2>}
101
+ {children}
102
+ </div>
103
+ </section>
104
+ );
105
+ }
106
+
107
+ function GateList({ f }) {
108
+ const unused = new Set(segmentStates(f).filter((s) => s.state === 'skipped').map((s) => s.id));
109
+ return (
110
+ <ol className="space-y-4">
111
+ {f.gates.map((g) => {
112
+ const decision = f.gateDecisions?.[g.gate];
113
+ const state = g.skipped || unused.has(g.gate) ? 'fase não usada' : g.approved ? 'aprovado' : g.paused ? 'pausado' : 'pendente';
114
+ const Icon = g.approved ? CheckCircle : g.paused ? PauseCircle : Circle;
115
+ const tone = g.approved ? 'text-[var(--color-ok)]' : g.paused ? 'text-[var(--color-warn)]' : 'text-[var(--color-faint)]';
116
+ return (
117
+ <li key={g.gate} className="grid grid-cols-[1.25rem_minmax(0,1fr)] gap-3">
118
+ <Icon weight="light" size={18} className={tone} />
119
+ <div className="min-w-0">
120
+ <p className="text-sm">
121
+ <span className="text-[var(--color-fg)]">{GATE_LABEL[g.gate] || g.gate}</span>
122
+ <span className="ml-2 text-[var(--color-dim)]">{state}{g.approvedBy ? `, por ${g.approvedBy}` : ''}</span>
123
+ </p>
124
+ {g.at && <p className="text-xs text-[var(--color-faint)]">{dateTime(g.at)}</p>}
125
+ {decision?.reason && <p className="mt-1 text-sm text-[var(--color-dim)]">{decision.reason}</p>}
126
+ {decision?.trust && <p className="text-xs text-[var(--color-faint)]">trust {decision.trust}</p>}
127
+ </div>
128
+ </li>
129
+ );
130
+ })}
131
+ </ol>
132
+ );
133
+ }
134
+
135
+ function NodeChips({ nodes }) {
136
+ if (!nodes) return <p className="text-sm text-[var(--color-faint)]">Nenhum verify registrado.</p>;
137
+ return (
138
+ <ul className="flex flex-wrap gap-2">
139
+ {Object.entries(nodes).map(([name, v]) => {
140
+ const status = typeof v === 'string' ? v : v?.status;
141
+ const tone = status === 'pass' ? 'text-[var(--color-ok)]' : status === 'fail' ? 'text-[var(--color-bad)]' : 'text-[var(--color-faint)]';
142
+ return (
143
+ <li key={name} className="rounded-full px-3 py-1 text-xs shadow-[inset_0_0_0_1px_var(--color-hair)]">
144
+ <span className="text-[var(--color-dim)]">{name}</span> <span className={tone}>{status || '—'}</span>
145
+ </li>
146
+ );
147
+ })}
148
+ </ul>
149
+ );
150
+ }
151
+
152
+ function ScoreBlock({ f }) {
153
+ const score = f.score ?? f.legacyScore;
154
+ const proofs = f.proofScores ? Object.entries(f.proofScores) : [];
155
+ return (
156
+ <div>
157
+ <p className="flex items-baseline gap-2">
158
+ <span className="text-4xl font-semibold tabular-nums tracking-tight">{score ?? '—'}</span>
159
+ <span className="text-sm text-[var(--color-dim)]">{score == null ? 'sem relatório do avaliador' : f.score != null ? 'Composite-Score do avaliador' : 'score legado'}</span>
160
+ </p>
161
+ {proofs.length > 0 && (
162
+ <ul className="mt-4 space-y-1.5">
163
+ {proofs.map(([k, v]) => (
164
+ <li key={k} className="grid grid-cols-[5.5rem_minmax(0,1fr)_2rem] items-center gap-3 text-xs">
165
+ <span className="text-[var(--color-dim)]">{k}</span>
166
+ <span className="h-1 overflow-hidden rounded-full bg-white/[0.07]">
167
+ <span className="block h-full rounded-full" style={{ width: `${v * 10}%`, background: v >= 9 ? 'var(--color-ok)' : v >= 7 ? 'var(--color-phase-plan)' : 'var(--color-warn)' }} />
168
+ </span>
169
+ <span className="text-right font-mono tabular-nums text-[var(--color-dim)]">{v}</span>
170
+ </li>
171
+ ))}
172
+ </ul>
173
+ )}
174
+ </div>
175
+ );
176
+ }
177
+
178
+ function CostBlock({ cost }) {
179
+ if (!cost) return <p className="text-sm text-[var(--color-faint)]">Lendo a telemetria…</p>;
180
+ const tokens = (u) => (u ? u.inputTokens + u.outputTokens + u.cacheCreationTokens : null);
181
+ const rows = [
182
+ ['Tokens da sessão', cost.sessionUsage ? num(tokens(cost.sessionUsage)) : 'desconhecido', cost.sessionUsage ? `${num(cost.sessionUsage.cacheReadTokens)} lidos do cache` : cost.legacyUsageSamples ? 'telemetria v1, sem delta somável' : null],
183
+ ['Tokens dos sub-agents', cost.agentUsage ? num(tokens(cost.agentUsage)) : 'desconhecido', cost.agentUsage ? `${cost.agentUsage.agents} sub-agents` : null],
184
+ ['Despachos', String(cost.dispatches?.count ?? 0), cost.dispatches?.promptChars ? `${num(cost.dispatches.promptChars)} caracteres de prompt` : null],
185
+ ['Tempo em tasks', duration(cost.taskDurationSeconds), null],
186
+ ['Lido para o contexto', bytes(cost.bytesTotal), null],
187
+ ];
188
+ return (
189
+ <dl className="space-y-3">
190
+ {rows.map(([k, v, hint]) => (
191
+ <div key={k} className="flex items-baseline justify-between gap-4">
192
+ <dt className="text-sm text-[var(--color-dim)]">{k}</dt>
193
+ <dd className="text-right">
194
+ <span className="font-mono text-sm tabular-nums">{v}</span>
195
+ {hint && <span className="block text-xs text-[var(--color-faint)]">{hint}</span>}
196
+ </dd>
197
+ </div>
198
+ ))}
199
+ </dl>
200
+ );
201
+ }
202
+
203
+ function OverviewTab({ f, cost }) {
204
+ return (
205
+ <div className="grid gap-5 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)]">
206
+ <div className="space-y-5">
207
+ {f.description && (
208
+ <Panel title="Descrição">
209
+ <p className="text-sm leading-relaxed text-[var(--color-dim)]">{f.description}</p>
210
+ </Panel>
211
+ )}
212
+ <Panel title="Gates"><GateList f={f} /></Panel>
213
+ </div>
214
+ <div className="space-y-5">
215
+ <Panel title="Avaliação"><ScoreBlock f={f} /></Panel>
216
+ <Panel title="Último verify">
217
+ {f.verification && (
218
+ <p className="mb-3 text-xs text-[var(--color-faint)]">
219
+ escopo {f.verification.scope || '—'}, {timeAgo(f.verification.ts)}
220
+ </p>
221
+ )}
222
+ <NodeChips nodes={f.verificationNodes} />
223
+ </Panel>
224
+ <Panel title="Custo"><CostBlock cost={cost} /></Panel>
225
+ </div>
226
+ </div>
227
+ );
228
+ }
229
+
230
+ function reportPathFor(f, taskId) {
231
+ const candidates = [`3-implement/reports/${taskId}.md`, `3-implement/evidencias/${taskId}.md`];
232
+ return candidates.filter((p) => f.documents.some((d) => d.path === p));
233
+ }
234
+
235
+ function TaskRow({ t, f, open, onToggle, duration: secs, ready }) {
236
+ const st = TASK_STATUS[t.status] || TASK_STATUS.pending;
237
+ const StIcon = st.icon;
238
+ const reports = reportPathFor(f, t.id);
239
+ const verify = t.verification;
240
+ return (
241
+ <li>
242
+ <button
243
+ type="button"
244
+ onClick={onToggle}
245
+ aria-expanded={open}
246
+ className="fluid grid w-full grid-cols-[1.25rem_2.75rem_minmax(0,1fr)] items-center gap-3 px-5 py-3 text-left transition-colors hover:bg-white/[0.025] md:grid-cols-[1.25rem_2.75rem_minmax(0,1fr)_9rem_4.5rem_3rem_1rem]"
247
+ >
248
+ <StIcon weight="light" size={17} className={st.tone} aria-label={st.label} />
249
+ <span className="font-mono text-xs text-[var(--color-dim)]">{t.id}</span>
250
+ <span className="flex min-w-0 items-center gap-2">
251
+ <span className="truncate text-sm text-[var(--color-fg)]">{t.title}</span>
252
+ {ready && <span className="shrink-0 text-xs text-[var(--color-phase-implement)]">pronta</span>}
253
+ {secs ? <span className="shrink-0 text-xs text-[var(--color-faint)]">{duration(secs)}</span> : null}
254
+ </span>
255
+ <span className="hidden truncate text-xs text-[var(--color-dim)] md:block">{t.agent ? `morph-${t.agent}` : '—'}</span>
256
+ <span className="hidden text-xs md:block">
257
+ {verify ? <span className={verify.status === 'pass' ? 'text-[var(--color-ok)]' : 'text-[var(--color-bad)]'}>verify {verify.status}</span> : <span className="text-[var(--color-faint)]">—</span>}
258
+ </span>
259
+ <span className="hidden text-right font-mono text-xs tabular-nums text-[var(--color-dim)] md:block">{t.score?.lastScore ?? verify?.score ?? '—'}</span>
260
+ <span className="hidden text-[var(--color-faint)] md:block">{open ? <CaretDown weight="light" size={14} /> : <CaretRight weight="light" size={14} />}</span>
261
+ </button>
262
+ {open && (
263
+ <div className="grid gap-5 px-5 pb-5 pl-[4.75rem] md:grid-cols-2">
264
+ <div className="space-y-4 text-sm">
265
+ {t.description && <p className="leading-relaxed text-[var(--color-dim)]">{t.description}</p>}
266
+ {Array.isArray(t.doneCriteria) && t.doneCriteria.length > 0 && (
267
+ <div>
268
+ <p className="mb-1.5 text-xs text-[var(--color-faint)]">Critérios de pronto</p>
269
+ <ul className="list-disc space-y-1 pl-4 text-[var(--color-dim)] marker:text-[var(--color-faint)]">
270
+ {t.doneCriteria.map((c, i) => <li key={i}>{c}</li>)}
271
+ </ul>
272
+ </div>
273
+ )}
274
+ {t.notes && <p className="text-[var(--color-dim)]"><span className="text-[var(--color-faint)]">Notas: </span>{t.notes}</p>}
275
+ </div>
276
+ <dl className="space-y-3 text-sm">
277
+ {t.agent && (
278
+ <div><dt className="text-xs text-[var(--color-faint)]">Persona</dt>
279
+ <dd><EntityLink kind="persona" id={t.agent}>morph-{t.agent}</EntityLink></dd></div>
280
+ )}
281
+ {t.standards?.length > 0 && (
282
+ <div><dt className="text-xs text-[var(--color-faint)]">Standards</dt>
283
+ <dd className="mt-1 flex flex-wrap gap-1.5">
284
+ {t.standards.map((s) => (
285
+ <EntityLink key={s} kind="standard" id={standardKey(s)} chip title={s}>{standardKey(s).replace(/\.md$/, '')}</EntityLink>
286
+ ))}
287
+ </dd></div>
288
+ )}
289
+ {t.dependencies?.length > 0 && (
290
+ <div><dt className="text-xs text-[var(--color-faint)]">Depende de</dt><dd className="font-mono text-xs text-[var(--color-dim)]">{t.dependencies.join(', ')}</dd></div>
291
+ )}
292
+ {t.usecase && <div><dt className="text-xs text-[var(--color-faint)]">Use case</dt><dd className="font-mono text-xs text-[var(--color-dim)]">{t.usecase}</dd></div>}
293
+ {verify?.nodes && <div><dt className="mb-1 text-xs text-[var(--color-faint)]">Verify da task</dt><dd><NodeChips nodes={verify.nodes} /></dd></div>}
294
+ {t.outputs?.length > 0 && (
295
+ <div><dt className="text-xs text-[var(--color-faint)]">Arquivos</dt>
296
+ <dd className="mt-1 space-y-0.5 font-mono text-[11px] text-[var(--color-dim)]">{t.outputs.map((o) => <p key={o} className="truncate" title={o}>{o}</p>)}</dd></div>
297
+ )}
298
+ {reports.length > 0 && (
299
+ <div className="flex flex-wrap gap-2 pt-1">
300
+ {reports.map((p) => (
301
+ <Link key={p} to={`?tab=docs&doc=${encodeURIComponent(p)}`} className="fluid rounded-full bg-white/[0.06] px-3 py-1.5 text-xs text-[var(--color-fg)] transition-colors hover:bg-white/[0.1]">
302
+ Abrir {p.includes('evidencias') ? 'evidências' : 'relatório'}
303
+ </Link>
304
+ ))}
305
+ </div>
306
+ )}
307
+ </dl>
308
+ </div>
309
+ )}
310
+ </li>
311
+ );
312
+ }
313
+
314
+ const BOARD_COLUMNS = [
315
+ { id: 'pending', label: 'Pendentes', match: (t) => !t.status || t.status === 'pending' },
316
+ { id: 'in_progress', label: 'Em andamento', match: (t) => t.status === 'in_progress' },
317
+ { id: 'blocked', label: 'Bloqueadas', match: (t) => t.status === 'blocked' },
318
+ { id: 'done', label: 'Feitas', match: (t) => t.status === 'done' || t.status === 'completed' || t.status === 'skipped' },
319
+ ];
320
+
321
+ function TaskCard({ t, f, secs, ready }) {
322
+ const reports = reportPathFor(f, t.id);
323
+ const score = t.score?.lastScore ?? t.verification?.score;
324
+ return (
325
+ <li className="rounded-xl bg-[var(--color-raised)] px-3.5 py-3 shadow-[inset_0_0_0_1px_var(--color-hair),inset_0_1px_0_rgb(255_255_255_/_0.05)]">
326
+ <p className="flex items-baseline gap-2">
327
+ <span className="font-mono text-xs text-[var(--color-dim)]">{t.id}</span>
328
+ {ready && <span className="text-xs text-[var(--color-phase-implement)]">pronta</span>}
329
+ </p>
330
+ <p className="mt-1 text-sm leading-snug text-[var(--color-fg)]">{t.title}</p>
331
+ {t.agent && (
332
+ <EntityLink kind="persona" id={t.agent} className="mt-2 inline-block text-xs">morph-{t.agent}</EntityLink>
333
+ )}
334
+ {t.dependencies?.length > 0 && <p className="mt-1 text-xs text-[var(--color-faint)]">depende de {t.dependencies.join(', ')}</p>}
335
+ <div className="mt-2.5 flex items-center justify-between text-xs">
336
+ <span className="flex items-center gap-2">
337
+ {t.verification && <span className={t.verification.status === 'pass' ? 'text-[var(--color-ok)]' : 'text-[var(--color-bad)]'}>verify {t.verification.status}</span>}
338
+ {score != null && <span className="font-mono text-[var(--color-dim)]">{score}</span>}
339
+ </span>
340
+ <span className="text-[var(--color-faint)]">{secs ? duration(secs) : ''}</span>
341
+ </div>
342
+ {reports.length > 0 && (
343
+ <Link to={`?tab=docs&doc=${encodeURIComponent(reports[0])}`} className="mt-2.5 inline-block text-xs text-[var(--color-dim)] underline decoration-[var(--color-hair-strong)] underline-offset-2 hover:text-[var(--color-fg)]">abrir relatório</Link>
344
+ )}
345
+ </li>
346
+ );
347
+ }
348
+
349
+ function TasksBoard({ f, durations, byId }) {
350
+ return (
351
+ <div className="-mx-4 overflow-x-auto px-4 pb-2 md:mx-0 md:px-0">
352
+ <div className="grid auto-cols-[16rem] grid-flow-col gap-3 md:auto-cols-[minmax(12rem,1fr)]">
353
+ {BOARD_COLUMNS.map((c) => {
354
+ const items = f.tasksList.filter(c.match);
355
+ return (
356
+ <section key={c.id} className="bezel min-w-0" aria-label={`${c.label}: ${items.length}`}>
357
+ <div className="bezel-core flex h-full min-h-[12rem] flex-col p-2.5">
358
+ <h3 className="flex items-center justify-between px-1.5 pb-2.5 pt-1 text-sm font-medium">
359
+ {c.label}<span className="font-mono text-xs text-[var(--color-faint)]">{items.length}</span>
360
+ </h3>
361
+ {items.length === 0
362
+ ? <p className="px-1.5 text-xs text-[var(--color-faint)]">Nenhuma.</p>
363
+ : <ul className="space-y-2">{items.map((t) => <TaskCard key={t.id} t={t} f={f} secs={durations[t.id]} ready={isReady(t, byId)} />)}</ul>}
364
+ </div>
365
+ </section>
366
+ );
367
+ })}
368
+ </div>
369
+ </div>
370
+ );
371
+ }
372
+
373
+ function TasksTab({ f, cost, view = 'list', onView = () => {} }) {
374
+ const [open, setOpen] = useState(null);
375
+ if (!f.tasksList.length) {
376
+ return <Panel><p className="text-sm text-[var(--color-dim)]">Esta feature ainda não tem <code className="font-mono">2-plan/tasks.json</code>. As tasks nascem na fase de plano.</p></Panel>;
377
+ }
378
+ const durations = taskDurations(cost);
379
+ const byId = Object.fromEntries(f.tasksList.map((t) => [t.id, t]));
380
+ const ready = f.tasksList.filter((t) => isReady(t, byId)).length;
381
+ return (
382
+ <div className="space-y-4">
383
+ <div className="flex flex-wrap items-center justify-between gap-3">
384
+ <Segmented label="Visualização das tasks" value={view} onChange={onView} options={[{ id: 'list', label: 'Lista', icon: Rows }, { id: 'board', label: 'Quadro', icon: Kanban }]} />
385
+ <p className="text-sm text-[var(--color-dim)]">
386
+ {f.tasks.done} de {f.tasks.total} feitas{f.tasks.inProgress ? `, ${f.tasks.inProgress} em andamento` : ''}{ready ? `, ${ready} prontas para começar` : ''}
387
+ </p>
388
+ </div>
389
+ {f.tasksErrors?.length > 0 && (
390
+ <p className="flex items-start gap-2 text-sm text-[var(--color-warn)]">
391
+ <WarningCircle weight="light" size={17} className="mt-0.5 shrink-0" />
392
+ O tasks.json não passa no schema ({f.tasksErrors.length} erros). As tasks aparecem como estão no arquivo.
393
+ </p>
394
+ )}
395
+ {view === 'board' ? <TasksBoard f={f} durations={durations} byId={byId} /> : (
396
+ <div className="bezel">
397
+ <ul className="bezel-core divide-y divide-[var(--color-hair)] overflow-hidden">
398
+ {f.tasksList.map((t) => (
399
+ <TaskRow key={t.id} t={t} f={f} duration={durations[t.id]} ready={isReady(t, byId)}
400
+ open={open === t.id} onToggle={() => setOpen(open === t.id ? null : t.id)} />
401
+ ))}
402
+ </ul>
403
+ </div>
404
+ )}
405
+ </div>
406
+ );
407
+ }
408
+
409
+ function groupDocuments(documents) {
410
+ const groups = PHASES.map((p) => ({ ...p, files: documents.filter((d) => d.phase === p.id) })).filter((g) => g.files.length);
411
+ const other = documents.filter((d) => !d.phase);
412
+ if (other.length) groups.push({ id: 'other', label: 'Estado e outros', color: 'var(--color-faint)', files: other });
413
+ return groups;
414
+ }
415
+
416
+ function DocViewer({ project, feature, path, dir }) {
417
+ const url = path ? `${apiBase(project)}/file?feature=${encodeURIComponent(feature)}&path=${encodeURIComponent(path)}` : null;
418
+ const { data, error, loading } = useMorphQuery(url, { project });
419
+ if (!path) return <p className="text-sm text-[var(--color-dim)]">Escolha um documento à esquerda.</p>;
420
+ if (error) return <p className="text-sm text-[var(--color-bad)]">{error}</p>;
421
+ if (loading || !data) return <p className="text-sm text-[var(--color-faint)]">Abrindo…</p>;
422
+ // One separator: `dir` comes from path.join (backslashes on win32), `path` from the API (slashes).
423
+ const abs = dir ? `${dir.replace(/\\/g, '/').replace(/\/$/, '')}/${path}` : null;
424
+ const header = (
425
+ <div className="mb-6 flex flex-wrap items-center justify-between gap-3 font-mono text-xs text-[var(--color-faint)]">
426
+ <span className="min-w-0 truncate">{path}, atualizado {timeAgo(data.mtime)}</span>
427
+ {abs && <span className="flex shrink-0 gap-2"><CopyButton text={abs} /><EditorButton path={abs} /></span>}
428
+ </div>
429
+ );
430
+ if (path.endsWith('.md')) return <>{header}<KnowledgeMarkdown text={data.content} /></>;
431
+ let text = data.content;
432
+ if (path.endsWith('.json')) {
433
+ try { text = JSON.stringify(JSON.parse(text), null, 2); } catch { /* shown as is */ }
434
+ }
435
+ return (
436
+ <>
437
+ {header}
438
+ <pre className="overflow-x-auto rounded-xl bg-[var(--color-ink)] p-4 font-mono text-xs leading-relaxed text-[var(--color-dim)] shadow-[inset_0_0_0_1px_var(--color-hair)]">{text}</pre>
439
+ </>
440
+ );
441
+ }
442
+
443
+ function DocsTab({ f, doc, onDoc, project }) {
444
+ const groups = groupDocuments(f.documents);
445
+ const selected = doc || f.documents.find((d) => d.path.endsWith('spec.md'))?.path || f.documents.find((d) => d.path.endsWith('.md'))?.path || null;
446
+ return (
447
+ <div className="grid gap-5 lg:grid-cols-[16rem_minmax(0,1fr)]">
448
+ <nav aria-label="Documentos da feature" className="lg:sticky lg:top-4 lg:self-start">
449
+ {groups.length === 0 && <p className="text-sm text-[var(--color-dim)]">Nenhum arquivo ainda.</p>}
450
+ {groups.map((g) => (
451
+ <div key={g.id} className="mb-5">
452
+ <p className="mb-1.5 flex items-center gap-2 px-2 text-xs text-[var(--color-dim)]">
453
+ <span className="h-2 w-2 rounded-full" style={{ background: g.color }} />{g.label}
454
+ </p>
455
+ <ul>
456
+ {g.files.map((d) => {
457
+ const active = d.path === selected;
458
+ const Icon = d.path.endsWith('.md') ? FileText : FileCode;
459
+ const name = d.path.split('/').slice(1).join('/') || d.path;
460
+ return (
461
+ <li key={d.path}>
462
+ <button
463
+ type="button"
464
+ onClick={() => onDoc(d.path)}
465
+ aria-current={active ? 'page' : undefined}
466
+ className={[
467
+ 'fluid flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors',
468
+ active ? 'bg-white/[0.08] text-[var(--color-fg)]' : 'text-[var(--color-dim)] hover:bg-white/[0.04] hover:text-[var(--color-fg)]',
469
+ ].join(' ')}
470
+ >
471
+ <Icon weight="light" size={15} className="shrink-0" />
472
+ <span className="truncate" title={d.path}>{name}</span>
473
+ </button>
474
+ </li>
475
+ );
476
+ })}
477
+ </ul>
478
+ </div>
479
+ ))}
480
+ </nav>
481
+ <div className="bezel min-w-0">
482
+ <div className="bezel-core min-w-0 px-6 py-6 md:px-10 md:py-8">
483
+ <DocViewer project={project} feature={f.name} path={selected} dir={f.dir} />
484
+ </div>
485
+ </div>
486
+ </div>
487
+ );
488
+ }
489
+
490
+ export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, latest = null, tasksView = 'list', onTasksView = () => {}, now = Date.now(), project, projectName }) {
491
+ const score = f.score ?? f.legacyScore;
492
+ const working = latest && isWorking({ ...f, lastActivity: latest }, now);
493
+ return (
494
+ <div className="mx-auto max-w-6xl px-4 pb-24 pt-8 md:px-10 md:pt-12">
495
+ <Link to={projectHref(project)} className="fluid mb-6 inline-flex items-center gap-2 text-sm text-[var(--color-dim)] transition-colors hover:text-[var(--color-fg)]">
496
+ <ArrowLeft weight="light" size={15} /> {projectName ? `Features de ${projectName}` : 'Features'}
497
+ </Link>
498
+
499
+ <header className="rise bezel mb-8">
500
+ <div className="bezel-core px-6 py-7 md:px-9 md:py-9">
501
+ <div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
502
+ <div className="min-w-0">
503
+ <h1 className="break-words text-3xl font-semibold tracking-tight md:text-4xl">{f.name}</h1>
504
+ <p className="mt-2 text-sm text-[var(--color-dim)]">
505
+ {STATUS_LABEL[f.status] || f.status}, {f.workType || 'feature'}, na {LOCATION_LABEL[f.location]}, atualizada {timeAgo(f.updatedAt)}
506
+ </p>
507
+ </div>
508
+ <div className="flex shrink-0 gap-6 text-right">
509
+ <div><p className="text-2xl font-semibold tabular-nums">{f.tasks.total ? `${f.tasks.done}/${f.tasks.total}` : '—'}</p><p className="text-xs text-[var(--color-faint)]">tasks</p></div>
510
+ <div><p className="text-2xl font-semibold tabular-nums">{score ?? '—'}</p><p className="text-xs text-[var(--color-faint)]">score</p></div>
511
+ </div>
512
+ </div>
513
+ <div className="mt-8"><PhaseRail feature={f} size="lg" /></div>
514
+ <p className="mt-6 text-sm text-[var(--color-fg)]">
515
+ <span style={{ color: phaseMeta(f.phase).color }}>●</span> {nextAction(f)}
516
+ </p>
517
+ {latest && f.status !== 'completed' && (
518
+ <p className="mt-2 flex items-center gap-2 text-sm text-[var(--color-dim)]">
519
+ {working && (
520
+ <span className="relative inline-flex h-2 w-2" aria-label="trabalhando agora">
521
+ <span className="absolute inset-0 animate-ping rounded-full bg-[var(--color-phase-implement)] opacity-60 motion-reduce:hidden" />
522
+ <span className="relative h-2 w-2 rounded-full bg-[var(--color-phase-implement)]" />
523
+ </span>
524
+ )}
525
+ <span>Último evento: {latest.title}, {timeAgo(latest.ts, now)}</span>
526
+ <button type="button" onClick={() => onTab('activity')} className="text-[var(--color-phase-proposal)] hover:underline">ver atividade</button>
527
+ </p>
528
+ )}
529
+ <div className="mt-5 flex flex-wrap gap-2">
530
+ <EditorButton path={f.dir} label="Abrir pasta da feature no VS Code" />
531
+ <CopyButton text={f.name} label="Copiar nome" />
532
+ </div>
533
+ {f.rootStale && (
534
+ <p className="mt-3 flex items-start gap-2 text-sm text-[var(--color-warn)]">
535
+ <WarningCircle weight="light" size={17} className="mt-0.5 shrink-0" />
536
+ A worktree dona desta feature não tem os arquivos dela. O que aparece é a cópia da raiz, que pode estar desatualizada.
537
+ </p>
538
+ )}
539
+ </div>
540
+ </header>
541
+
542
+ <div role="tablist" aria-label="Seções da feature" className="rise-2 mb-6 inline-flex rounded-full bg-white/[0.03] p-1 shadow-[inset_0_0_0_1px_var(--color-hair)]">
543
+ {TABS.map((t) => (
544
+ <button
545
+ key={t.id}
546
+ role="tab"
547
+ type="button"
548
+ aria-selected={tab === t.id}
549
+ onClick={() => onTab(t.id)}
550
+ className={[
551
+ 'fluid rounded-full px-4 py-1.5 text-sm transition-colors active:scale-[0.98]',
552
+ tab === t.id ? 'bg-white/[0.1] text-[var(--color-fg)] shadow-[inset_0_1px_0_rgb(255_255_255_/_0.08)]' : 'text-[var(--color-dim)] hover:text-[var(--color-fg)]',
553
+ ].join(' ')}
554
+ >
555
+ {t.label}{t.id === 'tasks' && f.tasks.total ? ` ${f.tasks.total}` : ''}{t.id === 'docs' ? ` ${f.documents.length}` : ''}
556
+ </button>
557
+ ))}
558
+ </div>
559
+
560
+ <div className="rise-3">
561
+ {tab === 'overview' && <OverviewTab f={f} cost={cost} />}
562
+ {tab === 'tasks' && <TasksTab f={f} cost={cost} view={tasksView} onView={onTasksView} />}
563
+ {tab === 'docs' && <DocsTab f={f} doc={doc} onDoc={onDoc} project={project} />}
564
+ {tab === 'activity' && (
565
+ <div className="bezel"><div className="bezel-core px-5 py-5"><ActivityFeed project={project} feature={f.name} /></div></div>
566
+ )}
567
+ </div>
568
+ </div>
569
+ );
570
+ }
571
+
572
+ export default function FeatureDetail() {
573
+ const { name, project } = useParams();
574
+ const [params, setParams] = useSearchParams();
575
+ const tab = TABS.some((t) => t.id === params.get('tab')) ? params.get('tab') : 'overview';
576
+ const doc = params.get('doc');
577
+ const { data, error, loading } = useMorphQuery(`${apiBase(project)}/features/${encodeURIComponent(name)}`, { project });
578
+ // The cost rides on the activity endpoint (it owns the event stream); one
579
+ // line of feed is enough to get it, and it refreshes with the log.
580
+ const costQuery = useMorphQuery(`${apiBase(project)}/activity?feature=${encodeURIComponent(name)}&limit=1`, { scope: 'activity', project });
581
+ const [tasksView, setTasksView] = usePref('tasks.view', 'list');
582
+ const now = useNow();
583
+
584
+ const onTab = useCallback((id) => setParams((p) => { const n = new URLSearchParams(p); n.set('tab', id); if (id !== 'docs') n.delete('doc'); return n; }), [setParams]);
585
+ const onDoc = useCallback((path) => setParams((p) => { const n = new URLSearchParams(p); n.set('tab', 'docs'); n.set('doc', path); return n; }), [setParams]);
586
+
587
+ if (error) {
588
+ return (
589
+ <div className="mx-auto max-w-3xl px-4 pt-16 md:px-10">
590
+ <Link to={projectHref(project)} className="text-sm text-[var(--color-dim)] hover:text-[var(--color-fg)]">Voltar para Features</Link>
591
+ <p className="mt-6 text-lg">Não encontrei a feature “{name}”.</p>
592
+ <p className="mt-1 text-sm text-[var(--color-dim)]">{error}</p>
593
+ </div>
594
+ );
595
+ }
596
+ if (loading || !data) return <p className="px-10 pt-16 text-sm text-[var(--color-faint)]">Lendo a feature…</p>;
597
+ return (
598
+ <FeatureDetailView
599
+ f={data} tab={tab} onTab={onTab} doc={doc} onDoc={onDoc}
600
+ cost={costQuery.data?.cost ?? null} latest={costQuery.data?.items?.[0] ?? null}
601
+ tasksView={tasksView} onTasksView={setTasksView} now={now} project={project}
602
+ />
603
+ );
604
+ }