@zaip-ai/cli 5.0.0 → 5.1.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 (93) hide show
  1. package/dist/services/adapters.d.ts +1 -1
  2. package/dist/services/adapters.js +1 -1
  3. package/package.json +1 -1
  4. package/templates/.agents/README-ENG.md +9 -0
  5. package/templates/.agents/README.md +9 -0
  6. package/templates/.agents/config/agents/discovery.json +1 -1
  7. package/templates/.agents/dashboard/.zaip-managed.json +95 -55
  8. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-w6pjt4OM.js → CostCalculationGuidePage-uR-asJv_.js} +1 -1
  9. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-DhlntQcR.js → DashboardHelpDialog-CngnpoLL.js} +12 -3
  10. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-BHoZT2an.js → PricingSettingsDialog-Bszw3ekd.js} +1 -1
  11. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-IcncIZ9Y.js → ProjectSettingsPage-IJ1znG4w.js} +1 -1
  12. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-C6rORjpr.js → TraceabilityWorkspace-CsBpHqq_.js} +1 -1
  13. package/templates/.agents/dashboard/apps/frontend/dist/assets/{cost-monitor-uPnOBShc.js → cost-monitor-B0Jwz_rk.js} +4 -4
  14. package/templates/.agents/dashboard/apps/frontend/dist/assets/{dashboard-localization-DWXjEIgc.js → dashboard-localization-D41g5rdo.js} +9 -0
  15. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-Hc9fp_gs.js → icons-vendor-CjGGux6h.js} +1 -1
  16. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-Bedlm0f7.js +2 -0
  17. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-C9ta_O10.js → react-vendor-B9jZSAKf.js} +1 -1
  18. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-IcefoTOv.js → traceability-DFpwb0TN.js} +1 -1
  19. package/templates/.agents/dashboard/apps/frontend/dist/index.html +5 -5
  20. package/templates/.agents/dashboard/apps/frontend/package.json +2 -2
  21. package/templates/.agents/dashboard/apps/frontend/src/AppDashboardContent.tsx +3 -0
  22. package/templates/.agents/dashboard/apps/frontend/src/AppOverlays.tsx +16 -7
  23. package/templates/.agents/dashboard/apps/frontend/src/__tests__/app-composition-support.ts +1 -0
  24. package/templates/.agents/dashboard/apps/frontend/src/cost-period-refresh.test.ts +1 -1
  25. package/templates/.agents/dashboard/apps/frontend/src/operations-period-render.test.ts +2 -0
  26. package/templates/.agents/dashboard/apps/frontend/src/use-app-trace-documents.navigation.test.tsx +125 -0
  27. package/templates/.agents/dashboard/apps/frontend/src/use-app-trace-documents.ts +67 -9
  28. package/templates/.agents/dashboard/apps/frontend/src/use-dashboard-app-model.tsx +15 -15
  29. package/templates/.agents/dashboard/apps/frontend/src/use-dashboard-locale.ts +18 -0
  30. package/templates/.agents/dashboard/modules/cost-monitor/package.json +1 -1
  31. package/templates/.agents/dashboard/modules/operations/package.json +1 -1
  32. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.tsx +4 -39
  33. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverviewTopPanel.navigation.test.tsx +150 -0
  34. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverviewTopPanel.tsx +7 -3
  35. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +4 -7
  36. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPageOverview.tsx +3 -0
  37. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-contract.ts +51 -0
  38. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +5 -1
  39. package/templates/.agents/dashboard/modules/operations/src/react/operations-page-contract.ts +1 -0
  40. package/templates/.agents/dashboard/modules/operations/src/react/use-operations-default-range.ts +16 -0
  41. package/templates/.agents/dashboard/modules/traceability/package.json +1 -1
  42. package/templates/.agents/dashboard/packages/branding/package.json +1 -1
  43. package/templates/.agents/dashboard/packages/contracts/package.json +1 -1
  44. package/templates/.agents/dashboard/packages/data-export/package.json +1 -1
  45. package/templates/.agents/dashboard/packages/local-runtime/dashboard-build-fingerprint.mjs +87 -0
  46. package/templates/.agents/dashboard/packages/local-runtime/dashboard-build-fingerprint.test.mjs +48 -0
  47. package/templates/.agents/dashboard/packages/local-runtime/package.json +1 -1
  48. package/templates/.agents/dashboard/packages/local-runtime/scripts/build-frontend.mjs +29 -0
  49. package/templates/.agents/dashboard/packages/local-runtime/scripts/build-sqlite-runtime.mjs +5 -0
  50. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-build.mjs +12 -5
  51. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-paths.mjs +8 -0
  52. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.test.mjs +1 -0
  53. package/templates/.agents/dashboard/packages/ui/package.json +1 -1
  54. package/templates/.agents/dashboard/tests/e2e/package.json +1 -1
  55. package/templates/.agents/dashboard/tests/e2e/qualify-top-five-trace.mjs +53 -0
  56. package/templates/.agents/dashboard/tests/e2e/verify-top-five-served.mjs +78 -0
  57. package/templates/.agents/design/.zaip-managed.json +14 -6
  58. package/templates/.agents/design/apps/catalog/dist/assets/index-BGxXxXTo.js +72 -0
  59. package/templates/.agents/design/apps/catalog/dist/index.html +1 -1
  60. package/templates/.agents/design/apps/catalog/package.json +1 -1
  61. package/templates/.agents/design/apps/catalog/src/components/TopEntitiesSpecimen.tsx +45 -0
  62. package/templates/.agents/design/apps/catalog/src/pages/ComponentsPage.tsx +3 -1
  63. package/templates/.agents/design/apps/catalog/src/stories/Components.stories.tsx +2 -0
  64. package/templates/.agents/design/packages/local-runtime/package.json +1 -1
  65. package/templates/.agents/requirements/autonomous-assurance.md +1 -1
  66. package/templates/.agents/requirements/cloud-hostinger-autonomy.md +1 -1
  67. package/templates/.agents/requirements/comparative-audit.md +1 -1
  68. package/templates/.agents/requirements/context-engine.md +1 -1
  69. package/templates/.agents/requirements/cost-observability.md +1 -1
  70. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  71. package/templates/.agents/requirements/data-export.md +1 -1
  72. package/templates/.agents/requirements/design-system.md +1 -1
  73. package/templates/.agents/requirements/execution-engine.md +1 -1
  74. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  75. package/templates/.agents/requirements/model-orchestration.md +1 -1
  76. package/templates/.agents/requirements/public-distribution.md +1 -1
  77. package/templates/.agents/requirements/software-architecture.md +1 -1
  78. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  79. package/templates/.agents/requirements/work-lifecycle.md +1 -1
  80. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  81. package/templates/.agents/runtime/VERSION +1 -1
  82. package/templates/.agents/runtime/bin/zaip.mjs +10 -10
  83. package/templates/.agents/runtime/checksums.sha256 +6 -6
  84. package/templates/.agents/runtime/manifest.json +1 -1
  85. package/templates/.agents/runtime/pnpm-lock.sha256 +1 -1
  86. package/templates/.agents/runtime/provenance.json +5 -5
  87. package/templates/.agents/runtime/sbom.cdx.json +1 -1
  88. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract-cli.test.mjs +53 -0
  89. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.test.mjs +1 -32
  90. package/templates/.agents/zaip-aios/VERSION +1 -1
  91. package/templates/.agents/zaip-aios/guides/10-seguranca.md +2 -2
  92. package/templates/template-manifest.json +146 -94
  93. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-BqeOEZW5.js +0 -2
