diff --git a/apps/aevatar-console-web/AGENTS.md b/apps/aevatar-console-web/AGENTS.md index bc24c7ea26..b6c04eaad6 100644 --- a/apps/aevatar-console-web/AGENTS.md +++ b/apps/aevatar-console-web/AGENTS.md @@ -200,6 +200,17 @@ pnpm --dir apps/aevatar-console-web build magic values. - Treat the console as an operational tool: prioritize scannability, clear hierarchy, predictable navigation, and efficient repeated actions. +- Default user-facing surfaces must show only information needed to understand + the current task, result, or next action. Do not expose backend architecture, + transport, storage, or consistency terminology such as `read model`, + `projection`, `materialization`, `receipt`, raw actor/command/correlation + identifiers, state versions/watermarks, DTO or endpoint names, or query + sampling limits in page titles, descriptions, helper copy, primary tables, + empty states, or primary error messages. Preserve truthful loading, accepted, + delayed, and failed semantics in plain product language. When raw values are + genuinely useful for support or debugging, place them behind an explicit, + user-opened technical-details disclosure instead of making them the default + interface. - Preserve responsive behavior, keyboard access, focus visibility, semantic controls, and readable contrast. Verify dense real content and narrow mobile widths without overlap, clipping, or inaccessible actions. @@ -209,6 +220,34 @@ pnpm --dir apps/aevatar-console-web build - Use established icon libraries and control patterns. Add accessible names or tooltips for icon-only or unfamiliar actions. +### Action Feedback and Toasts + +- Use the shared `ConsoleToastProvider` and `useConsoleToast` from + `src/shared/ui/ConsoleToast.tsx` for transient user-action feedback. Do not + introduce new direct `antd` `message` or `notification` calls inside React + product surfaces. Non-React transport error boundaries retain their existing + handling unless the boundary receives a deliberate React-safe migration. +- A success toast is evidence of a completed user-visible action, not of a + click, request dispatch, `202 Accepted` response, local optimistic update, + or background observation still in progress. Show it only after the API + contract has reached the state the copy claims. +- Keep a toast short, localized, and action-oriented. Do not put endpoint + names, DTO fields, request IDs, raw backend errors, or recovery diagnostics + in it; expose those through the surface's existing technical-details path. +- Report transient API request failures through the shared error toast. Do not + add page-wide warning or error banners above otherwise usable content, and + do not render an error while another request required to classify the same + state is still pending. +- Use persistent inline state, alerts, or panels for loading, accepted, + observing, delayed, retryable, authorization, forbidden, and primary-content + failures where the user needs a durable next step. A toast must not be the + only evidence of a durable status or recovery action. +- Emit at most one toast for one user action. Avoid success toasts for local + form edits that still require an explicit page-level save. Migrate touched + user-feedback paths to the shared abstraction while preserving legacy + behavior outside the requested surface unless a deliberate migration is in + scope. + ## Change and Review Hygiene - Branch names use `/YYYY-MM-DD_`, where `` is one of diff --git a/apps/aevatar-console-web/config/proxy.ts b/apps/aevatar-console-web/config/proxy.ts index 7dcbb001e3..216495365a 100644 --- a/apps/aevatar-console-web/config/proxy.ts +++ b/apps/aevatar-console-web/config/proxy.ts @@ -10,8 +10,7 @@ * @doc https://umijs.org/docs/guides/proxy */ const apiTarget = process.env.AEVATAR_API_TARGET || 'http://127.0.0.1:5080'; -const studioApiTarget = - process.env.AEVATAR_STUDIO_API_TARGET || apiTarget; +const studioApiTarget = process.env.AEVATAR_STUDIO_API_TARGET || apiTarget; const preserveAuthHost = process.env.AEVATAR_PROXY_PRESERVE_AUTH_HOST; const buildProxyTarget = (target: string) => ({ @@ -35,8 +34,8 @@ const buildAuthProxyTarget = (target: string) => ? buildHostPreservingProxyTarget(target) : buildProxyTarget(target) : shouldPreserveHost(target) - ? buildHostPreservingProxyTarget(target) - : buildProxyTarget(target); + ? buildHostPreservingProxyTarget(target) + : buildProxyTarget(target); const studioProxyEntries = [ '/api/app', @@ -46,33 +45,29 @@ const studioProxyEntries = [ '/api/executions', '/api/roles', '/api/studio', + '/api/user-config', '/api/workspace', -].reduce>>((entries, path) => { - const proxyFactory = path === '/api/auth' - ? buildAuthProxyTarget - : buildProxyTarget; - entries[`^${path}$`] = proxyFactory(studioApiTarget); - entries[`${path}/`] = proxyFactory(studioApiTarget); - return entries; -}, {}); +].reduce>>( + (entries, path) => { + const proxyFactory = + path === '/api/auth' ? buildAuthProxyTarget : buildProxyTarget; + entries[`^${path}$`] = proxyFactory(studioApiTarget); + entries[`${path}/`] = proxyFactory(studioApiTarget); + return entries; + }, + {}, +); const studioScopeProxyEntries = { - '^/api/scopes/[^/]+/chat-history(?:/.*)?$': - buildProxyTarget(studioApiTarget), - '^/api/scopes/[^/]+/teams/[^/]+/invoke(?:/.*)?$': - buildProxyTarget(apiTarget), - '^/api/scopes/[^/]+/teams(?:/.*)?$': - buildProxyTarget(studioApiTarget), - '^/api/scopes/[^/]+/members$': - buildProxyTarget(studioApiTarget), - '^/api/scopes/[^/]+/members/[^/]+$': - buildProxyTarget(studioApiTarget), + '^/api/scopes/[^/]+/chat-history(?:/.*)?$': buildProxyTarget(studioApiTarget), + '^/api/scopes/[^/]+/teams/[^/]+/invoke(?:/.*)?$': buildProxyTarget(apiTarget), + '^/api/scopes/[^/]+/teams(?:/.*)?$': buildProxyTarget(studioApiTarget), + '^/api/scopes/[^/]+/members$': buildProxyTarget(studioApiTarget), + '^/api/scopes/[^/]+/members/[^/]+$': buildProxyTarget(studioApiTarget), '^/api/scopes/[^/]+/members/[^/]+/(?:binding(?:/.*)?|binding-runs(?:/.*)?|endpoints/[^/]+/contract)$': buildProxyTarget(studioApiTarget), - '^/api/scopes/[^/]+/scripts/draft-run$': - buildProxyTarget(studioApiTarget), - '^/api/scopes/[^/]+/workflow/draft-run$': - buildProxyTarget(studioApiTarget), + '^/api/scopes/[^/]+/scripts/draft-run$': buildProxyTarget(studioApiTarget), + '^/api/scopes/[^/]+/workflow/draft-run$': buildProxyTarget(studioApiTarget), '^/api/scopes/[^/]+/workflows:save-and-bind$': buildProxyTarget(studioApiTarget), '^/api/scripts/validate$': buildProxyTarget(studioApiTarget), diff --git a/apps/aevatar-console-web/config/routes.ts b/apps/aevatar-console-web/config/routes.ts index 9a556947d9..c000e8384e 100644 --- a/apps/aevatar-console-web/config/routes.ts +++ b/apps/aevatar-console-web/config/routes.ts @@ -12,272 +12,307 @@ */ export default [ { - path: "/login", - component: "./login", + path: '/login', + component: './login', layout: false, }, { - path: "/auth/callback", - component: "./auth/callback", + path: '/auth/callback', + component: './auth/callback', layout: false, }, { - path: "/overview", - redirect: "/scopes", + path: '/overview', + redirect: '/scopes', hideInMenu: true, }, { - path: "/chat", - name: "Chat", - component: "./chat", - menuGroupKey: "chat", + path: '/chat', + name: 'Chat', + component: './chat', + menuGroupKey: 'chat', hideInMenu: false, }, { - path: "/scopes", - name: "My Teams", - component: "./teams", - menuGroupKey: "teams", + path: '/scopes', + name: 'My Teams', + component: './teams', + menuGroupKey: 'teams', hideInMenu: false, }, { - path: "/scopes/:scopeId/teams/new", - name: "Create Team", - component: "./teams/new", + path: '/scopes/:scopeId/workflow-activity-vnext', + redirect: '/scopes/:scopeId/workflow-activity-vnext/workflows', hideInMenu: true, - parentKeys: ["/scopes"], }, { - path: "/scopes/:scopeId/teams", - name: "My Teams", - component: "./teams", + path: '/scopes/:scopeId/workflow-activity-vnext/workflows', + component: './workflow-activity-vnext', hideInMenu: true, - parentKeys: ["/scopes"], }, { - path: "/scopes/:scopeId/teams/:teamId/members/new/workflow", - name: "Team Member Workflow Studio", - component: "./team-member-workflow-studio", + path: '/scopes/:scopeId/workflow-activity-vnext/workflows/new', + component: './workflow-activity-vnext', hideInMenu: true, - parentKeys: ["/scopes"], }, { - path: "/scopes/:scopeId/teams/:teamId/members/:memberId/workflow", - name: "Team Member Workflow Studio", - component: "./team-member-workflow-studio", + path: '/scopes/:scopeId/workflow-activity-vnext/workflows/:workflowId', + component: './workflow-activity-vnext', hideInMenu: true, - parentKeys: ["/scopes"], }, { - path: "/scopes/:scopeId/teams/:teamId/members/:memberId/invoke", - name: "Team Member Invoke", - component: "./team-member-invoke", + path: '/scopes/:scopeId/workflow-activity-vnext/activity', + component: './workflow-activity-vnext', hideInMenu: true, - parentKeys: ["/scopes"], }, { - path: "/scopes/:scopeId/teams/:teamId/members/:memberId/runs", - name: "Team Member Published Runs", - component: "./runtime-published-runs", + path: '/scopes/:scopeId/workflow-activity-vnext/activity/:runId', + component: './workflow-activity-vnext', hideInMenu: true, - parentKeys: ["/scopes"], }, { - path: "/scopes/:scopeId/teams/:teamId/members/:memberId/automations", - component: "./teams/detail", + path: '/scopes/:scopeId/workflow-activity-vnext/settings', + component: './workflow-activity-vnext', hideInMenu: true, - parentKeys: ["/scopes"], }, { - path: "/scopes/:scopeId/teams/:teamId", - name: "Team Details", - component: "./teams/detail", + path: '/scopes/:scopeId/teams/new', + name: 'Create Team', + component: './teams/new', hideInMenu: true, - parentKeys: ["/scopes"], + parentKeys: ['/scopes'], }, { - path: "/scopes/assets", - component: "./scopes/assets", + path: '/scopes/:scopeId/teams', + name: 'My Teams', + component: './teams', hideInMenu: true, + parentKeys: ['/scopes'], }, { - path: "/scopes/files", - name: "Files", - component: "./scopes/files", - menuGroupKey: "build", + path: '/scopes/:scopeId/teams/:teamId/members/new/workflow', + name: 'Team Member Workflow Studio', + component: './team-member-workflow-studio', + hideInMenu: true, + parentKeys: ['/scopes'], + }, + { + path: '/scopes/:scopeId/teams/:teamId/members/:memberId/workflow', + name: 'Team Member Workflow Studio', + component: './team-member-workflow-studio', + hideInMenu: true, + parentKeys: ['/scopes'], + }, + { + path: '/scopes/:scopeId/teams/:teamId/members/:memberId/invoke', + name: 'Team Member Invoke', + component: './team-member-invoke', + hideInMenu: true, + parentKeys: ['/scopes'], + }, + { + path: '/scopes/:scopeId/teams/:teamId/members/:memberId/runs', + name: 'Team Member Published Runs', + component: './runtime-published-runs', + hideInMenu: true, + parentKeys: ['/scopes'], + }, + { + path: '/scopes/:scopeId/teams/:teamId/members/:memberId/automations', + component: './teams/detail', + hideInMenu: true, + parentKeys: ['/scopes'], + }, + { + path: '/scopes/:scopeId/teams/:teamId', + name: 'Team Details', + component: './teams/detail', + hideInMenu: true, + parentKeys: ['/scopes'], + }, + { + path: '/scopes/assets', + component: './scopes/assets', + hideInMenu: true, + }, + { + path: '/scopes/files', + name: 'Files', + component: './scopes/files', + menuGroupKey: 'build', }, { - path: "/studio", - component: "./studio", + path: '/studio', + component: './studio', hideInMenu: true, }, { - path: "/runtime/workflows", - component: "./workflows", + path: '/runtime/workflows', + component: './workflows', hideInMenu: true, }, { - path: "/runtime/primitives", - name: "Connectors", - component: "./primitives", + path: '/runtime/primitives', + name: 'Connectors', + component: './primitives', hideInMenu: true, }, { - path: "/scopes/invoke", - component: "./scopes/invoke", + path: '/scopes/invoke', + component: './scopes/invoke', hideInMenu: true, }, { - path: "/runtime/runs", - name: "Event Stream", - component: "./runs", - menuGroupKey: "platform", + path: '/runtime/runs', + name: 'Event Stream', + component: './runs', + menuGroupKey: 'platform', }, { - path: "/runtime/mission-control", - name: "Mission Control", - component: "./MissionControl", + path: '/runtime/mission-control', + name: 'Mission Control', + component: './MissionControl', hideInMenu: true, }, { - path: "/runtime/mission-wall", - component: "./MissionWall", + path: '/runtime/mission-wall', + component: './MissionWall', hideInMenu: true, }, { - path: "/services", - name: "Services", - component: "./services", - menuGroupKey: "platform", + path: '/services', + name: 'Services', + component: './services', + menuGroupKey: 'platform', }, { - path: "/services/:serviceId", - component: "./services", + path: '/services/:serviceId', + component: './services', hideInMenu: true, - parentKeys: ["/services"], + parentKeys: ['/services'], }, { - path: "/governance", - name: "Governance", - component: "./governance", - menuGroupKey: "platform", + path: '/governance', + name: 'Governance', + component: './governance', + menuGroupKey: 'platform', }, { - path: "/governance/policies", - component: "./governance/policies", + path: '/governance/policies', + component: './governance/policies', hideInMenu: true, - parentKeys: ["/governance"], + parentKeys: ['/governance'], }, { - path: "/governance/bindings", - component: "./governance/bindings", + path: '/governance/bindings', + component: './governance/bindings', hideInMenu: true, - parentKeys: ["/governance"], + parentKeys: ['/governance'], }, { - path: "/governance/endpoints", - component: "./governance/endpoints", + path: '/governance/endpoints', + component: './governance/endpoints', hideInMenu: true, - parentKeys: ["/governance"], + parentKeys: ['/governance'], }, { - path: "/governance/activation", - component: "./governance/activation", + path: '/governance/activation', + component: './governance/activation', hideInMenu: true, - parentKeys: ["/governance"], + parentKeys: ['/governance'], }, { - path: "/deployments", - name: "Deployments", - component: "./Deployments", - menuGroupKey: "platform", + path: '/deployments', + name: 'Deployments', + component: './Deployments', + menuGroupKey: 'platform', }, { - path: "/runtime/explorer", - name: "Topology", - component: "./actors", - menuGroupKey: "platform", + path: '/runtime/explorer', + name: 'Topology', + component: './actors', + menuGroupKey: 'platform', }, { - path: "/runtime/explorer/detail", - component: "./actors/detail", + path: '/runtime/explorer/detail', + component: './actors/detail', hideInMenu: true, - parentKeys: ["/runtime/explorer"], + parentKeys: ['/runtime/explorer'], }, { - path: "/runtime/gagents", - name: "Members", - component: "./gagents", + path: '/runtime/gagents', + name: 'Members', + component: './gagents', hideInMenu: true, }, { - path: "/scopes/overview", - component: "./scopes/overview", + path: '/scopes/overview', + component: './scopes/overview', hideInMenu: true, }, { - path: "/settings", - name: "Settings", - component: "./settings", - menuGroupKey: "settings", + path: '/settings', + name: 'Settings', + component: './settings', + menuGroupKey: 'settings', }, { - path: "/scopes/workflows", - redirect: "/runtime/workflows", + path: '/scopes/workflows', + redirect: '/runtime/workflows', hideInMenu: true, }, { - path: "/scopes/scripts", - redirect: "/studio?tab=scripts", + path: '/scopes/scripts', + redirect: '/studio?tab=scripts', hideInMenu: true, }, { - path: "/governance/audit", - redirect: "/governance?view=changes", + path: '/governance/audit', + redirect: '/governance?view=changes', hideInMenu: true, }, { - path: "/workflows", - redirect: "/runtime/workflows", + path: '/workflows', + redirect: '/runtime/workflows', hideInMenu: true, }, { - path: "/primitives", - redirect: "/runtime/primitives", + path: '/primitives', + redirect: '/runtime/primitives', hideInMenu: true, }, { - path: "/runs", - redirect: "/runtime/runs", + path: '/runs', + redirect: '/runtime/runs', hideInMenu: true, }, { - path: "/actors", - redirect: "/runtime/explorer", + path: '/actors', + redirect: '/runtime/explorer', hideInMenu: true, }, { - path: "/gagents", - redirect: "/runtime/gagents", + path: '/gagents', + redirect: '/runtime/gagents', hideInMenu: true, }, { - path: "/mission-control", - redirect: "/runtime/mission-control", + path: '/mission-control', + redirect: '/runtime/mission-control', hideInMenu: true, }, { - path: "/mission-wall", - redirect: "/runtime/mission-wall", + path: '/mission-wall', + redirect: '/runtime/mission-wall', hideInMenu: true, }, { - path: "/", - redirect: "/scopes", + path: '/', + redirect: '/scopes', }, { - component: "404", + component: '404', layout: false, - path: "/*", + path: '/*', }, ]; diff --git a/apps/aevatar-console-web/docs/superpowers/plans/2026-08-04-workflow-activity-vnext-implementation.md b/apps/aevatar-console-web/docs/superpowers/plans/2026-08-04-workflow-activity-vnext-implementation.md new file mode 100644 index 0000000000..627f4ae6ab --- /dev/null +++ b/apps/aevatar-console-web/docs/superpowers/plans/2026-08-04-workflow-activity-vnext-implementation.md @@ -0,0 +1,99 @@ +# Workflow Activity vNext Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver every approved Workflow Activity vNext user path under the isolated scoped route namespace using only authoritative backend data and existing authentication/localization behavior. + +**Architecture:** The route package owns page composition and transient interaction state. Existing Studio, scope, runtime, auth, and Settings adapters remain authoritative; one new typed adapter owns observatory and fork transport decoding. TanStack Query keys include the route scope and normalized filters, while accepted-to-readable and accepted-to-observed transitions use explicit receipt-bound state machines. + +**Tech Stack:** React 19, TypeScript, Umi Max, Ant Design, TanStack Query, XYFlow/GraphCanvas, Jest, Testing Library, Biome. + +--- + +### Task 1: Isolated routes and typed transport boundary + +**Files:** +- Modify: `config/routes.ts` +- Modify: `src/routesConfig.test.ts` +- Create: `src/shared/models/workflowActivity.ts` +- Create: `src/shared/api/workflowActivityApi.ts` +- Create: `src/shared/api/workflowActivityApi.test.ts` + +- [ ] Write route assertions for all seven hidden vNext routes, literal `workflows/new` ordering, namespace-only redirect, and unchanged legacy redirects; run the single route test and confirm RED. +- [ ] Add precise observatory summary/detail/graph, filter, and fork receipt models plus decoders that preserve unknown status values and reject malformed required identity/version fields. +- [ ] Write adapter tests for encoded scope/filter queries, independent detail/graph requests, 401/403/404 propagation, fork request identity, and receipt decoding; run and confirm RED before implementation. +- [ ] Implement the seven route records and typed adapter, then rerun only `src/routesConfig.test.ts` and `src/shared/api/workflowActivityApi.test.ts` to GREEN. + +### Task 2: Scoped shell, catalogue, and direct creation + +**Files:** +- Create: `src/pages/workflow-activity-vnext/index.tsx` +- Create: `src/pages/workflow-activity-vnext/WorkflowActivityVNextShell.tsx` +- Create: `src/pages/workflow-activity-vnext/navigation.ts` +- Create: `src/pages/workflow-activity-vnext/workflows/WorkflowsPage.tsx` +- Create: `src/pages/workflow-activity-vnext/workflows/NewWorkflowPage.tsx` +- Create: `src/pages/workflow-activity-vnext/workflows/workflowCreation.ts` +- Create: `src/pages/workflow-activity-vnext/hooks/useDraftMaterialization.ts` +- Create: `src/pages/workflow-activity-vnext/index.test.tsx` + +- [ ] Write route integration tests for authoritative catalogue loading, successful empty data, partial-source failure, search, and real-ID navigation; run the new page test and confirm RED. +- [ ] Write creation tests for Describe, blank, YAML import, and bundled versioned template paths. Cover materialized and `202 projection_pending` responses, bounded `404`, preserved receipt/input, retrying the same GET, and duplicate-submit prevention; confirm RED. +- [ ] Implement the local rail/mobile navigation with existing `ConsoleLanguageSwitch` and `ConsoleAuthActions`, two-source catalogue, four direct creation forms, and receipt-bound materialization hook. +- [ ] Rerun only the new route test after each user-path slice until UP-00 through UP-05 are GREEN. + +### Task 3: Common editor, first save, and draft execution + +**Files:** +- Create: `src/pages/workflow-activity-vnext/workflows/WorkflowEditorPage.tsx` +- Create: `src/pages/workflow-activity-vnext/hooks/useWorkflowEditor.ts` +- Create: `src/pages/workflow-activity-vnext/hooks/useDraftRun.ts` +- Reuse: `src/pages/team-member-workflow-studio/components/WorkflowStudioCanvas.tsx` +- Reuse: `src/pages/team-member-workflow-studio/components/WorkflowStudioNodeLibrary.tsx` +- Reuse: `src/pages/team-member-workflow-studio/components/WorkflowStudioNodeDetailPanel.tsx` +- Reuse: `src/pages/team-member-workflow-studio/components/WorkflowStudioYamlPanel.tsx` +- Reuse: `src/pages/team-member-workflow-studio/components/WorkflowStudioDraftRunPanel.tsx` + +- [ ] Write editor tests for exact draft loading, committed fallback, dirty state, YAML/canvas shared state, validation findings, existing-draft PUT, and committed-only first-save POST with returned-ID route replacement; confirm RED. +- [ ] Implement the editor hook and page using existing Studio graph/document helpers without member APIs or member identity. +- [ ] Write draft-run tests for real serialized YAML, stable submission, accepted/running state, disconnected/error state, no Activity completion claim, and general Open Activity when no trustworthy run ID exists; confirm RED. +- [ ] Implement `runtimeRunsApi.streamDraftRun` integration and authoritative stream presentation, then rerun the route test to GREEN for UP-06 and UP-07. + +### Task 4: Activity observation, ledger, detail, and recovery + +**Files:** +- Create: `src/pages/workflow-activity-vnext/activity/ActivityPage.tsx` +- Create: `src/pages/workflow-activity-vnext/activity/RunDetailPage.tsx` +- Create: `src/pages/workflow-activity-vnext/activity/runRecovery.ts` +- Create: `src/pages/workflow-activity-vnext/hooks/useRunObservation.ts` + +- [ ] Write tests for URL-backed server filters, workflow definition resolution, filter-unavailable fallback, loading/empty/error/unknown statuses, and recent-window wording; confirm RED. +- [ ] Implement the Activity query and ledger without name joins, local totals, revisions, duration, usage, outcome, or Needs-you filtering. +- [ ] Write tests for independent detail/graph loading, safe not-found, partial running detail, graph-only failure, failed-step eligibility, first-executable-step eligibility, immutable source, fork receipt display, and no `newRunActorId` detail navigation; confirm RED. +- [ ] Implement detail, graph, Retry, Run again, and bounded observation, then rerun the route test to GREEN for UP-08 through UP-12. + +### Task 5: Settings, identity, responsive behavior, and locale + +**Files:** +- Create: `src/pages/workflow-activity-vnext/settings/SettingsPage.tsx` +- Create: `src/pages/workflow-activity-vnext/styles.ts` +- Modify: `src/locales/en-US.ts` +- Modify: `src/locales/zh-CN.ts` +- Modify: `src/locales/projectMessages.en-US.ts` +- Modify: `src/locales/projectMessages.zh-CN.ts` +- Modify: `src/locales/catalog.test.ts` + +- [ ] Write tests for real LLM loading/dirty/accepted/observed/catalogue-unavailable/save-failure states, auth-me identity/expiry, existing auth actions, runtime loading/unavailable, and no localStorage authority; confirm RED. +- [ ] Implement AI Defaults by reusing `userLlmSelection.ts` and `observeUserLlmSave`, Account through existing session/actions, and Advanced through `getUserConfigRuntime`. +- [ ] Add every new message to both locale catalogues and verify catalogue parity with the single locale test. +- [ ] Add responsive Operational Automation Ledger styles and test keyboard-visible controls, semantic status, dialogs, long identities, and mobile section navigation; rerun only the new page and locale tests to GREEN for UP-13 through UP-16. + +### Task 6: Focused verification and delivery + +**Files:** +- Review all files changed relative to `origin/feat/2026-08-04_workflow-activity-vnext` + +- [ ] Run `frontend_change_scope.py --base origin/feat/2026-08-04_workflow-activity-vnext`, explicitly execute every changed/new Jest file and dependency-related tests, then run Biome only on reported static-check files. +- [ ] Because the user explicitly requested full TypeScript and production-build validation, run the package `tsc` and `build` with `CODEX_ALLOW_FULL_FRONTEND_VALIDATION=1`; do not run the complete Jest suite. +- [ ] Run the design baseline verifier and confirm the declared SHA, byte-identical generator output, and 17/17 frames. +- [ ] After the environment synchronizer succeeds, verify real authenticated desktop/tablet/mobile routes in-browser and capture screenshots. If the synchronizer/backend/OAuth environment remains unavailable, record the exact gap without mocks. +- [ ] Review the complete frontend-only diff, stage only this task's files, commit with an imperative message, push, and create a Draft implementation PR without modifying PR #3187 or enabling auto-merge. diff --git a/apps/aevatar-console-web/docs/superpowers/plans/2026-08-05-workflow-activity-vnext-nyxid-visual-alignment.md b/apps/aevatar-console-web/docs/superpowers/plans/2026-08-05-workflow-activity-vnext-nyxid-visual-alignment.md new file mode 100644 index 0000000000..a9e153c316 --- /dev/null +++ b/apps/aevatar-console-web/docs/superpowers/plans/2026-08-05-workflow-activity-vnext-nyxid-visual-alignment.md @@ -0,0 +1,131 @@ +# Workflow Activity vNext NyxID Visual Alignment Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Align every Workflow Activity vNext page with NyxID's measured layout, typography, density, table, form, and responsive patterns while preserving Aevatar colors and all existing product behavior. + +**Architecture:** Keep the current route and component ownership. Centralize the measured visual contract in `styles.ts`, make `WorkflowActivityVNextShell.tsx` own the 52px top bar and responsive navigation, and limit page edits to semantic class hooks or removal of conflicting inline styles. No API, identity, query, routing, locale, or workflow behavior changes are allowed. + +**Tech Stack:** React 19, TypeScript, Ant Design 6, CSS variables in the existing vNext style module, Jest/Testing Library for existing behavior coverage, Biome, and real-browser responsive verification. + +--- + +## Design baseline declaration + +```text +Design baseline: + apps/aevatar-console-web/docs/design-baselines/workflow-activity-vnext/ +Primary design: + aevatar-workflow-activity-vnext.excalidraw +Design SHA-256: + 30e74d7b410ae72c4c91432355436679033679c54c10b1702908435b001577de +Contract specification: + apps/aevatar-console-web/docs/superpowers/specs/ + 2026-08-04-workflow-activity-vnext-design.md +User paths: + apps/aevatar-console-web/docs/superpowers/specs/ + 2026-08-04-workflow-activity-vnext-user-paths.md +Authentication and localization: + Existing Aevatar login, callback, session, returnTo, and Umi locale logic; + presentation may change, behavior may not. +Production data source: + Real APIs and API-acknowledged user actions only; no mock fallback. +Baseline integrity: + python3 apps/aevatar-console-web/docs/design-baselines/ + workflow-activity-vnext/verify-baseline.py +``` + +### Task 1: Lock the measured design contract + +**Files:** +- Modify: `docs/canon/frontend-design.md` +- Create: `apps/aevatar-console-web/docs/superpowers/plans/2026-08-05-workflow-activity-vnext-nyxid-visual-alignment.md` + +- [x] Record the measured shell, typography, spacing, control, table, responsive, and accessibility rules. +- [x] Record the permitted Aevatar deviations: existing colors, AlibabaSans delivery, and the repository's 8px radius cap. +- [x] Update issue `#3194` with the measurement table, page inventory, and acceptance evidence. +- [x] Run `bash tools/docs/lint.sh` and `git diff --check`. +- [x] Commit the documentation as `Document NyxID visual alignment`. + +### Task 2: Align the shared shell and navigation + +**Files:** +- Modify: `src/pages/workflow-activity-vnext/WorkflowActivityVNextShell.tsx` +- Modify: `src/pages/workflow-activity-vnext/styles.ts` +- Test: `src/pages/workflow-activity-vnext/index.test.tsx` + +- [x] Preserve the existing semantic navigation and account/language components while moving their presentation into a 52px top bar. +- [x] Place the Aevatar brand and current breadcrumb in the top bar, start the 200px local rail below it, and render the page title inside the main content surface. +- [x] Replace the permanent mobile navigation strip with an accessible menu button and drawer using the same route builders. +- [x] Define shared spacing, type, radius, shadow, control, and table tokens in `.wa-vnext`. +- [x] Run the existing route integration test before and after the shell change to prove navigation, auth action reuse, and page behavior remain intact: + +```bash +pnpm exec jest --runInBand --runTestsByPath \ + src/pages/workflow-activity-vnext/index.test.tsx +``` + +### Task 3: Align Workflows and New workflow + +**Files:** +- Modify: `src/pages/workflow-activity-vnext/workflows/WorkflowsPage.tsx` +- Modify: `src/pages/workflow-activity-vnext/workflows/NewWorkflowPage.tsx` +- Modify: `src/pages/workflow-activity-vnext/styles.ts` +- Test: `src/pages/workflow-activity-vnext/index.test.tsx` + +- [x] Keep Workflow search and source filter in one responsive toolbar. +- [x] Render the Workflow table with a 32px header, 53-60px rows, one clear Open action, and the branch's existing accessible secondary actions. +- [x] Remove the mobile card transformation and use a named local horizontal scroll region. +- [x] Replace New workflow inline presentation styles with shared classes: four equal-level creation choices, then one form container for the selected method. +- [x] Verify Describe, blank, import, and template retain their current authoritative creation behavior through the existing focused test file. + +### Task 4: Align the Workflow editor + +**Files:** +- Modify: `src/pages/workflow-activity-vnext/workflows/WorkflowEditorPage.tsx` +- Modify: `src/pages/workflow-activity-vnext/styles.ts` +- Test: `src/pages/workflow-activity-vnext/index.test.tsx` + +- [x] Keep Canvas and YAML as a compact segmented mode control and preserve the existing shared editor state. +- [x] Keep the canvas unframed/full-width inside the work surface; remove conflicting fixed layout values where shared responsive tokens can own sizing. +- [x] Preserve visible Save, Run, Publish availability and the current node library/detail/YAML behavior. +- [x] At mobile width, keep editor actions reachable, allow horizontal action scrolling only inside the toolbar, and preserve the existing accessible node panel behavior. + +### Task 5: Align Activity and Run detail + +**Files:** +- Modify: `src/pages/workflow-activity-vnext/activity/ActivityPage.tsx` +- Modify: `src/pages/workflow-activity-vnext/activity/RunDetailPage.tsx` +- Modify: `src/pages/workflow-activity-vnext/styles.ts` +- Test: `src/pages/workflow-activity-vnext/activity/ActivityPage.test.tsx` +- Test: `src/pages/workflow-activity-vnext/activity/RunDetailPage.test.tsx` + +- [x] Apply the NyxID ledger density to Activity while preserving status, source, updated time, server filters, and the single detail target. +- [x] Keep mobile Activity as a semantic table in a bounded horizontal scroller rather than a card layout. +- [x] Move Run status and recovery actions close to the page title; use a 32px underline tab strip and single-layer detail sections. +- [x] Keep Steps and Diagnostics tables semantic and locally scrollable; preserve independent graph failure and immutable source behavior. +- [x] Run both focused behavior tests after the visual-only edits. + +### Task 6: Align Settings + +**Files:** +- Modify: `src/pages/workflow-activity-vnext/settings/SettingsPage.tsx` +- Modify: `src/pages/workflow-activity-vnext/styles.ts` +- Test: `src/pages/workflow-activity-vnext/index.test.tsx` + +- [x] Replace the desktop secondary side navigation with a 32px underline tab strip that remains horizontally scrollable on mobile. +- [x] Render each section in one bordered 8px container with a compact section header and 32-36px controls. +- [x] Preserve authoritative LLM clean/dirty/accepted/observed states, current account actions, and read-only advanced values. +- [x] Keep dirty actions at the bottom of the owning container and sticky only when needed; ensure they do not cover mobile inputs. + +### Task 7: Focused verification and delivery + +**Files:** +- Review every file changed by Tasks 1-6. + +- [x] Run the frontend scope analyzer with the task start commit as base and use only its changed source/test/static-check files. +- [x] Run directly changed tests and dependency-related Jest tests only. Do not run the full frontend suite. +- [x] Run Biome with explicit changed frontend file paths. Skip package-wide typecheck because no repository-native affected typecheck target exists; GitHub CI owns full type verification. +- [x] Run `bash tools/ci/test_stability_guards.sh`, `bash tools/docs/lint.sh`, baseline verification, and `git diff --check`. +- [x] Verify real pages at `1440x900`, `834x1112`, and `390x844`, including local table scroll and page-level overflow checks. +- [x] Review `git diff`, stage only this task's files, commit `Align Workflow Activity with NyxID`, push the existing feature branch, update issue `#3194`, and update PR `#3189` with exact focused commands and the GitHub CI delegation statement. diff --git a/apps/aevatar-console-web/docs/superpowers/plans/2026-08-05-workflow-list-filter-semantics.md b/apps/aevatar-console-web/docs/superpowers/plans/2026-08-05-workflow-list-filter-semantics.md new file mode 100644 index 0000000000..8d6575dd76 --- /dev/null +++ b/apps/aevatar-console-web/docs/superpowers/plans/2026-08-05-workflow-list-filter-semantics.md @@ -0,0 +1,408 @@ +# Workflow List Filter Semantics Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Align Workflow and Activity list filtering without inventing unsupported Workflow states. + +**Architecture:** Keep resource-specific filter semantics while sharing one toolbar interaction model. Workflows derives Draft membership only from the real scoped draft API; Activity keeps its real Run API filters, and both pages synchronize list state with the URL. + +**Tech Stack:** React 19, TypeScript, TanStack Query, Ant Design, Umi locale, Jest, Testing Library, Biome. + +--- + +## Task 1: Specify Workflow filter behavior in tests + +**Files:** + +- Modify: `src/pages/workflow-activity-vnext/index.test.tsx` +- Test: `src/pages/workflow-activity-vnext/index.test.tsx` + +- [ ] Replace the pathname-only test location with a pathname-plus-search snapshot so `useConsoleLocation()` sees copied Workflow URLs: + +```tsx +let mockLocation = '/scopes/scope-alpha/workflow-activity-vnext/workflows'; + +const readMockUrl = () => new URL(mockLocation, 'http://console.local'); + +useLocation: () => ({ + hash: '', + pathname: readMockUrl().pathname, + search: readMockUrl().search, +}), + +getLocationSnapshot: () => + `${readMockUrl().pathname}${readMockUrl().search}`, +``` + +- [ ] Add focused tests with distinct identities (`wf-draft-alpha`, `wf-committed-beta`) proving: + +```tsx +mockLocation = + '/scopes/scope-alpha/workflow-activity-vnext/workflows?q=support&view=drafts'; + +expect(await screen.findByText('Support triage')).toBeInTheDocument(); +expect(screen.queryByText('Invoice review')).not.toBeInTheDocument(); +expect(screen.getByRole('searchbox', { name: 'Search workflows' })).toHaveValue( + 'support', +); +expect(screen.getByRole('combobox', { name: 'Workflow view' })).toHaveTextContent( + 'Drafts', +); +``` + +```tsx +fireEvent.mouseDown(screen.getByRole('combobox', { name: 'Workflow view' })); +expect(await screen.findByRole('option', { name: 'All workflows' })).toBeVisible(); +expect(screen.getByRole('option', { name: 'Drafts' })).toBeVisible(); +expect(screen.queryByRole('option', { name: 'Committed' })).not.toBeInTheDocument(); +expect(screen.queryByRole('option', { name: 'Published' })).not.toBeInTheDocument(); +expect(screen.queryByRole('option', { name: 'Failing' })).not.toBeInTheDocument(); +``` + +```tsx +fireEvent.change(screen.getByRole('searchbox', { name: 'Search workflows' }), { + target: { value: 'invoice' }, +}); +await waitFor(() => + expect(history.replace).toHaveBeenLastCalledWith( + '/scopes/scope-alpha/workflow-activity-vnext/workflows?q=invoice', + ), +); +``` + +```tsx +mockStudioApi.listWorkflowDrafts.mockRejectedValue(new Error('draft source down')); +mockLocation = + '/scopes/scope-alpha/workflow-activity-vnext/workflows?view=drafts'; +expect(await screen.findByText('Draft workflows unavailable')).toBeInTheDocument(); +expect(screen.getByRole('button', { name: 'Retry workflows' })).toBeEnabled(); +expect(screen.queryByText('No workflows yet')).not.toBeInTheDocument(); +``` + +- [ ] Run the Workflow test and confirm RED because the view select, URL-backed search/view, clear action, and draft-unavailable state do not exist yet: + +```bash +pnpm exec jest src/pages/workflow-activity-vnext/index.test.tsx --runInBand +``` + +Expected: the newly added assertions fail for missing `Workflow view`, missing URL restoration, or missing draft-unavailable state; unrelated existing tests remain runnable. + +## Task 2: Implement the Workflow view filter and honest draft failure + +**Files:** + +- Modify: `src/pages/workflow-activity-vnext/workflows/WorkflowsPage.tsx` +- Modify: `src/locales/workflowActivityVNextMessages.en-US.ts` +- Modify: `src/locales/workflowActivityVNextMessages.zh-CN.ts` +- Test: `src/pages/workflow-activity-vnext/index.test.tsx` +- Test: `src/locales/catalog.test.ts` + +- [ ] Import `Select`, `useConsoleLocation`, and move Refresh into the shell header next to New workflow: + +```tsx +headerActions={ + + + + +} +``` + +- [ ] Restore `query` and `view` from `location.search`, synchronize browser navigation into state, and serialize only non-default values: + +```tsx +type WorkflowView = 'all' | 'drafts'; + +function readWorkflowView(params: URLSearchParams): WorkflowView { + return params.get('view') === 'drafts' ? 'drafts' : 'all'; +} + +const location = useConsoleLocation(); +const initialParams = React.useMemo( + () => new URLSearchParams(location.search), + [location.search], +); +const [query, setQuery] = React.useState(initialParams.get('q') ?? ''); +const [view, setView] = React.useState( + readWorkflowView(initialParams), +); + +React.useEffect(() => { + const params = new URLSearchParams(location.search); + setQuery(params.get('q') ?? ''); + setView(readWorkflowView(params)); +}, [location.search]); + +React.useEffect(() => { + const params = new URLSearchParams(); + if (query.trim()) params.set('q', query.trim()); + if (view === 'drafts') params.set('view', 'drafts'); + const suffix = params.toString(); + history.replace(`${location.pathname}${suffix ? `?${suffix}` : ''}`); +}, [location.pathname, query, view]); +``` + +- [ ] Build draft membership exclusively from exact API-returned Workflow IDs and apply it before text search: + +```tsx +const draftWorkflowIds = React.useMemo( + () => new Set((drafts.data ?? []).map((item) => item.workflowId)), + [drafts.data], +); + +return [...merged.values()] + .filter((item) => view !== 'drafts' || draftWorkflowIds.has(item.workflowId)) + .filter((item) => { + const normalized = query.trim().toLowerCase(); + return ( + !normalized || + [item.name, item.description, item.workflowId].some((value) => + value.toLowerCase().includes(normalized), + ) + ); + }) + .sort(/* retain current updated-at ordering */); +``` + +- [ ] Add the two-option view Select. Disable only Drafts when the draft source failed; do not add Committed, Published, Failing, Ready, or inferred lifecycle values: + +```tsx + + + {/* page-specific Select controls */} + +``` + +- [ ] Add stable dimensions and mobile full-width behavior without changing other vNext tables or the shell: + +```css +.wa-vnext__toolbar-search { flex: 0 1 360px; max-width: 100%; width: 360px; } +.wa-vnext__toolbar-filters { justify-content: flex-end; } +.wa-vnext__toolbar-filters .ant-select { min-width: 160px; } + +@media (max-width: 600px) { + .wa-vnext__toolbar-search { flex-basis: auto; width: 100%; } + .wa-vnext__toolbar-filters { display: grid; grid-template-columns: 1fr; width: 100%; } + .wa-vnext__toolbar-filters .ant-select, + .wa-vnext__toolbar-filters .ant-space-item, + .wa-vnext__toolbar-filters .ant-btn { width: 100%; } +} +``` + +- [ ] Run the two page tests after the markup/style refactor: + +```bash +pnpm exec jest \ + src/pages/workflow-activity-vnext/index.test.tsx \ + src/pages/workflow-activity-vnext/activity/ActivityPage.test.tsx \ + --runInBand +``` + +Expected: both page test files pass. + +## Task 5: Focused verification, browser evidence, and delivery + +**Files:** + +- Verify only the files changed by Tasks 1-4 and this plan. + +- [ ] Run the exact selected test set: + +```bash +pnpm exec jest \ + src/pages/workflow-activity-vnext/index.test.tsx \ + src/pages/workflow-activity-vnext/activity/ActivityPage.test.tsx \ + src/locales/catalog.test.ts \ + --runInBand +``` + +- [ ] Run dependency-selected tests: + +```bash +pnpm exec jest --findRelatedTests \ + src/pages/workflow-activity-vnext/workflows/WorkflowsPage.tsx \ + src/pages/workflow-activity-vnext/activity/ActivityPage.tsx \ + src/pages/workflow-activity-vnext/styles.ts \ + src/locales/workflowActivityVNextMessages.en-US.ts \ + src/locales/workflowActivityVNextMessages.zh-CN.ts \ + --runInBand +``` + +- [ ] Compute the frontend-only change scope: + +```bash +python3 /Users/abigaildeng/.codex/skills/frontend-incremental-pr/scripts/frontend_change_scope.py \ + --repo . --base HEAD +``` + +- [ ] Run Biome only on the analyzer's exact changed frontend files, for example: + +```bash +pnpm exec biome check \ + src/pages/workflow-activity-vnext/workflows/WorkflowsPage.tsx \ + src/pages/workflow-activity-vnext/activity/ActivityPage.tsx \ + src/pages/workflow-activity-vnext/index.test.tsx \ + src/pages/workflow-activity-vnext/activity/ActivityPage.test.tsx \ + src/pages/workflow-activity-vnext/styles.ts \ + src/locales/workflowActivityVNextMessages.en-US.ts \ + src/locales/workflowActivityVNextMessages.zh-CN.ts +``` + +- [ ] Run whitespace validation: + +```bash +git diff --check +``` + +- [ ] Do not run the full frontend test suite, package-wide lint, package-wide typecheck, or production build locally. Record that GitHub CI owns full verification under the machine-wide `frontend-incremental-pr` policy. + +- [ ] Verify the already-running real frontend at `http://localhost:5173` without mock data at desktop `1440x900`, tablet `834x1112`, and mobile `390x844`. Capture screenshots proving the resource-specific filters, header Refresh placement, URL restoration, responsive full-width controls, and lack of horizontal overflow. Record any real remote API or authentication gap instead of synthesizing success. + +- [ ] Review `git diff --stat`, `git diff`, and `git status --short`. Confirm every changed path is inside `apps/aevatar-console-web/`, no legacy route/auth/menu/API adapter changed, and no unsupported Workflow state or identity conversion was introduced. + +- [ ] Stage only this task's files, commit with: + +```bash +git commit -m "Align Workflow and Activity filters" +``` + +- [ ] Push `feat/2026-08-04_workflow-activity-vnext-implementation` and update Draft PR #3189 with exact focused commands/results plus the CI-delegated full checks. Keep PR #3189 Draft. Do not change PR #3187, enable auto-merge, or merge either PR. diff --git a/apps/aevatar-console-web/docs/superpowers/plans/2026-08-06-workflow-activity-vnext-scoped-publication.md b/apps/aevatar-console-web/docs/superpowers/plans/2026-08-06-workflow-activity-vnext-scoped-publication.md new file mode 100644 index 0000000000..e22c26fdd8 --- /dev/null +++ b/apps/aevatar-console-web/docs/superpowers/plans/2026-08-06-workflow-activity-vnext-scoped-publication.md @@ -0,0 +1,228 @@ +# Workflow Activity vNext Scoped Publication Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Let an operator publish a saved, valid vNext Workflow to one explicitly selected real scope service, then observe the accepted update through exact workflow and service-revision reads. + +**Architecture:** A vNext-local publication hook owns the accepted receipt and bounded observation state. A focused dialog loads only real scope services, requires a deliberate service selection, and collects any real external-request confirmations before calling the existing typed `studioApi.saveAndBindWorkflow` adapter. The editor remains the owner of document validation and serialization; no Team/member API, inferred service identity, mock data, or browser-storage state is introduced. + +**Tech Stack:** React 19, TypeScript, Ant Design, TanStack Query, existing Studio/scope-runtime adapters, Jest, Testing Library, Biome. + +**Design baseline:** + +```text +Design baseline: + apps/aevatar-console-web/docs/design-baselines/workflow-activity-vnext/ +Primary design: + aevatar-workflow-activity-vnext.excalidraw +Design SHA-256: + 30e74d7b410ae72c4c91432355436679033679c54c10b1702908435b001577de +Contract specification: + apps/aevatar-console-web/docs/superpowers/specs/ + 2026-08-04-workflow-activity-vnext-design.md +User paths: + apps/aevatar-console-web/docs/superpowers/specs/ + 2026-08-04-workflow-activity-vnext-user-paths.md +Production data source: + Real APIs and API-acknowledged user actions only; no mock fallback. +``` + +--- + +### Task 1: Receipt-Bound Publication Observation + +**Files:** +- Create: `src/pages/workflow-activity-vnext/hooks/useWorkflowPublication.ts` +- Create: `src/pages/workflow-activity-vnext/hooks/useWorkflowPublication.test.ts` + +- [ ] **Step 1: Write failing observation tests** + +Cover these public behaviors with distinct fixtures: + +```ts +it('observes only the accepted workflow, service, and revision once the active serving revision is published', async () => { + const result = await observeWorkflowPublication({ + receipt: { scopeId: 'scope-alpha', workflowId: 'wf-alpha', revisionId: 'rev-alpha', serviceId: 'svc-alpha' }, + readWorkflow: async () => readableWorkflowDetail('scope-alpha', 'wf-alpha'), + readRevisions: async () => revisionCatalog('scope-alpha', 'svc-alpha', publishedActiveRevision('rev-alpha')), + delaysMs: [0], + }); + + expect(result.kind).toBe('observed'); +}); + +it('treats receipt-bound workflow 404 and 409 plus a missing revision as observation delay without creating another publish request', async () => { + const result = await observeWorkflowPublication({ + receipt: { scopeId: 'scope-alpha', workflowId: 'wf-alpha', revisionId: 'rev-alpha', serviceId: 'svc-alpha' }, + readWorkflow: async () => { throw httpStatusError(409); }, + readRevisions: async () => revisionCatalog('scope-alpha', 'svc-alpha', null), + delaysMs: [0], + }); + + expect(result).toEqual({ kind: 'delayed' }); +}); +``` + +- [ ] **Step 2: Run the new observation test to verify it fails** + +Run: + +```bash +pnpm --dir apps/aevatar-console-web exec jest --runInBand --runTestsByPath src/pages/workflow-activity-vnext/hooks/useWorkflowPublication.test.ts +``` + +Expected: FAIL because `observeWorkflowPublication` and its receipt-bound observation behavior do not exist. + +- [ ] **Step 3: Implement the minimal observation hook** + +Export a `WorkflowPublicationReceipt` with only API-returned `scopeId`, `workflowId`, `revisionId`, and `serviceId`. `observeWorkflowPublication` must query the exact `scopesApi.getWorkflowDetail` and `scopeRuntimeApi.getServiceRevisions` identities in parallel on each bounded attempt. Return `observed` only when: + +```ts +workflow.available === true && +workflow.scopeId === receipt.scopeId && +workflow.workflow?.workflowId === receipt.workflowId && +catalog.scopeId === receipt.scopeId && +catalog.serviceId === receipt.serviceId && +catalog.activeServingRevisionId === receipt.revisionId && +revision.revisionId === receipt.revisionId && +revision.implementationKind === 'workflow' && +normalize(revision.status) === 'published' && +revision.isActiveServing && +revision.isServingTarget && +revision.allocationWeight > 0 && +normalize(revision.servingState) === 'active' +``` + +Treat workflow `404`/`409`, service catalog `404`, and a catalog lacking the exact revision as eventual-consistency observation states. Map `401` and `403` distinctly. Treat `PreparationFailed`, a nonempty service failure reason, and `Retired` as terminal failure. The hook retry calls only the exact read functions and never calls `saveAndBindWorkflow`. + +- [ ] **Step 4: Run the observation test to verify it passes** + +Run the same Jest command from Step 2. + +Expected: PASS with no timer-driven artificial success state. + +### Task 2: Explicit Target Selection And External-Request Review + +**Files:** +- Create: `src/pages/workflow-activity-vnext/workflows/WorkflowPublishDialog.tsx` +- Modify: `src/locales/workflowActivityVNextMessages.en-US.ts` +- Modify: `src/locales/workflowActivityVNextMessages.zh-CN.ts` +- Test: `src/pages/workflow-activity-vnext/index.test.tsx` + +- [ ] **Step 1: Write failing route-integration tests** + +Add tests that render the real vNext editor through its route owner and prove: + +```ts +it('requires an explicitly selected real scope service before publishing a saved workflow', async () => { + // Open Publish, wait for real service rows, and assert the primary submit action is disabled. + // Select 'Service alpha', submit, and assert saveAndBindWorkflow receives serviceId: 'svc-alpha'. +}); + +it('keeps publish accepted while the exact workflow and service revision are still being observed', async () => { + // Return 202 data, hold or delay the exact GETs, and assert no success toast or ready claim. +}); + +it('retries only observation reads after a delayed publication', async () => { + // Make the receipt-bound reads return 404/409, click Check again, and assert one POST total. +}); + +it('renders unauthorized and forbidden publication observation states distinctly', async () => { + // Exercise 401 and 403 from an exact observation read through the rendered alert. +}); +``` + +- [ ] **Step 2: Run only the named new route tests to verify they fail** + +Run: + +```bash +pnpm --dir apps/aevatar-console-web exec jest --runInBand --runTestsByPath src/pages/workflow-activity-vnext/index.test.tsx --testNamePattern 'explicitly selected real scope service|publish accepted|retries only observation reads|unauthorized and forbidden publication' +``` + +Expected: FAIL because the editor's current Publish action is disabled and no publication dialog or observation UI exists. + +- [ ] **Step 3: Implement the focused dialog** + +The dialog must load `scopeRuntimeApi.listServices(scopeId, { take: 200 })` only while open. It begins with no selection, uses real service display names, and has distinct loading, empty, failure, retry, unauthorized, and forbidden content. It must never choose or submit a default service on the user's behalf. + +On submission, use an exact editor-generated publication snapshot and call `studioApi.previewExplicitRequests`. When preview items exist, render an in-dialog review using only relevant user decisions: method/path, risk, and whether approval is required. Keep call-site IDs and request digests out of the default UI; use them only to construct the confirmation payload. A user cancellation returns to the target-selection state without making a publish POST. + +- [ ] **Step 4: Run the named route tests to verify they pass** + +Run the command from Step 2. + +Expected: PASS; each test uses a different `workflowId`, `serviceId`, and `revisionId` fixture identity. + +### Task 3: Editor Integration And Honest User Feedback + +**Files:** +- Modify: `src/pages/workflow-activity-vnext/hooks/useWorkflowEditor.ts` +- Modify: `src/pages/workflow-activity-vnext/workflows/WorkflowEditorPage.tsx` +- Modify: `src/pages/workflow-activity-vnext/workflows/WorkflowPublishDialog.tsx` +- Modify: `src/locales/workflowActivityVNextMessages.en-US.ts` +- Modify: `src/locales/workflowActivityVNextMessages.zh-CN.ts` +- Test: `src/pages/workflow-activity-vnext/index.test.tsx` + +- [ ] **Step 1: Add the smallest editor publication preparation boundary** + +Expose a function from `useWorkflowEditor` that parses and serializes the current document only when it is saved, valid, and a real draft. It returns the exact draft `workflowId`, display name, and serialized YAML, or an actionable validation failure. It must not fabricate a revision, modify the current draft, or save silently. + +- [ ] **Step 2: Wire the dialog to the existing typed adapters** + +Use `createWorkflowRevisionIdentityCandidate()` only as the required request identity candidate. Pass the selected service ID exactly to `studioApi.saveAndBindWorkflow`; capture only API-returned `workflowId`, `revisionId`, and `binding.serviceId` as the observation receipt. Reject a missing binding service ID, scope mismatch, or returned target mismatch as a visible publish failure rather than substituting any identity. + +- [ ] **Step 3: Render persistent state and one completed-action toast** + +Keep the receipt-bound state in the editor while it is mounted: + +```text +Ready -> Reviewing -> Submitting -> Accepted -> Observing -> Published + | | + | +-> Delayed -> Check again (GET only) + +-> Failed / Unauthorized / Forbidden +``` + +Use inline alerts for every nonterminal phase. Emit one localized `ConsoleToast` success only after exact workflow and active service-revision evidence reaches `Published`; do not toast for a click, preview request, or `202 Accepted` response. Never claim full invocation readiness because no public HTTP contract proves it. + +- [ ] **Step 4: Run the focused route and hook tests to verify the integrated flow passes** + +Run: + +```bash +pnpm --dir apps/aevatar-console-web exec jest --runInBand --runTestsByPath src/pages/workflow-activity-vnext/index.test.tsx src/pages/workflow-activity-vnext/hooks/useWorkflowPublication.test.ts +``` + +Expected: PASS. + +### Task 4: Focused Verification And Delivery + +**Files:** +- Review only the current task's vNext hook, dialog, editor, locale, test, and plan files. + +- [ ] **Step 1: Re-run the frontend scope analyzer** + +Run: + +```bash +python3 /Users/abigaildeng/.codex/skills/frontend-incremental-pr/scripts/frontend_change_scope.py --repo . --base origin/feat/2026-08-04_workflow-activity-vnext +``` + +- [ ] **Step 2: Run dependency-related tests and changed-file static checks** + +Run explicit changed/new Jest tests plus `pnpm exec jest --findRelatedTests` only for changed shared source files when the analyzer identifies a direct consumer graph. Run `pnpm exec biome check` only with the analyzer's `staticCheckFiles` that belong to this task. + +- [ ] **Step 3: Verify baseline and diff hygiene** + +Run: + +```bash +python3 apps/aevatar-console-web/docs/design-baselines/workflow-activity-vnext/verify-baseline.py +git diff --check -- apps/aevatar-console-web +``` + +Expected: declared SHA, byte-identical generator output, and 17/17 frame inventory pass; no whitespace errors. + +- [ ] **Step 4: Review and deliver only task-owned changes** + +Review the complete diff. Stage only the publication task's frontend files, commit with an imperative message, push the implementation branch, and create or update the implementation Draft PR. Do not modify, ready, merge, or auto-merge PR #3187. Full frontend suite, package-wide lint, package-wide TypeScript, and production build are delegated to GitHub CI under the personal incremental frontend policy. diff --git a/apps/aevatar-console-web/docs/superpowers/specs/2026-08-05-workflow-list-filter-semantics-design.md b/apps/aevatar-console-web/docs/superpowers/specs/2026-08-05-workflow-list-filter-semantics-design.md new file mode 100644 index 0000000000..80b46a64cc --- /dev/null +++ b/apps/aevatar-console-web/docs/superpowers/specs/2026-08-05-workflow-list-filter-semantics-design.md @@ -0,0 +1,135 @@ +# Workflow List Filter Semantics Design + +## Status + +Approved in conversation on 2026-08-05. This document records the product +semantics for aligning the Workflow and Activity list toolbars before the +frontend implementation changes. + +This design supplements, but does not modify, the Workflow Activity vNext +design baseline. Real backend responses remain authoritative over prototype +labels and demonstration data. + +## Mismatch + +The UI currently makes the two list pages appear inconsistent: Activity has +categorical filters while Workflows exposes only search and refresh. Copying +Activity's filters into Workflows would still be wrong because the pages own +different resources and facts. + +- Activity lists Runs. Its status and source are authoritative Run API fields. +- Workflows merges the scoped Workflow list with the scoped draft list. + `draft` and `committed` describe frontend merge inputs, not two mutually + exclusive product lifecycle states. +- A Workflow can have both a committed source and a draft. The frontend must + not present `Committed` as the opposite of `Draft`. +- The current backend does not provide the published identity, last-Run + outcome, or other facts required to implement the prototype's `Published` + and `Failing` Workflow filters honestly. + +The semantic owner of Workflow filtering is therefore the Workflow catalogue, +using only facts returned by its existing scoped Workflow and draft APIs. Run +status and source remain owned by Activity. + +## Decision + +Both pages use the same toolbar grammar: + +- search on the left; +- resource-specific categorical filters on the right; +- refresh in the page header rather than mixed into the filter group; +- active search and filter values represented in the URL; +- an honest filtered-empty state with a clear-filters action. + +Activity keeps its existing Run status and Run source filters. + +Workflows adds one select with two options: + +| Option | Product meaning | Evidence | +| --- | --- | --- | +| `All workflows` | Every row produced by the existing merge of scoped Workflows and scoped drafts | Successful results from the two existing list APIs | +| `Drafts` | Workflows whose exact `workflowId` is present in `studioApi.listWorkflowDrafts(scopeId)` | A real draft summary returned by that API | + +The Workflows filter must not include `Committed`, `Published`, `Failing`, +`Ready`, or similar values until a real contract supplies an unambiguous, +user-facing fact for that option. + +## Data And URL Behavior + +The existing Workflow merge remains authoritative and unchanged: + +1. Load the scoped committed Workflow summaries. +2. Load the scoped draft summaries. +3. Merge by exact `workflowId` without inferring identity from names, prefixes, + route position, member IDs, or service IDs. +4. When `view=drafts`, retain only merged rows whose exact `workflowId` was + returned by the draft API. +5. Apply the text search to the selected view and keep the existing updated-at + ordering. + +The Workflows URL uses: + +- `q=` for non-empty search text; +- `view=drafts` for the Drafts view; +- no `view` parameter for All workflows. + +Activity adds its non-empty search text as `q=` while preserving its +existing `status`, `origin`, `definition`, and `workflowFilter` parameters. +Empty values are omitted. Reloading, browser navigation, or returning to a +copied URL restores the same visible search and filters. + +## Loading, Failure, And Empty States + +- While either Workflow source is loading, retain the existing loading state. +- If the draft source fails, the Drafts option is disabled because the + frontend cannot know which Workflows have drafts. +- A draft-source failure does not hide successfully returned scoped Workflow + rows in All workflows. +- If the user was already viewing Drafts when the draft source fails, show an + unavailable state with Retry rather than an empty result. +- A successful filtered result with zero rows shows `No matching workflows` + and a `Clear filters` action. +- Clearing filters removes `q` and `view` while preserving the scoped route. +- No mock rows, localStorage state, timer results, or successful fallback may + stand in for either API. + +## Presentation And Accessibility + +- Workflows and Activity use the same toolbar spacing, search width, select + height, responsive wrapping, and mobile full-width controls. +- Selects retain visible accessible names through existing localized labels. +- The Drafts option is a normal menu option, not a status badge or lifecycle + claim. +- Keyboard, touch, and screen-reader behavior continue through Ant Design's + existing Input and Select controls. +- All new copy is added to both the `en-US` and `zh-CN` catalogues. + +## Test Contract + +Focused tests must prove that: + +- Workflows filters Drafts by exact draft API membership while All workflows + still contains committed-only and draft-backed rows. +- Workflows never renders unsupported `Committed`, `Published`, or `Failing` + filter options. +- Workflows search and view values restore from and write to the URL. +- Activity search joins its existing URL-backed filters without dropping + `status`, `origin`, `definition`, or `workflowFilter`. +- A failed draft source disables or makes the Drafts view unavailable instead + of presenting a false empty state. +- Clearing filters restores the unfiltered list and scoped URL. +- Both locale catalogues contain every new message key. + +Browser verification uses the running frontend with the real remote backend at +desktop, tablet, and mobile widths. If a remote endpoint is unavailable, the +unavailable state is recorded as a verification gap; test fixtures are not +used to fabricate browser evidence. + +## Non-Goals + +- No backend endpoint, DTO, projection, identity, or persistence change. +- No change to existing Workflow, Run, Settings, Studio, Team, member, login, + callback, redirect, session, menu, or locale route behavior. +- No attempt to implement prototype-only Published or Failing filters. +- No identity conversion among `workflowId`, `memberId`, + `definitionActorId`, or `publishedServiceId`. diff --git a/apps/aevatar-console-web/src/app.layout.test.ts b/apps/aevatar-console-web/src/app.layout.test.ts index 8f1f79d89b..ec2fa339d5 100644 --- a/apps/aevatar-console-web/src/app.layout.test.ts +++ b/apps/aevatar-console-web/src/app.layout.test.ts @@ -1,16 +1,22 @@ -import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { getLocale, setLocale } from "@umijs/max"; -import React from "react"; -import defaultSettings from "../config/defaultSettings"; -import { layout } from "./app"; - -describe("layout menu collapse behavior", () => { +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { getLocale, setLocale } from '@umijs/max'; +import React from 'react'; +import defaultSettings from '../config/defaultSettings'; +import { layout } from './app'; + +describe('layout menu collapse behavior', () => { beforeEach(() => { - setLocale("en-US", false); - window.history.replaceState({}, "", "/scopes"); + setLocale('en-US', false); + window.history.replaceState({}, '', '/scopes'); }); - it("keeps grouped navigation titles hidden in collapsed mode", () => { + it('keeps grouped navigation titles hidden in collapsed mode', () => { const runtimeLayout = layout({ initialState: { auth: {} as never, @@ -22,15 +28,15 @@ describe("layout menu collapse behavior", () => { collapsedWidth: 40, collapsedShowGroupTitle: false, collapsedShowTitle: false, - type: "group", + type: 'group', }); }); - it("collapses the global menu for Studio create-member intent", () => { + it('collapses the global menu for Studio create-member intent', () => { window.history.replaceState( {}, - "", - "/studio?tab=studio&intent=create-member", + '', + '/studio?tab=studio&intent=create-member', ); const runtimeLayout = layout({ @@ -44,8 +50,8 @@ describe("layout menu collapse behavior", () => { expect(runtimeLayout.collapsed).toBe(true); }); - it("leaves the global menu uncontrolled for ordinary Studio entry", () => { - window.history.replaceState({}, "", "/studio?tab=studio"); + it('leaves the global menu uncontrolled for ordinary Studio entry', () => { + window.history.replaceState({}, '', '/studio?tab=studio'); const runtimeLayout = layout({ initialState: { @@ -58,8 +64,8 @@ describe("layout menu collapse behavior", () => { expect(runtimeLayout.collapsed).toBeUndefined(); }); - it("hides console chrome for the fullscreen Mission Wall route", () => { - window.history.replaceState({}, "", "/runtime/mission-wall"); + it('hides console chrome for the fullscreen Mission Wall route', () => { + window.history.replaceState({}, '', '/runtime/mission-wall'); const runtimeLayout = layout({ initialState: { @@ -75,18 +81,51 @@ describe("layout menu collapse behavior", () => { | undefined; expect(runtimeLayout.headerRender).toBe(false); - expect(menuRender?.({}, React.createElement("nav"))).toBe(false); + expect(menuRender?.({}, React.createElement('nav'))).toBe(false); expect(actionsRender?.({}, {})).toEqual([]); expect(runtimeLayout.contentStyle).toMatchObject({ - background: "#09110f", - height: "100vh", - overflow: "hidden", + background: '#09110f', + height: '100vh', + overflow: 'hidden', padding: 0, }); }); - it("updates the controlled global menu collapse state after SPA route changes", () => { - window.history.replaceState({}, "", "/scopes/scope-a/teams"); + it('renders Workflow Activity vNext without the global console chrome', () => { + window.history.replaceState( + {}, + '', + '/scopes/scope-a/workflow-activity-vnext/workflows/wf-a', + ); + + const runtimeLayout = layout({ + initialState: { + auth: {} as never, + settings: defaultSettings, + }, + }); + const menuRender = runtimeLayout.menuRender as + | ((props: unknown, defaultDom: unknown) => React.ReactNode) + | undefined; + const actionsRender = runtimeLayout.actionsRender as + | ((props: unknown, dom: unknown) => React.ReactNode[]) + | undefined; + + expect(runtimeLayout.headerRender).toBe(false); + expect(menuRender?.({}, React.createElement('nav'))).toBe(false); + expect(actionsRender?.({}, {})).toEqual([]); + expect(runtimeLayout.contentStyle).toMatchObject({ + background: '#ffffff', + height: 'auto', + inset: 0, + overflow: 'hidden', + padding: 0, + position: 'fixed', + }); + }); + + it('updates the controlled global menu collapse state after SPA route changes', () => { + window.history.replaceState({}, '', '/scopes/scope-a/teams'); const teamsLayout = layout({ initialState: { auth: {} as never, @@ -94,7 +133,7 @@ describe("layout menu collapse behavior", () => { }, }); - window.history.pushState({}, "", "/studio?tab=studio&intent=create-member"); + window.history.pushState({}, '', '/studio?tab=studio&intent=create-member'); const studioLayout = layout({ initialState: { auth: {} as never, @@ -106,7 +145,7 @@ describe("layout menu collapse behavior", () => { expect(studioLayout.collapsed).toBe(true); }); - it("renders a global language switch in the layout actions", async () => { + it('renders a global language switch in the layout actions', async () => { const runtimeLayout = layout({ initialState: { auth: {} as never, @@ -117,24 +156,18 @@ describe("layout menu collapse behavior", () => { | ((props: unknown, dom: unknown) => React.ReactNode[]) | undefined; - render( - React.createElement( - React.Fragment, - null, - actionsRender?.({}, {}), - ), - ); + render(React.createElement(React.Fragment, null, actionsRender?.({}, {}))); - fireEvent.click(screen.getByRole("button", { name: "Switch language" })); - fireEvent.click(await screen.findByText("中文")); + fireEvent.click(screen.getByRole('button', { name: 'Switch language' })); + fireEvent.click(await screen.findByText('中文')); await waitFor(() => { - expect(getLocale()).toBe("zh-CN"); + expect(getLocale()).toBe('zh-CN'); }); }); - it("keeps page content in sync when the locale changes without a reload", async () => { - window.history.replaceState({}, "", "/studio"); + it('keeps page content in sync when the locale changes without a reload', async () => { + window.history.replaceState({}, '', '/studio'); const runtimeLayout = layout({ initialState: { auth: {} as never, @@ -149,34 +182,32 @@ describe("layout menu collapse behavior", () => { React.createElement( React.Fragment, null, - childrenRender?.( - React.createElement(LocalizedRuntimeProbe), - ), + childrenRender?.(React.createElement(LocalizedRuntimeProbe)), ), ); - expect(screen.getByText("My AI teams")).toBeTruthy(); + expect(screen.getByText('My AI teams')).toBeTruthy(); act(() => { - setLocale("zh-CN", false); + setLocale('zh-CN', false); }); await waitFor(() => { - expect(screen.getByText("我的 AI 团队")).toBeTruthy(); + expect(screen.getByText('我的 AI 团队')).toBeTruthy(); }); - expect(screen.queryByText("My AI teams")).toBeNull(); + expect(screen.queryByText('My AI teams')).toBeNull(); }); }); const LocalizedRuntimeProbe: React.FC = () => { - const { useIntl } = require("@umijs/max") as typeof import("@umijs/max"); + const { useIntl } = require('@umijs/max') as typeof import('@umijs/max'); const intl = useIntl(); return React.createElement( - "div", + 'div', null, intl.formatMessage({ - id: "teams.home.title", - }), + id: 'teams.home.title', + }), ); }; diff --git a/apps/aevatar-console-web/src/app.tsx b/apps/aevatar-console-web/src/app.tsx index c8263aa0ec..1c12ba0ffc 100644 --- a/apps/aevatar-console-web/src/app.tsx +++ b/apps/aevatar-console-web/src/app.tsx @@ -1,61 +1,70 @@ -import { ProConfigProvider } from "@ant-design/pro-components"; -import { QueryClientProvider } from "@tanstack/react-query"; -import { Badge, ConfigProvider } from "antd"; -import { getLocale, useIntl } from "@umijs/max"; -import React from "react"; -import MainLayout from "@/layouts/MainLayout"; -import { history } from "./shared/navigation/history"; -import { CONSOLE_HOME_ROUTE } from "@/shared/navigation/consoleHome"; -import BrandLogo from "@/components/BrandLogo"; -import defaultSettings from "../config/defaultSettings"; -import { errorConfig } from "./requestErrorConfig"; +import { ProConfigProvider } from '@ant-design/pro-components'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { getLocale, useIntl } from '@umijs/max'; +import { Badge, ConfigProvider } from 'antd'; +import React from 'react'; +import BrandLogo from '@/components/BrandLogo'; +import MainLayout from '@/layouts/MainLayout'; +import { buildMissionSnapshotFromRuntime } from '@/pages/MissionControl/runtimeAdapter'; +import { readMissionControlRouteContext } from '@/pages/MissionControl/services/api'; +import { runtimeActorsApi } from '@/shared/api/runtimeActorsApi'; +import { runtimeRunsApi } from '@/shared/api/runtimeRunsApi'; +import { + normalizeConsoleLocale, + resolveAntdLocale, + resolveProIntl, +} from '@/shared/i18n/localeProvider'; +import { CONSOLE_HOME_ROUTE } from '@/shared/navigation/consoleHome'; +import { loadRecentRuns } from '@/shared/runs/recentRuns'; +import { AevatarPageLoading } from '@/shared/ui/AevatarLoading'; +import { aevatarThemeConfig } from '@/shared/ui/aevatarWorkbench'; +import { ConsoleHeaderActions } from '@/shared/ui/ConsoleHeaderActions'; +import { ConsoleToastProvider } from '@/shared/ui/ConsoleToast'; +import defaultSettings from '../config/defaultSettings'; +import { errorConfig } from './requestErrorConfig'; import { ensureActiveAuthSession, hasRestorableAuthSession, -} from "./shared/auth/client"; -import { getNyxIDRuntimeConfig } from "./shared/auth/config"; +} from './shared/auth/client'; +import { getNyxIDRuntimeConfig } from './shared/auth/config'; +import { ProtectedRouteRedirectGate } from './shared/auth/ProtectedRouteRedirectGate'; import { buildAuthInitialState, - loadRestorableAuthSession, loadStoredAuthSession, sanitizeReturnTo, -} from "./shared/auth/session"; -import { ProtectedRouteRedirectGate } from "./shared/auth/ProtectedRouteRedirectGate"; +} from './shared/auth/session'; +import { history } from './shared/navigation/history'; import { getNavigationGroupOrder, type NavigationGroup, -} from "./shared/navigation/navigationGroups"; -import { getNavigationSelectedKeys } from "./shared/navigation/navigationMenuSelection"; +} from './shared/navigation/navigationGroups'; import { groupNavigationMenuItems, type NavigationMenuItem, -} from "./shared/navigation/navigationMenuGrouping"; -import { runtimeActorsApi } from "@/shared/api/runtimeActorsApi"; -import { runtimeRunsApi } from "@/shared/api/runtimeRunsApi"; -import { buildMissionSnapshotFromRuntime } from "@/pages/MissionControl/runtimeAdapter"; -import { readMissionControlRouteContext } from "@/pages/MissionControl/services/api"; -import { loadRecentRuns } from "@/shared/runs/recentRuns"; -import { queryClient } from "./shared/query/queryClient"; -import { aevatarThemeConfig } from "@/shared/ui/aevatarWorkbench"; -import { ConsoleHeaderActions } from "@/shared/ui/ConsoleHeaderActions"; -import { - normalizeConsoleLocale, - resolveAntdLocale, - resolveProIntl, -} from "@/shared/i18n/localeProvider"; -import { AevatarPageLoading } from "@/shared/ui/AevatarLoading"; +} from './shared/navigation/navigationMenuGrouping'; +import { getNavigationSelectedKeys } from './shared/navigation/navigationMenuSelection'; +import { queryClient } from './shared/query/queryClient'; -const PUBLIC_ROUTES = new Set(["/login", "/auth/callback"]); +const PUBLIC_ROUTES = new Set(['/login', '/auth/callback']); const DEFAULT_PROTECTED_ROUTE = CONSOLE_HOME_ROUTE; -const FULLSCREEN_DISPLAY_ROUTES = new Set(["/runtime/mission-wall"]); +const FULLSCREEN_DISPLAY_ROUTES = new Set(['/runtime/mission-wall']); +const WORKFLOW_ACTIVITY_VNEXT_ROUTE = + /^\/scopes\/[^/]+\/workflow-activity-vnext(?:\/|$)/; const STUDIO_HOST_ROUTES = new Set([ - "/studio", - "/scopes/:scopeId/teams/:teamId/members/new/workflow", - "/scopes/:scopeId/teams/:teamId/members/:memberId/workflow", + '/studio', + '/scopes/:scopeId/teams/:teamId/members/new/workflow', + '/scopes/:scopeId/teams/:teamId/members/:memberId/workflow', ]); function isFullscreenDisplayRoute(pathname: string): boolean { - return FULLSCREEN_DISPLAY_ROUTES.has(pathname); + return ( + FULLSCREEN_DISPLAY_ROUTES.has(pathname) || + WORKFLOW_ACTIVITY_VNEXT_ROUTE.test(pathname) + ); +} + +function isWorkflowActivityVNextRoute(pathname: string): boolean { + return WORKFLOW_ACTIVITY_VNEXT_ROUTE.test(pathname); } function isStudioHostRoute(pathname: string): boolean { @@ -63,19 +72,20 @@ function isStudioHostRoute(pathname: string): boolean { return true; } - return ( - /^\/scopes\/[^/]+\/teams\/[^/]+\/members\/(?:new|[^/]+)\/workflow$/.test( - pathname, - ) + return /^\/scopes\/[^/]+\/teams\/[^/]+\/members\/(?:new|[^/]+)\/workflow$/.test( + pathname, ); } -function shouldDefaultCollapseLayout(pathname: string, search: string): boolean { +function shouldDefaultCollapseLayout( + pathname: string, + search: string, +): boolean { if (!isStudioHostRoute(pathname)) { return false; } - return new URLSearchParams(search).get("intent") === "create-member"; + return new URLSearchParams(search).get('intent') === 'create-member'; } function shouldCollapseLayout(pathname: string, search: string): boolean { @@ -90,7 +100,7 @@ function buildLoginRoute(returnTo: string): string { } function getCurrentReturnTo(pathname: string): string { - return pathname === "/" + return pathname === '/' ? DEFAULT_PROTECTED_ROUTE : `${pathname}${window.location.search}${window.location.hash}`; } @@ -141,20 +151,21 @@ type ConsoleRuntimeProvidersProps = { search: string; }; -const LIVE_OPS_ATTENTION_BADGE_KEY = "live.attention"; +const LIVE_OPS_ATTENTION_BADGE_KEY = 'live.attention'; const LIVE_OPS_ATTENTION_MAX_CANDIDATES = 6; const LIVE_OPS_ATTENTION_MAX_AGE_MS = 12 * 60 * 60 * 1000; const LIVE_OPS_ATTENTION_REFRESH_MS = 30_000; -const NAVIGATION_GROUP_ORDER: readonly NavigationGroup[] = getNavigationGroupOrder(); +const NAVIGATION_GROUP_ORDER: readonly NavigationGroup[] = + getNavigationGroupOrder(); const NAVIGATION_MENU_MESSAGE_IDS: Readonly> = { - "/chat": "nav.items.chat", - "/scopes": "nav.items.myTeams", - "/runtime/runs": "nav.items.eventStream", - "/services": "nav.items.services", - "/governance": "nav.items.governance", - "/deployments": "nav.items.deployments", - "/runtime/explorer": "nav.items.topology", - "/settings": "nav.items.settings", + '/chat': 'nav.items.chat', + '/scopes': 'nav.items.myTeams', + '/runtime/runs': 'nav.items.eventStream', + '/services': 'nav.items.services', + '/governance': 'nav.items.governance', + '/deployments': 'nav.items.deployments', + '/runtime/explorer': 'nav.items.topology', + '/settings': 'nav.items.settings', }; const LIVE_OPS_DEFAULT_ATTENTION_SNAPSHOT: LiveOpsAttentionSnapshot = { hasPendingAttention: false, @@ -164,11 +175,11 @@ const liveOpsAttentionListeners = new Set<() => void>(); let liveOpsAttentionSnapshot = LIVE_OPS_DEFAULT_ATTENTION_SNAPSHOT; const navigationGroupLabelStyle: React.CSSProperties = { - color: "#667085", - display: "inline-flex", + color: '#667085', + display: 'inline-flex', fontSize: 14, fontWeight: 700, - lineHeight: "22px", + lineHeight: '22px', }; const LocalizedNavigationText: React.FC<{ @@ -177,7 +188,7 @@ const LocalizedNavigationText: React.FC<{ }> = ({ defaultLabel, messageId }) => { const intl = useIntl(); const defaultMessage = - typeof defaultLabel === "string" ? defaultLabel : undefined; + typeof defaultLabel === 'string' ? defaultLabel : undefined; return ( <> @@ -225,11 +236,13 @@ function setLiveOpsAttentionSnapshot(next: LiveOpsAttentionSnapshot): void { } liveOpsAttentionSnapshot = next; - liveOpsAttentionListeners.forEach((listener) => listener()); + liveOpsAttentionListeners.forEach((listener) => { + listener(); + }); } function buildLiveOpsAttentionCandidateKey( - candidate: LiveOpsAttentionCandidate + candidate: LiveOpsAttentionCandidate, ): string { const actorId = trimOptional(candidate.actorId); if (actorId) { @@ -237,16 +250,16 @@ function buildLiveOpsAttentionCandidateKey( } return [ - "run", - trimOptional(candidate.scopeId) || "", - trimOptional(candidate.serviceId) || "", - trimOptional(candidate.runId) || "", - ].join(":"); + 'run', + trimOptional(candidate.scopeId) || '', + trimOptional(candidate.serviceId) || '', + trimOptional(candidate.runId) || '', + ].join(':'); } function collectLiveOpsAttentionCandidates( pathname: string, - search: string + search: string, ): LiveOpsAttentionCandidate[] { const nowMs = Date.now(); const deduped = new Map(); @@ -260,7 +273,7 @@ function collectLiveOpsAttentionCandidates( continue; } - if (entry.status === "finished" || entry.status === "error") { + if (entry.status === 'finished' || entry.status === 'error') { continue; } @@ -280,7 +293,7 @@ function collectLiveOpsAttentionCandidates( } } - if (pathname === "/runtime/mission-control") { + if (pathname === '/runtime/mission-control') { const context = readMissionControlRouteContext(search); const candidate: LiveOpsAttentionCandidate = { actorId: trimOptional(context.actorId), @@ -297,11 +310,14 @@ function collectLiveOpsAttentionCandidates( } } - return Array.from(deduped.values()).slice(0, LIVE_OPS_ATTENTION_MAX_CANDIDATES); + return Array.from(deduped.values()).slice( + 0, + LIVE_OPS_ATTENTION_MAX_CANDIDATES, + ); } async function resolveLiveOpsAttentionActorId( - candidate: LiveOpsAttentionCandidate + candidate: LiveOpsAttentionCandidate, ): Promise { const actorId = trimOptional(candidate.actorId); if (actorId) { @@ -325,7 +341,7 @@ async function resolveLiveOpsAttentionActorId( } async function runNeedsLiveOpsAttention( - candidate: LiveOpsAttentionCandidate + candidate: LiveOpsAttentionCandidate, ): Promise { const actorId = await resolveLiveOpsAttentionActorId(candidate); if (!actorId) { @@ -337,7 +353,7 @@ async function runNeedsLiveOpsAttention( const [graph, timeline] = await Promise.all([ runtimeActorsApi.getActorGraphEnriched(actorId, { depth: 4, - direction: "Both", + direction: 'Both', take: 120, }), runtimeActorsApi.getActorTimeline(actorId, { @@ -346,7 +362,7 @@ async function runNeedsLiveOpsAttention( ]); const snapshot = buildMissionSnapshotFromRuntime({ - connectionStatus: "degraded", + connectionStatus: 'degraded', nowMs: fetchedAtMs, recentEvents: [], routeContext: { @@ -363,15 +379,15 @@ async function runNeedsLiveOpsAttention( }, session: { runId: trimOptional(candidate.runId), - status: "running", + status: 'running', }, }, }); return ( snapshot.intervention?.required === true && - (snapshot.intervention.kind === "human_approval" || - snapshot.intervention.kind === "human_input") + (snapshot.intervention.kind === 'human_approval' || + snapshot.intervention.kind === 'human_input') ); } catch { return false; @@ -380,7 +396,7 @@ async function runNeedsLiveOpsAttention( async function loadLiveOpsAttentionSnapshot( pathname: string, - search: string + search: string, ): Promise { const candidates = collectLiveOpsAttentionCandidates(pathname, search); if (candidates.length === 0) { @@ -388,10 +404,10 @@ async function loadLiveOpsAttentionSnapshot( } const results = await Promise.allSettled( - candidates.map((candidate) => runNeedsLiveOpsAttention(candidate)) + candidates.map((candidate) => runNeedsLiveOpsAttention(candidate)), ); const pendingCount = results.reduce((count, result) => { - if (result.status === "fulfilled" && result.value) { + if (result.status === 'fulfilled' && result.value) { return count + 1; } @@ -412,7 +428,7 @@ const NavigationMenuLabel: React.FC<{ const snapshot = React.useSyncExternalStore( subscribeLiveOpsAttention, getLiveOpsAttentionSnapshot, - getLiveOpsAttentionSnapshot + getLiveOpsAttentionSnapshot, ); const showCountBadge = badgeKey === LIVE_OPS_ATTENTION_BADGE_KEY && snapshot.pendingCount > 0; @@ -420,18 +436,18 @@ const NavigationMenuLabel: React.FC<{ return ( {label} @@ -450,10 +466,10 @@ const NavigationMenuLabel: React.FC<{