@@ -0,0 +1,125 @@
1
+ import { createRequire } from "node:module";
2
+ import { fileURLToPath } from "node:url";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const browserRequire = createRequire(
6
+ fileURLToPath(new URL("../../../tests/e2e/package.json", import.meta.url)),
7
+ );
8
+ const { chromium, expect: browserExpect } = browserRequire("@playwright/test");
9
+
10
+ describe("macro trace navigation from a compact overview", () => {
11
+ it("hydrates exact macro details, keeps linked rows, and ignores stale macro and Task requests", async () => {
12
+ const esbuild = browserRequire("esbuild");
13
+ const harness = await esbuild.build({
14
+ bundle: true,
15
+ format: "iife",
16
+ logLevel: "silent",
17
+ platform: "browser",
18
+ write: false,
19
+ stdin: {
20
+ contents: `
21
+ import React, { useState } from "react";
22
+ import { createRoot } from "react-dom/client";
23
+ import { useAppTraceDocuments } from "./use-app-trace-documents.js";
24
+
25
+ const pending = [];
26
+ window.__macroTraceProbe = { pending };
27
+ const api = {
28
+ operations: (_period, _scope, signal) => new Promise((resolve) => {
29
+ pending.push({ resolve, signal });
30
+ }),
31
+ taskTrace: async (id) => ({ task_id: id, task_title: id }),
32
+ prd: async (id) => ({ id, title: id, body: "Full document", artifact_path: "prds/one.md" }),
33
+ };
34
+ function Harness() {
35
+ const [error, setError] = useState(null);
36
+ const [, setFailedArea] = useState(null);
37
+ const [, setLoadingArea] = useState(null);
38
+ const state = useAppTraceDocuments({
39
+ api,
40
+ operations: { prds: [], specs: [], tasks: [], runs: [], changes: [] },
41
+ isOperationsDetailLoaded: () => false,
42
+ setError,
43
+ setFailedArea,
44
+ setLoadingArea,
45
+ });
46
+ return <>
47
+ <button onClick={() => void state.openTraceMacro("SPEC", "SPEC-TARGET")}>Open SPEC</button>
48
+ <button onClick={() => void state.openTraceMacro("PRD", "PRD-TARGET")}>Open PRD</button>
49
+ <button onClick={() => void state.openTrace("TASK-TARGET")}>Open Task</button>
50
+ <button onClick={() => void state.openDocument("prd", "PRD-TARGET")}>Read PRD</button>
51
+ <output id="state">{JSON.stringify({
52
+ macro: state.traceLink?.kind === "macro" ? state.traceLink.row.id : null,
53
+ kind: state.traceLink?.kind === "macro" ? state.traceLink.row.kind : null,
54
+ linkedTaskIds: state.traceLink?.kind === "macro"
55
+ ? state.traceLink.projection.tasks.map((row) => row.id) : [],
56
+ loading: state.traceMacroLoading?.id ?? null,
57
+ task: state.trace?.task_id ?? null,
58
+ document: state.documentDetail?.preview.id ?? null,
59
+ error,
60
+ })}</output>
61
+ </>;
62
+ }
63
+ createRoot(document.getElementById("root")).render(<Harness />);
64
+ `,
65
+ loader: "tsx",
66
+ resolveDir: fileURLToPath(new URL(".", import.meta.url)),
67
+ sourcefile: "macro-trace-navigation-harness.tsx",
68
+ },
69
+ });
70
+ const browser = await chromium.launch({ headless: true });
71
+ try {
72
+ const page = await browser.newPage();
73
+ const pageErrors: string[] = [];
74
+ page.on("pageerror", (error: Error) => pageErrors.push(error.message));
75
+ await page.setContent('<div id="root"></div>');
76
+ await page.addScriptTag({ content: harness.outputFiles[0].text });
77
+ const state = () => page.locator("#state");
78
+ const resolve = (index: number, prds: unknown[], specs: unknown[]) =>
79
+ page.evaluate(
80
+ ({ index, prds, specs }: { index: number; prds: unknown[]; specs: unknown[] }) => {
81
+ const request = (
82
+ window as typeof window & {
83
+ __macroTraceProbe: { pending: Array<{ resolve: (value: unknown) => void }> };
84
+ }
85
+ ).__macroTraceProbe.pending[index];
86
+ if (!request) throw new Error(`Missing pending macro request ${index}`);
87
+ request.resolve({
88
+ prds,
89
+ specs,
90
+ tasks: [{ id: "TASK-LINKED" }],
91
+ runs: [],
92
+ });
93
+ },
94
+ { index, prds, specs },
95
+ );
96
+
97
+ await page.getByRole("button", { name: "Open SPEC" }).click();
98
+ await browserExpect(state()).toContainText('"loading":"SPEC-TARGET"');
99
+ await page.getByRole("button", { name: "Open PRD" }).click();
100
+ await browserExpect(state()).toContainText('"loading":"PRD-TARGET"');
101
+ await resolve(0, [], [{ id: "SPEC-TARGET", kind: "spec" }]);
102
+ await browserExpect(state()).toContainText('"macro":null');
103
+ await resolve(1, [{ id: "PRD-TARGET", kind: "prd" }], []);
104
+ await browserExpect(state()).toContainText('"macro":"PRD-TARGET"');
105
+ await browserExpect(state()).toContainText('"linkedTaskIds":["TASK-LINKED"]');
106
+ await page.getByRole("button", { name: "Read PRD" }).click();
107
+ await browserExpect(state()).toContainText('"document":"PRD-TARGET"');
108
+
109
+ await page.getByRole("button", { name: "Open SPEC" }).click();
110
+ await page.getByRole("button", { name: "Open Task" }).click();
111
+ await browserExpect(state()).toContainText('"task":"TASK-TARGET"');
112
+ await resolve(2, [], [{ id: "SPEC-TARGET", kind: "spec" }]);
113
+ await browserExpect(state()).toContainText('"macro":null');
114
+
115
+ await page.getByRole("button", { name: "Open PRD" }).click();
116
+ await resolve(3, [{ id: "PRD-OTHER", kind: "prd" }], []);
117
+ await browserExpect(state()).toContainText('"error":"PRD PRD-TARGET not found"');
118
+ await browserExpect(state()).toContainText('"loading":null');
119
+ expect(pageErrors).toEqual([]);
120
+ await page.close();
121
+ } finally {
122
+ await browser.close();
123
+ }
124
+ }, 60_000);
125
+ });
@@ -13,12 +13,14 @@ import type { DashboardApiClient } from "./dashboard-api.js";
13
13
  export function useAppTraceDocuments({
14
14
  api,
15
15
  operations,
16
+ isOperationsDetailLoaded,
16
17
  setError,
17
18
  setFailedArea,
18
19
  setLoadingArea,
19
20
  }: {
20
21
  api: DashboardApiClient;
21
22
  operations: OperationsProjection | null;
23
+ isOperationsDetailLoaded: () => boolean;
22
24
  setError: Dispatch<SetStateAction<string | null>>;
23
25
  setFailedArea: Dispatch<SetStateAction<LoadingArea | null>>;
24
26
  setLoadingArea: Dispatch<SetStateAction<LoadingArea | null>>;
@@ -34,16 +36,31 @@ export function useAppTraceDocuments({
34
36
  } | null>(null);
35
37
  const [traceLink, setTraceLink] = useState<
36
38
  | { kind: "change"; row: ChangeProjection }
37
- | { kind: "macro"; label: "PRD" | "SPEC"; row: MacroProjection }
39
+ | {
40
+ kind: "macro";
41
+ label: "PRD" | "SPEC";
42
+ row: MacroProjection;
43
+ projection: OperationsProjection;
44
+ }
38
45
  | null
39
46
  >(null);
40
47
  const documentRequestId = useRef(0);
41
48
  const traceRequestId = useRef(0);
49
+ const macroRequestId = useRef(0);
50
+ const macroAbortController = useRef<AbortController | null>(null);
51
+ const [traceMacroLoading, setTraceMacroLoading] = useState<{
52
+ label: "PRD" | "SPEC";
53
+ id: string;
54
+ } | null>(null);
42
55
  const traceAbortController = useRef<AbortController | null>(null);
43
56
  const [traceLoadingTaskId, setTraceLoadingTaskId] = useState<string | null>(null);
44
57
  const [selectedTraceTaskId, setSelectedTraceTaskId] = useState("");
45
58
 
46
59
  async function openTrace(taskId: string) {
60
+ macroRequestId.current += 1;
61
+ macroAbortController.current?.abort();
62
+ setTraceMacroLoading(null);
63
+ setTraceLink(null);
47
64
  const requestId = ++traceRequestId.current;
48
65
  traceAbortController.current?.abort();
49
66
  const controller = new AbortController();
@@ -92,18 +109,58 @@ export function useAppTraceDocuments({
92
109
  setTraceLink({ kind: "change", row });
93
110
  }
94
111
 
95
- function openTraceMacro(label: "PRD" | "SPEC", id: string) {
96
- const rows = label === "PRD" ? operations?.prds : operations?.specs;
97
- const row = rows?.find((macro) => macro.id === id);
98
- if (row === undefined) return;
112
+ async function openTraceMacro(label: "PRD" | "SPEC", id: string) {
113
+ const requestId = ++macroRequestId.current;
114
+ macroAbortController.current?.abort();
115
+ macroAbortController.current = null;
116
+ traceRequestId.current += 1;
117
+ traceAbortController.current?.abort();
118
+ traceAbortController.current = null;
119
+ setTraceLoadingTaskId(null);
120
+ setLoadingArea((current) => (current === "traceability" ? null : current));
121
+ setError(null);
122
+ setFailedArea(null);
99
123
  setTrace(null);
100
- setTraceLink({ kind: "macro", label, row });
124
+ setTraceLink(null);
125
+ const rows = label === "PRD" ? operations?.prds : operations?.specs;
126
+ const existing = rows?.find((macro) => macro.id === id && macro.kind === label.toLowerCase());
127
+ if (existing && operations && isOperationsDetailLoaded()) {
128
+ setTraceMacroLoading(null);
129
+ setTraceLink({ kind: "macro", label, row: existing, projection: operations });
130
+ return;
131
+ }
132
+ const controller = new AbortController();
133
+ macroAbortController.current = controller;
134
+ setTraceMacroLoading({ label, id });
135
+ try {
136
+ // The compact first fold has ranking IDs but omits macro and linked Task rows.
137
+ // Hydrate only after the user requests this traceability dialog.
138
+ const projection = await api.operations(undefined, "detail", controller.signal);
139
+ if (requestId !== macroRequestId.current || controller.signal.aborted) return;
140
+ const row = (label === "PRD" ? projection.prds : projection.specs).find(
141
+ (macro) => macro.id === id && macro.kind === label.toLowerCase(),
142
+ );
143
+ if (!row) throw new Error(`${label} ${id} not found`);
144
+ setTraceLink({ kind: "macro", label, row, projection });
145
+ } catch (cause) {
146
+ if (requestId !== macroRequestId.current || controller.signal.aborted) return;
147
+ setError(cause instanceof Error ? cause.message : String(cause));
148
+ } finally {
149
+ if (requestId === macroRequestId.current) {
150
+ if (macroAbortController.current === controller) macroAbortController.current = null;
151
+ setTraceMacroLoading(null);
152
+ }
153
+ }
101
154
  }
102
155
 
103
156
  async function openDocument(kind: "prd" | "spec", id: string) {
104
- const preview = (kind === "prd" ? operations?.prds : operations?.specs)?.find(
105
- (macro) => macro.id === id,
106
- );
157
+ const preview =
158
+ (kind === "prd" ? operations?.prds : operations?.specs)?.find((macro) => macro.id === id) ??
159
+ (traceLink?.kind === "macro"
160
+ ? (kind === "prd" ? traceLink.projection.prds : traceLink.projection.specs).find(
161
+ (macro) => macro.id === id,
162
+ )
163
+ : undefined);
107
164
  if (!preview) return;
108
165
  const requestId = ++documentRequestId.current;
109
166
  const label = kind === "prd" ? "PRD" : "SPEC";
@@ -143,6 +200,7 @@ export function useAppTraceDocuments({
143
200
  setTraceTasks,
144
201
  trace,
145
202
  traceLink,
203
+ traceMacroLoading,
146
204
  traceLoadingTaskId,
147
205
  traceTasks,
148
206
  };
@@ -62,6 +62,7 @@ import { useAppTraceDocuments } from "./use-app-trace-documents.js";
62
62
  import { useAppTraceTaskPagination } from "./use-app-trace-task-pagination.js";
63
63
  import { useAppWorkBoard } from "./use-app-work-board.js";
64
64
  import { useAppWorkListPage } from "./use-app-work-list-page.js";
65
+ import { useDashboardLocale } from "./use-dashboard-locale.js";
65
66
  import { useOperationsDetailExport } from "./use-operations-detail-export.js";
66
67
  import { useSnapshotNotice } from "./use-snapshot-notice.js";
67
68
  import "./loading-feedback.css";
@@ -103,18 +104,8 @@ export function useDashboardAppModel() {
103
104
  useState<OverviewClassificationPages>({});
104
105
  const [costs, setCosts] = useState<CostMonitorProjection | null>(null);
105
106
  const [displayedCosts, setDisplayedCosts] = useState<CostMonitorProjection | null>(null);
106
- const [costLocale, setCostLocale] = useState<CostLocale>(savedLocale);
107
- const [currency, setCurrency] = useState<CostCurrency>(() =>
108
- window.localStorage.getItem(currencyKey) === "USD" ? "USD" : "BRL",
109
- );
110
- const currencyRef = useRef(currency);
111
- currencyRef.current = currency;
112
- // Language is the global selector's source of truth for the paired display
113
- // currency, so Operations and Cost Monitor cannot diverge.
114
- const changeDashboardLocale = useCallback((next: CostLocale) => {
115
- setCostLocale(next);
116
- setCurrency(next === "en-US" ? "USD" : "BRL");
117
- }, []);
107
+ const { costLocale, currency, setCurrency, currencyRef, changeDashboardLocale } =
108
+ useDashboardLocale();
118
109
  const [operationsPeriod, setOperationsPeriod] = useState(() => defaultOperationsPeriod);
119
110
  // All Time may update its displayed factual range without changing the
120
111
  // unbounded request identity. Keep the refresh callback stable for that
@@ -174,6 +165,7 @@ export function useDashboardAppModel() {
174
165
  cancelled = true;
175
166
  };
176
167
  }, [api, applyDashboardIdentity]);
168
+ const operationsDetailLoadedRef = useRef(false);
177
169
  const {
178
170
  closeDocument,
179
171
  documentDetail,
@@ -181,6 +173,7 @@ export function useDashboardAppModel() {
181
173
  openTrace,
182
174
  openTraceChange,
183
175
  openTraceMacro,
176
+ traceMacroLoading,
184
177
  selectedTraceTaskId,
185
178
  setSelectedTraceTaskId,
186
179
  setTrace,
@@ -191,7 +184,14 @@ export function useDashboardAppModel() {
191
184
  traceLink,
192
185
  traceLoadingTaskId,
193
186
  traceTasks,
194
- } = useAppTraceDocuments({ api, operations, setError, setFailedArea, setLoadingArea });
187
+ } = useAppTraceDocuments({
188
+ api,
189
+ operations,
190
+ isOperationsDetailLoaded: () => operationsDetailLoadedRef.current,
191
+ setError,
192
+ setFailedArea,
193
+ setLoadingArea,
194
+ });
195
195
  const { loadMoreTraceTasks, traceTasksHasMore, traceTasksLoading } = useAppTraceTaskPagination(
196
196
  api,
197
197
  traceTasks,
@@ -205,7 +205,6 @@ export function useDashboardAppModel() {
205
205
  const costSummaryRequestVersion = useRef(0);
206
206
  const costSummaryAbortRef = useRef<AbortController | null>(null);
207
207
  const costsLoadedRef = useRef(false);
208
- const operationsDetailLoadedRef = useRef(false);
209
208
  const operationsDetailCancelRef = useRef<(() => void) | null>(null);
210
209
  const operationsDetailAbortRef = useRef<AbortController | null>(null);
211
210
  const operationsPageAbortRef = useRef<AbortController | null>(null);
@@ -571,7 +570,7 @@ export function useDashboardAppModel() {
571
570
  setLoadingArea((current) => (current === "costs" ? null : current));
572
571
  if (costSummaryAbortRef.current === controller) costSummaryAbortRef.current = null;
573
572
  }
574
- }, [api, currency, operationsPeriod]);
573
+ }, [api, currency, operationsPeriod, currencyRef]);
575
574
 
576
575
  // The global language selector can request USD while Operations is active.
577
576
  // Acquire its coherent Cost Monitor snapshot here instead of requiring a
@@ -695,6 +694,7 @@ export function useDashboardAppModel() {
695
694
  openTraceChange,
696
695
  openTraceMacro,
697
696
  selectedTraceTaskId,
697
+ traceMacroLoading,
698
698
  trace,
699
699
  traceLoadingTaskId,
700
700
  traceTasksHasMore,
@@ -0,0 +1,18 @@
1
+ import type { CostCurrency, CostLocale } from "@zaip-ai/dashboard-cost-monitor/react";
2
+ import { useCallback, useRef, useState } from "react";
3
+ import { currencyKey, savedLocale } from "./app-preferences.js";
4
+ export function useDashboardLocale() {
5
+ const [costLocale, setCostLocale] = useState<CostLocale>(savedLocale);
6
+ const [currency, setCurrency] = useState<CostCurrency>(() =>
7
+ window.localStorage.getItem(currencyKey) === "USD" ? "USD" : "BRL",
8
+ );
9
+ const currencyRef = useRef(currency);
10
+ currencyRef.current = currency;
11
+ // Language is the global selector's source of truth for the paired display
12
+ // currency, so Operations and Cost Monitor cannot diverge.
13
+ const changeDashboardLocale = useCallback((next: CostLocale) => {
14
+ setCostLocale(next);
15
+ setCurrency(next === "en-US" ? "USD" : "BRL");
16
+ }, []);
17
+ return { costLocale, currency, setCurrency, currencyRef, changeDashboardLocale };
18
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaip-ai/dashboard-cost-monitor",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaip-ai/dashboard-operations",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,3 +1,4 @@
1
+ import type { OperationsOverviewProps } from "./operations-overview-contract.js";
1
2
  import { formatSummaryMetricValue, unmeasuredZeroDescription } from "./overview-metric-format.js";
2
3
  import { overviewMetricStats } from "./overview-metric-stats.js";
3
4
 
@@ -94,6 +95,7 @@ export function OperationsOverview({
94
95
  onDatePresetChange,
95
96
  onOpenTask,
96
97
  onOpenDocument,
98
+ onOpenMacro,
97
99
  onLoadStatus,
98
100
  statusPage,
99
101
  onLoadStatusPage,
@@ -109,45 +111,7 @@ export function OperationsOverview({
109
111
  costLoading = false,
110
112
  resolveRunCost,
111
113
  includeLegacyEffort: _includeLegacyEffort = false,
112
- }: {
113
- currency: "BRL" | "USD";
114
- data: OperationsProjection;
115
- /** App-controlled range paired with datePreset; retained by Page while loading. */
116
- dateRange?: OperationDateRange;
117
- datePreset: OperationDatePreset;
118
- locale: OperationsLocale;
119
- onDatePresetChange: (preset: OperationDatePreset) => void;
120
- onOpenTask: (taskId: string) => Promise<boolean>;
121
- onOpenDocument: (kind: "prd" | "spec", id: string) => void;
122
- onLoadStatus?: ((stage: WorkStage, kind: OverviewStatusKind, offset: number) => void) | undefined;
123
- statusPage?:
124
- | ((stage: WorkStage, kind: OverviewStatusKind) => OverviewStatusPage | undefined)
125
- | undefined;
126
- onLoadStatusPage?:
127
- | ((stage: WorkStage, kind: OverviewStatusKind, offset: number) => void)
128
- | undefined;
129
- classificationPage?:
130
- | ((
131
- kind: OverviewStatusKind,
132
- classification: OverviewClassification,
133
- ) => OverviewClassificationPage | undefined)
134
- | undefined;
135
- onLoadClassificationPage?:
136
- | ((kind: OverviewStatusKind, classification: OverviewClassification, offset: number) => void)
137
- | undefined;
138
- onLoadMacroMetrics?: (() => void) | undefined;
139
- prds?: OperationsProjection["prds"];
140
- runs?: OperationsProjection["runs"];
141
- specs?: OperationsProjection["specs"];
142
- tasks?: OperationsProjection["tasks"];
143
- /** Factual Cost Monitor snapshot total for the selected locale/currency. */
144
- factualCostTotal?: number | null | undefined;
145
- /** Measurement completeness for the matching Cost Monitor snapshot. */
146
- factualCostFact?: FactualCostFact | undefined;
147
- costLoading?: boolean | undefined;
148
- resolveRunCost?: ((runId: string) => number | null) | undefined;
149
- includeLegacyEffort?: boolean;
150
- }) {
114
+ }: OperationsOverviewProps) {
151
115
  const [overviewView, setOverviewView] = useState<OverviewView>("tasks");
152
116
  const [selectedStatus, setSelectedStatus] = useState<WorkStage | null>(null);
153
117
  const [selectedClassification, setSelectedClassification] =
@@ -687,6 +651,7 @@ export function OperationsOverview({
687
651
  currency={currency}
688
652
  locale={locale}
689
653
  maxDuration={maxDuration}
654
+ onOpenMacro={onOpenMacro}
690
655
  onOpenTask={onOpenTask}
691
656
  summary={topEntitiesSummary}
692
657
  text={text}
@@ -0,0 +1,150 @@
1
+ import { createRequire } from "node:module";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { Browser } from "@playwright/test";
4
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
5
+
6
+ declare global {
7
+ interface Window {
8
+ __topFiveNavigationProbe: {
9
+ tasks: string[];
10
+ documents: { kind: "prd" | "spec"; id: string }[];
11
+ };
12
+ }
13
+ }
14
+
15
+ const browserRequire = createRequire(
16
+ fileURLToPath(new URL("../../../../tests/e2e/package.json", import.meta.url)),
17
+ );
18
+ const { chromium, expect: expectPlaywright } = browserRequire("@playwright/test");
19
+
20
+ describe("Operations Top 5 navigation", () => {
21
+ let browser: Browser;
22
+ let bundle: string;
23
+
24
+ beforeAll(async () => {
25
+ const esbuild = browserRequire("esbuild");
26
+ const harness = await esbuild.build({
27
+ bundle: true,
28
+ define: { "import.meta.url": '"http://localhost/"' },
29
+ format: "iife",
30
+ loader: { ".css": "text" },
31
+ logLevel: "silent",
32
+ platform: "browser",
33
+ alias: {
34
+ "@zaip-ai/dashboard-ui": fileURLToPath(
35
+ new URL("../../../../packages/ui/src/index.ts", import.meta.url),
36
+ ),
37
+ react: fileURLToPath(
38
+ new URL(
39
+ "../../../../../../node_modules/.pnpm/react@19.2.7/node_modules/react",
40
+ import.meta.url,
41
+ ),
42
+ ),
43
+ "react-dom": fileURLToPath(
44
+ new URL(
45
+ "../../../../../../node_modules/.pnpm/react-dom@19.2.7_react@19.2.7/node_modules/react-dom",
46
+ import.meta.url,
47
+ ),
48
+ ),
49
+ },
50
+ stdin: {
51
+ contents: `
52
+ import React, { useState } from "react";
53
+ import { createRoot } from "react-dom/client";
54
+ import { OperationsOverviewTopPanel } from "./OperationsOverviewTopPanel.js";
55
+
56
+ const entities = {
57
+ tasks: [
58
+ { id: "TASK-TRACE-FIRST", title: "First task", durationMs: 120000, tokens: 1, costBrl: 1, runCount: 1 },
59
+ { id: "TASK-TRACE-SECOND", title: "Second task", durationMs: 60000, tokens: 1, costBrl: 1, runCount: 1 },
60
+ ],
61
+ specs: [
62
+ { id: "SPEC-TRACE-FIRST", title: "First spec", durationMs: 120000, tokens: 1, costBrl: 1, runCount: 1 },
63
+ { id: "SPEC-TRACE-SECOND", title: "Second spec", durationMs: 60000, tokens: 1, costBrl: 1, runCount: 1 },
64
+ ],
65
+ prds: [
66
+ { id: "PRD-TRACE-FIRST", title: "First prd", durationMs: 120000, tokens: 1, costBrl: 1, runCount: 1 },
67
+ { id: "PRD-TRACE-SECOND", title: "Second prd", durationMs: 60000, tokens: 1, costBrl: 1, runCount: 1 },
68
+ ],
69
+ };
70
+ const probe = { tasks: [], documents: [] };
71
+ window.__topFiveNavigationProbe = probe;
72
+
73
+ function Harness() {
74
+ const [view, setView] = useState("tasks");
75
+ return <>
76
+ <nav aria-label="Overview mode">
77
+ {Object.keys(entities).map((mode) => <button key={mode} type="button" onClick={() => setView(mode)}>{mode}</button>)}
78
+ </nav>
79
+ <OperationsOverviewTopPanel
80
+ copy={{ empty: "Empty", hint: "Duration", title: "Top 5" }}
81
+ currency="BRL" locale="en-US" maxDuration={120000}
82
+ onOpenTask={async (id) => { probe.tasks.push(id); return true; }}
83
+ onOpenMacro={(kind, id) => probe.documents.push({ kind, id })}
84
+ summary={{ durationMs: 180000, durationShare: 1, runCount: 2, tokens: 2 }}
85
+ text={{ accumulatedTime: "Time", associatedRuns: "runs", concentration: "Concentration", totalMeasuredTime: "Total", topFiveCost: "Cost", tokensUnavailable: "Unavailable" }}
86
+ topCost={2} topEntities={entities[view]} view={view}
87
+ />
88
+ </>;
89
+ }
90
+ createRoot(document.getElementById("root")).render(<Harness />);
91
+ `,
92
+ loader: "tsx",
93
+ resolveDir: fileURLToPath(new URL(".", import.meta.url)),
94
+ sourcefile: "OperationsOverviewTopPanel.navigation-harness.tsx",
95
+ },
96
+ write: false,
97
+ });
98
+ bundle = harness.outputFiles[0].text;
99
+ browser = await chromium.launch({ headless: true });
100
+ }, 60_000);
101
+
102
+ afterAll(async () => {
103
+ await browser?.close();
104
+ }, 60_000);
105
+
106
+ it("opens the selected Task, SPEC, and PRD after switching modes by mouse and keyboard", async () => {
107
+ const page = await browser.newPage();
108
+ const pageErrors: string[] = [];
109
+ page.on("pageerror", (error) => pageErrors.push(error.message));
110
+ try {
111
+ await page.setContent('<div id="root"></div>');
112
+ await page.addScriptTag({ content: bundle });
113
+ await expectPlaywright(page.locator(".operations-run-bar")).toHaveCount(2);
114
+
115
+ await page.getByRole("button", { name: /Open task details for Second task/ }).click();
116
+ await page
117
+ .getByRole("navigation", { name: "Overview mode" })
118
+ .getByRole("button", { name: "specs" })
119
+ .click();
120
+ const spec = page.getByRole("button", { name: /Open details for SPEC First spec/ });
121
+ await expectPlaywright(spec).toBeEnabled();
122
+ await spec.focus();
123
+ await page.keyboard.press("Enter");
124
+ await page
125
+ .getByRole("navigation", { name: "Overview mode" })
126
+ .getByRole("button", { name: "prds" })
127
+ .click();
128
+ const prd = page.getByRole("button", { name: /Open details for PRD Second prd/ });
129
+ await expectPlaywright(prd).toBeEnabled();
130
+ await prd.focus();
131
+ await page.keyboard.press("Space");
132
+ await page
133
+ .getByRole("navigation", { name: "Overview mode" })
134
+ .getByRole("button", { name: "tasks" })
135
+ .click();
136
+ await page.getByRole("button", { name: /Open task details for First task/ }).click();
137
+
138
+ expect(await page.evaluate(() => window.__topFiveNavigationProbe)).toEqual({
139
+ tasks: ["TASK-TRACE-SECOND", "TASK-TRACE-FIRST"],
140
+ documents: [
141
+ { kind: "spec", id: "SPEC-TRACE-FIRST" },
142
+ { kind: "prd", id: "PRD-TRACE-SECOND" },
143
+ ],
144
+ });
145
+ expect(pageErrors).toEqual([]);
146
+ } finally {
147
+ await page.close();
148
+ }
149
+ }, 60_000);
150
+ });
@@ -18,6 +18,7 @@ export function OperationsOverviewTopPanel({
18
18
  currency,
19
19
  locale,
20
20
  maxDuration,
21
+ onOpenMacro,
21
22
  onOpenTask,
22
23
  summary,
23
24
  text,
@@ -30,6 +31,7 @@ export function OperationsOverviewTopPanel({
30
31
  currency: "BRL" | "USD";
31
32
  locale: OperationsLocale;
32
33
  maxDuration: number;
34
+ onOpenMacro: (kind: "prd" | "spec", id: string) => void;
33
35
  onOpenTask: (taskId: string) => Promise<boolean>;
34
36
  summary: {
35
37
  durationMs: number | null;
@@ -68,14 +70,16 @@ export function OperationsOverviewTopPanel({
68
70
  const label =
69
71
  view === "tasks"
70
72
  ? `${locale === "pt-BR" ? "Abrir detalhes da tarefa" : "Open task details for"} ${entity.title}: ${measuredDuration}`
71
- : `${entityKind} ${entity.title}: ${measuredDuration}`;
73
+ : `${locale === "pt-BR" ? "Abrir detalhes da" : "Open details for"} ${entityKind} ${entity.title}: ${measuredDuration}`;
72
74
  return (
73
75
  <button
74
76
  aria-label={label}
75
77
  className={`operations-run-bar${entity.durationMs === null ? " is-duration-unavailable" : ""}`}
76
- disabled={view !== "tasks"}
77
78
  key={entity.id}
78
- onClick={() => void onOpenTask(entity.id)}
79
+ onClick={() => {
80
+ if (view === "tasks") void onOpenTask(entity.id);
81
+ else onOpenMacro(view === "specs" ? "spec" : "prd", entity.id);
82
+ }}
79
83
  title={`${entity.title}: ${measuredDuration}`}
80
84
  type="button"
81
85
  >
@@ -20,6 +20,7 @@ import {
20
20
  workListTypeOnViewChange,
21
21
  } from "./operations-page-contract.js";
22
22
  import { useOperationFilterFeedback } from "./use-operation-filter-feedback.js";
23
+ import { useOperationsDefaultRange } from "./use-operations-default-range.js";
23
24
  import { useOperationsFilterSync } from "./use-operations-filter-sync.js";
24
25
 
25
26
  export {
@@ -86,6 +87,7 @@ export function OperationsPage({
86
87
  locale,
87
88
  onLocaleChange,
88
89
  onOpenDocument,
90
+ onOpenMacro,
89
91
  onOpenTask,
90
92
  onPriorityChange,
91
93
  onLoadMacroMetrics,
@@ -125,13 +127,7 @@ export function OperationsPage({
125
127
  const [originFilter, setOriginFilter] = useState<TaskOrigin | "">("");
126
128
  const { filterLoading, showFilterLoading } = useOperationFilterFeedback();
127
129
  const searchInputRef = useRef<HTMLInputElement>(null);
128
- const defaultDateRange = useMemo(() => {
129
- const operationsRange = initialOperationsDateRange(
130
- data.tasks,
131
- Object.keys(data.summary.periods?.daily ?? {}),
132
- );
133
- return factualCostDateRange(factualCostPeriods?.day, operationsRange);
134
- }, [data.summary.periods, data.tasks, factualCostPeriods?.day]);
130
+ const defaultDateRange = useOperationsDefaultRange(data, factualCostPeriods);
135
131
  const initialDateRange = useMemo(() => dateRangeForPreset("this-month"), []);
136
132
  const [localDateRange, setLocalDateRange] = useState(() => initialDateRange);
137
133
  const [localDatePreset, setLocalDatePreset] = useState<OperationDatePreset>("all");
@@ -633,6 +629,7 @@ export function OperationsPage({
633
629
  locale={locale}
634
630
  onOpenTask={onOpenTask}
635
631
  onOpenDocument={onOpenDocument}
632
+ onOpenMacro={onOpenMacro}
636
633
  onLoadStatus={onLoadStatusPage}
637
634
  prds={prds}
638
635
  query={query}