From 66fd37ee6f801388a5606e374be3d17266c087cd Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 24 Aug 2026 12:48:31 +0100 Subject: [PATCH] Simplify object drawer headers --- .../subsystems/frontend-primitives.md | 5 + .../subsystems/performance-and-scalability.md | 4 + .../internal/subsystems/unified-resources.md | 4 + frontend-modern/browser-verification.json | 77 +++-------- .../scripts/shared-template-registry.json | 20 +++ .../Infrastructure/ResourceDetailDrawer.tsx | 27 +--- ...urceDetailDrawer.identity-runtime.test.tsx | 104 +-------------- .../useResourceDetailDrawerState.ts | 42 ------ .../components/Workloads/GuestDrawer.test.tsx | 123 ++---------------- .../src/components/Workloads/GuestDrawer.tsx | 32 +---- ...loadsSurface.performance.contract.test.tsx | 2 +- .../Workloads/useGuestDrawerState.ts | 42 ------ .../SharedPrimitives.guardrails.test.ts | 21 ++- 13 files changed, 86 insertions(+), 417 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index e3c0536e7..15e39ad0b 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -3607,6 +3607,11 @@ automatic-action policy, saved access configuration, and action audit belong to a dedicated `Manage` tab and must not render inside Overview. Guest, node, Docker-host, and unified-resource drawers must compose these shared primitives instead of defining provider-local attention or technical disclosure shells. +Drawer headers reserve their limited space for the subject and a close control, +plus a direct object-specific lifecycle action only where the object contract +requires one. Generic `Ask Assistant` and `Copy context` actions do not belong +in object drawer headers: Assistant remains available through the global shell, +and raw context export must not compete with the operational reading path. The shared interactive sparkline now follows that same split. `frontend-modern/src/components/shared/InteractiveSparkline.tsx` stays the render shell, `frontend-modern/src/components/shared/useInteractiveSparklineState.ts` diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 63f07c0da..bd282f58f 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -1366,6 +1366,10 @@ selected. Alert lookup may consume the already-loaded active-alert map through the resource's bounded canonical/provider identity candidate list; it must not add a per-drawer fetch, rescan the workload inventory, or eagerly mount hidden technical sections. +Guest and resource drawer headers must not initialize clipboard-context or +Assistant-handoff state for generic header buttons. Removing those controls +also removes their drawer-local signal, API, and formatting wiring; the global +Assistant shell remains the single generic entry point. The investigation enrichment path in `MaybeInvestigateFinding` adds at most one operator-state projection lookup per investigation diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 30c86e363..aa453cb2a 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -4659,3 +4659,7 @@ details. Operator overrides, lifecycle policy, update actions, audit controls, and saved web access belong to Manage. Provider pages may supply typed detail models, but they must not create a provider-local drawer hierarchy or promote metadata into Overview solely because it exists. +The drawer header contains no generic Assistant handoff or agent-context copy +action. Those shell-level capabilities must not consume object identity space; +only the close control and a capability-backed object lifecycle action may sit +beside the heading. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 098a49c73..19ce88107 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,65 +1,21 @@ { "version": 1, - "base_sha": "a73abe909caceee084e647b959f9adae9a355bb3", - "verified_at": "2026-08-24T11:27:51Z", + "base_sha": "9e622c291d499cd88b8269ef98c878c702de801b", + "verified_at": "2026-08-24T11:47:38Z", "result": "passed", "changed_paths": [ "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx", - "frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx", - "frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx", - "frontend-modern/src/components/Infrastructure/ResourceDetailSummary.tsx", - "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts", "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts", "frontend-modern/src/components/Workloads/GuestDrawer.tsx", - "frontend-modern/src/components/Workloads/GuestDrawerManage.tsx", - "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx", - "frontend-modern/src/components/Workloads/NodeDrawer.tsx", - "frontend-modern/src/components/Workloads/NodeDrawerOverview.tsx", - "frontend-modern/src/components/Workloads/WorkloadPanel.tsx", - "frontend-modern/src/components/Workloads/guestDrawerModel.ts", - "frontend-modern/src/components/shared/DetailSectionTable.tsx", - "frontend-modern/src/components/shared/DrawerAttentionSection.tsx", - "frontend-modern/src/components/shared/TechnicalDetailsDisclosure.tsx", - "frontend-modern/src/components/shared/detailSectionModel.ts", - "frontend-modern/src/features/alerts/alertOverridesModel.ts", - "frontend-modern/src/features/docker/DockerHostDrawer.tsx", - "frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx", - "frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx", - "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx", - "frontend-modern/src/hooks/useWorkloads.ts", - "frontend-modern/src/types/workloads.ts", - "frontend-modern/src/utils/alerts.ts", - "frontend-modern/src/utils/workloadGuestPresentation.ts" + "frontend-modern/src/components/Workloads/useGuestDrawerState.ts" ], "content_sha256": { - "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx": "8d759c4043eca6ffc4353e478e8ecc9117eb5cac0ee0799afb186f669802bf09", - "frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx": "7a69a40a65452affb25c5a136dcfb5a84bb07d3ee948c08eb77e0fe6707bee8f", - "frontend-modern/src/components/Infrastructure/ResourceDetailDrawerSupportDisclosure.tsx": "040da0942faabe89455b2d478b2fac03697e50ee9c12426a399b2cb0004327dd", - "frontend-modern/src/components/Infrastructure/ResourceDetailSummary.tsx": "0d40dbff2b59f63813f98419004fedf1269c99efa29308210e11c3d18ee8d904", - "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts": "10dc7c47e116f50f197908b52b88c9acca42ffd35e89106390a5d69f73a00837", - "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts": "850cd9dba3b91530068c8542121e4929bebc547b3abda1a10744fa886c0f9361", - "frontend-modern/src/components/Workloads/GuestDrawer.tsx": "543a17e2bcb03cc7e411aebe63330e575c069e03f78ceb31baa6ca727cb01f57", - "frontend-modern/src/components/Workloads/GuestDrawerManage.tsx": "8def7d128956c2e24e7f8c0913b7f79f7c9b3f21c54130ed51f4149915f417b5", - "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx": "b9bddadb3c2386329a2caae6c8ae6d785e935af35c41c79be9f47f2163463252", - "frontend-modern/src/components/Workloads/NodeDrawer.tsx": "f6803503882e380e2d0d3df56d6676607717579da81ed7e24c06894961fb1258", - "frontend-modern/src/components/Workloads/NodeDrawerOverview.tsx": "5d9f130c58cffe8f653730b961b297fb789df3a5bc79b83c3da874c2067bc31e", - "frontend-modern/src/components/Workloads/WorkloadPanel.tsx": "e5ccf2149958fc5cfe3b0711b9d56557e0c86d525efdef69af229f325a0fe4df", - "frontend-modern/src/components/Workloads/guestDrawerModel.ts": "faf23c024fd0c0395ef1fbcebb10f13063ed9a4b7dcb5800c2ab836c9d5c5e8b", - "frontend-modern/src/components/shared/DetailSectionTable.tsx": "871c66c39f727ae46fbfb6e3246c6ea0acd37d02e9abe6ba7143013913f19e22", - "frontend-modern/src/components/shared/DrawerAttentionSection.tsx": "b9035090571c09e791021f789e8e4a4bcaffbff33dd37458ffbebf9a223d319c", - "frontend-modern/src/components/shared/TechnicalDetailsDisclosure.tsx": "d6a8ed7f006cecbe1b2e7055d041e767ff8d200ecb2c8454827bfc397e0dbdeb", - "frontend-modern/src/components/shared/detailSectionModel.ts": "b2effcc4ff9ce98e9fdd73d4853cb38166b92487c553e8cd981bb41ff7c11d86", - "frontend-modern/src/features/alerts/alertOverridesModel.ts": "8e344f8d76f4038efad5fe2765ba92ef5ca8edc0afbcdbfbc3d4cda9696a1d0a", - "frontend-modern/src/features/docker/DockerHostDrawer.tsx": "8bfd9ea8ec7cd8be5a77568aa33774a2c30f6bcc33113044ef4bf1e34422bcc4", - "frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx": "4adb8252dcac84ed8523f798338091852869f891f1583e130053649affc987a7", - "frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx": "05bc58b83bb5ecce65243a6849d6ded08bcbb656fcb3eda23d13129463e94ccd", - "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "65c945d3ae01adeb6f41b2ba144b5b7b523c3e7c118721890c78bd6d19b42efb", - "frontend-modern/src/hooks/useWorkloads.ts": "f56021125846e931dd175ed8c4a0c46bd884edce434c61bf5a332a8c0a444f52", - "frontend-modern/src/types/workloads.ts": "e878e4d96359b4add4ea650ce5e3af361e6cb15e3fede51a8f7bc9d2c8ca8aa9", - "frontend-modern/src/utils/alerts.ts": "6b9556d0415294a8151d786489a4ecb9667b26ddc272c8fe33cf955a9ebcbbeb", - "frontend-modern/src/utils/workloadGuestPresentation.ts": "ef23c08bed5f507a163aca438ec8edd2e95e47e20d164a68e0e15b7697941886" + "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx": "4bc28b3a2f26f1e011502a55deb6befe62e001db2d4020c14b016a7d2f1458e5", + "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts": "8e1f892548ec92d5e895013c8247b73e6119e909cb9d635dcab354c25d14fd04", + "frontend-modern/src/components/Workloads/GuestDrawer.tsx": "17291d2c1f574e8bb1a60ca3d933dc6fab6badbd80cd4097542d74db13050429", + "frontend-modern/src/components/Workloads/useGuestDrawerState.ts": "ec24e7f9c32f9ffdf36eebc0391fbd7019af8ed096a01513e81ac54806e315f2" }, - "routes": ["/proxmox/overview", "/docker/overview", "/truenas/overview"], + "routes": ["/truenas/overview", "/proxmox/overview"], "viewports": [ { "width": 1440, @@ -71,17 +27,14 @@ } ], "states": [ - "Proxmox guest with an active powered-off alert and condensed additive operator context", - "Proxmox node with an active offline alert and condensed node context", - "Docker host overview and Manage tab with update actions separated from context", - "TrueNAS system with multiple active health issues and both technical disclosure groups collapsed", - "generic resource Manage tab with operator overrides and saved web access" + "TrueNAS system drawer with active health issues and only the close control in its header", + "Proxmox guest drawer with only the close control in its header", + "global platform Assistant entry remaining available outside the object drawer" ], "interactions": [ - "expanded guest, node, Docker host, and TrueNAS resource drawers", - "confirmed exact active alert messages render immediately and wrap at phone width", - "expanded Technical details and Platform details independently", - "switched to Manage and confirmed overrides, actions, and access are absent from Overview", - "confirmed the hierarchy at desktop width and restored the temporary browser viewport" + "expanded a TrueNAS resource drawer at phone width and confirmed no object Assistant or context-copy controls", + "expanded a Proxmox guest drawer at phone width and confirmed no object Assistant or context-copy controls", + "confirmed the guest header rule at desktop width", + "restored the temporary browser viewport" ] } diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index 9f94d4edf..aad020490 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -23,6 +23,26 @@ "path": "src/features/docker/DockerHostDrawerOverview.tsx" } ], + "forbiddenPatterns": [ + { + "path": "src/components/Infrastructure/ResourceDetailDrawer.tsx", + "patterns": [ + "Ask Pulse Assistant about", + "Copy Pulse context for", + "openAssistantForResource", + "copyAgentContext" + ] + }, + { + "path": "src/components/Workloads/GuestDrawer.tsx", + "patterns": [ + "Ask Pulse Assistant about", + "Copy Pulse context for", + "openAssistantForGuest", + "copyAgentContext" + ] + } + ], "proof": [ "src/components/shared/SharedPrimitives.guardrails.test.ts", "src/components/Workloads/GuestDrawer.test.tsx", diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx index b30759552..0d44b9b0a 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx @@ -1,14 +1,8 @@ import { Show, For, Suspense } from 'solid-js'; import type { Component } from 'solid-js'; -import CopyIcon from 'lucide-solid/icons/copy'; -import MessageSquareIcon from 'lucide-solid/icons/message-square'; import XIcon from 'lucide-solid/icons/x'; import type { Resource } from '@/types/resource'; -import { - DrawerHeaderActionButton, - DrawerHeaderActionGroup, - DrawerHeaderIconButton, -} from '@/components/shared/Button'; +import { DrawerHeaderActionGroup, DrawerHeaderIconButton } from '@/components/shared/Button'; import { DiscoveryLoadingFallback } from '@/components/shared/DiscoveryLoadingFallback'; import { StatusDot } from '@/components/shared/StatusDot'; import { Subtabs } from '@/components/shared/Subtabs'; @@ -129,25 +123,6 @@ const DrawerContent: Component = (props) => { onActionSettled={props.onResourceActionSettled} /> - - drawer.openAssistantForResource()} - title={`Ask Pulse Assistant about ${drawer.displayName()}`} - aria-label={`Ask Pulse Assistant about ${drawer.displayName()}`} - > - - - - - void drawer.copyAgentContext()} - disabled={drawer.copyingAgentContext()} - title={`Copy Pulse context for ${drawer.displayName()}`} - aria-label={`Copy Pulse context for ${drawer.displayName()}`} - > - - - props.onClose?.()} diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx index e4054b6d1..8e7a6d8be 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx @@ -2,7 +2,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { fireEvent, render as solidRender, waitFor, within } from '@solidjs/testing-library'; import type { Resource } from '@/types/resource'; -import { aiChatStore } from '@/stores/aiChat'; import { ResourceDetailDrawer, getSpecializedTabAvailabilityMessage, @@ -29,10 +28,6 @@ const render = (...args: Parameters): ReturnType ({ pmg: [] as any[] })); const reconnectSpy = vi.hoisted(() => vi.fn()); -const getResourceContextMock = vi.hoisted(() => vi.fn()); -const copyToClipboardMock = vi.hoisted(() => vi.fn()); -const notificationSuccessMock = vi.hoisted(() => vi.fn()); -const notificationErrorMock = vi.hoisted(() => vi.fn()); vi.mock('@/contexts/appRuntime', () => ({ useWebSocket: () => ({ @@ -104,23 +99,6 @@ vi.mock('@/api/ai', () => ({ }, })); -vi.mock('@/api/agentContext', () => ({ - AgentContextAPI: { - getResourceContext: getResourceContextMock, - }, -})); - -vi.mock('@/utils/clipboard', () => ({ - copyToClipboard: copyToClipboardMock, -})); - -vi.mock('@/stores/notifications', () => ({ - notificationStore: { - success: notificationSuccessMock, - error: notificationErrorMock, - }, -})); - class ResizeObserverMock { constructor(_callback: ResizeObserverCallback) {} observe() {} @@ -146,84 +124,16 @@ const baseResource = (overrides: Partial): Resource => ({ ...overrides, }); -afterEach(() => { - aiChatStore.setEnabled(false); - getResourceContextMock.mockReset(); - copyToClipboardMock.mockReset(); - notificationSuccessMock.mockReset(); - notificationErrorMock.mockReset(); -}); +afterEach(() => vi.clearAllMocks()); describe('ResourceDetailDrawer runtime and identity cards', () => { - it('opens Pulse Assistant with a resource-scoped handoff from the drawer header', () => { - aiChatStore.setEnabled(true); - const openSpy = vi.spyOn(aiChatStore, 'open'); - const resource = baseResource({ - type: 'app-container', - displayName: 'Home Assistant', - technology: 'docker', - parentName: 'ha-lxc', - discoveryTarget: { - resourceType: 'app-container', - agentId: 'agent:pve-1', - resourceId: 'homeassistant', - }, - }); + it('keeps generic Assistant and context-copy actions out of the drawer header', () => { + const { queryByRole } = render(() => ( + + )); - const { getByRole } = render(() => ); - - fireEvent.click(getByRole('button', { name: 'Ask Pulse Assistant about Home Assistant' })); - - expect(openSpy).toHaveBeenCalledWith( - expect.objectContaining({ - targetType: 'resource', - targetId: 'resource-1', - autonomousMode: false, - handoffResources: [ - { - id: 'resource-1', - name: 'Home Assistant', - type: 'app-container', - node: 'ha-lxc', - }, - ], - handoffMetadata: { kind: 'resource_context' }, - }), - ); - openSpy.mockRestore(); - }); - - it('copies the canonical agent resource context from the drawer header', async () => { - getResourceContextMock.mockResolvedValue({ - canonicalId: 'resource-1', - resourceType: 'agent', - resourceName: 'host-1', - activeFindings: [], - pendingApprovals: [], - recentActions: [], - generatedAt: '2026-05-06T14:00:00Z', - contextSections: [ - { - id: 'identity', - title: 'Identity', - source: 'unified-resource', - trustTier: 'pulse-authored', - generatedAt: '2026-05-06T14:00:00Z', - facts: [{ label: 'Display name', value: 'host-1' }], - }, - ], - }); - copyToClipboardMock.mockResolvedValue(true); - - const { getByRole } = render(() => ); - - fireEvent.click(getByRole('button', { name: 'Copy Pulse context for host-1' })); - - await waitFor(() => { - expect(getResourceContextMock).toHaveBeenCalledWith('resource-1'); - expect(copyToClipboardMock).toHaveBeenCalledWith(expect.stringContaining('host-1')); - expect(notificationSuccessMock).toHaveBeenCalledWith('Resource context copied.'); - }); + expect(queryByRole('button', { name: 'Ask Pulse Assistant about Home Assistant' })).toBeNull(); + expect(queryByRole('button', { name: 'Copy Pulse context for Home Assistant' })).toBeNull(); }); it('omits empty operator context without repeating healthy source status', () => { diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts index 3af13ae8e..cb093ac8d 100644 --- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts +++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts @@ -1,17 +1,11 @@ import { createEffect, createSignal } from 'solid-js'; -import { AgentContextAPI } from '@/api/agentContext'; import type { Resource } from '@/types/resource'; import type { HistoryTimeRange } from '@/api/charts'; import { GUEST_DRAWER_HISTORY_DEFAULT_RANGE } from '@/components/Workloads/guestDrawerModel'; -import { aiChatStore } from '@/stores/aiChat'; -import { notificationStore } from '@/stores/notifications'; import { createLocalStorageBooleanSignal, STORAGE_KEYS } from '@/utils/localStorage'; -import { copyToClipboard } from '@/utils/clipboard'; -import { formatAgentResourceContextForClipboard } from '@/utils/agentContextPresentation'; import { useResourceDetailDrawerDockerActionsState } from './useResourceDetailDrawerDockerActionsState'; import { useResourceDetailDrawerHistoryState } from './useResourceDetailDrawerHistoryState'; import { useResourceDetailDrawerDerivedState } from './useResourceDetailDrawerDerivedState'; -import { buildResourceAssistantContext } from '@/utils/resourceAssistantContextModel'; import type { ResourceDetailDrawerPresentation } from './resourceDetailDrawerPresentation'; type DrawerTab = @@ -42,8 +36,6 @@ export const useResourceDetailDrawerState = (options: UseResourceDetailDrawerSta ); const [debugEnabled] = createLocalStorageBooleanSignal(STORAGE_KEYS.DEBUG_MODE, false); const [copied, setCopied] = createSignal(false); - const [copyingAgentContext, setCopyingAgentContext] = createSignal(false); - const [agentContextCopied, setAgentContextCopied] = createSignal(false); const [showReportModal, setShowReportModal] = createSignal(false); const [showHistoryFilters, setShowHistoryFilters] = createSignal(false); const [showAccessContext, setShowAccessContext] = createSignal( @@ -123,35 +115,6 @@ export const useResourceDetailDrawerState = (options: UseResourceDetailDrawerSta } }; - const assistantAvailable = () => aiChatStore.enabled === true; - - const openAssistantForResource = () => { - if (!assistantAvailable()) return; - aiChatStore.open(buildResourceAssistantContext(resource)); - }; - - const copyAgentContext = async () => { - if (copyingAgentContext()) return; - setCopyingAgentContext(true); - setAgentContextCopied(false); - - try { - const context = await AgentContextAPI.getResourceContext(resource.id); - const copiedContext = await copyToClipboard(formatAgentResourceContextForClipboard(context)); - if (!copiedContext) { - throw new Error('Clipboard unavailable'); - } - setAgentContextCopied(true); - notificationStore.success('Resource context copied.'); - setTimeout(() => setAgentContextCopied(false), 2000); - } catch { - notificationStore.error('Unable to copy resource context.'); - setAgentContextCopied(false); - } finally { - setCopyingAgentContext(false); - } - }; - return { activeTab, setActiveTab, @@ -159,8 +122,6 @@ export const useResourceDetailDrawerState = (options: UseResourceDetailDrawerSta setMetricsHistoryRange, debugEnabled, copied, - copyingAgentContext, - agentContextCopied, showReportModal, setShowReportModal, showHistoryFilters, @@ -190,9 +151,6 @@ export const useResourceDetailDrawerState = (options: UseResourceDetailDrawerSta ...history, ...derived, ...dockerActions, - assistantAvailable, - openAssistantForResource, - copyAgentContext, handleCopyJson, }; }; diff --git a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx index 0e2d4b65e..64abee42a 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx @@ -11,7 +11,6 @@ import { Suspense } from 'solid-js'; import type { WorkloadGuest } from '@/types/workloads'; import type { Memory, Disk, GuestNetworkInterface } from '@/types/api'; import { resetCreateNonSuspendingQueryCacheForTest } from '@/hooks/createNonSuspendingQuery'; -import { aiChatStore } from '@/stores/aiChat'; import { getCanonicalWorkloadId, getWorkloadMetadataId } from '@/utils/workloads'; import { getDiscoveryProvenanceTitle } from '@/utils/discoveryPresentation'; import guestDrawerSource from './GuestDrawer.tsx?raw'; @@ -69,10 +68,6 @@ const discoveryApiMocks = vi.hoisted(() => ({ null, ), })); -const getResourceContextMock = vi.hoisted(() => vi.fn()); -const copyToClipboardMock = vi.hoisted(() => vi.fn()); -const notificationSuccessMock = vi.hoisted(() => vi.fn()); -const notificationErrorMock = vi.hoisted(() => vi.fn()); vi.mock('@/api/discovery', async () => { const actual = await vi.importActual('@/api/discovery'); @@ -82,23 +77,6 @@ vi.mock('@/api/discovery', async () => { }; }); -vi.mock('@/api/agentContext', () => ({ - AgentContextAPI: { - getResourceContext: getResourceContextMock, - }, -})); - -vi.mock('@/utils/clipboard', () => ({ - copyToClipboard: copyToClipboardMock, -})); - -vi.mock('@/stores/notifications', () => ({ - notificationStore: { - success: notificationSuccessMock, - error: notificationErrorMock, - }, -})); - vi.mock('@/components/shared/WebInterfaceUrlField', () => ({ WebInterfaceUrlField: (props: { metadataKind: string; @@ -217,7 +195,6 @@ beforeEach(() => { afterEach(() => { cleanup(); - aiChatStore.setEnabled(false); vi.clearAllMocks(); vi.useRealTimers(); }); @@ -277,99 +254,15 @@ describe('GuestDrawer', () => { expect(screen.queryByTestId('guest-manage-tab')).not.toBeInTheDocument(); }); - describe('Assistant context actions', () => { - it('opens Pulse Assistant with a canonical Proxmox LXC resource handoff', () => { - aiChatStore.setEnabled(true); - const openSpy = vi.spyOn(aiChatStore, 'open'); - const guest = makeGuest({ - id: 'legacy-row-id', - instance: 'pve-main', - node: 'pve-a', - vmid: 101, - name: 'homeassistant', - type: 'lxc', - workloadType: 'system-container', - platformType: 'proxmox', - status: 'running', - discoveryTarget: { - resourceType: 'system-container', - agentId: 'agent-pve-a', - resourceId: '101', - }, - }); + it('keeps generic Assistant and context-copy actions out of the drawer header', () => { + render(() => ); - render(() => ); - - fireEvent.click( - screen.getByRole('button', { name: 'Ask Pulse Assistant about homeassistant' }), - ); - - expect(openSpy).toHaveBeenCalledWith( - expect.objectContaining({ - targetType: 'resource', - targetId: 'pve-main:pve-a:101', - autonomousMode: false, - handoffResources: [ - { - id: 'pve-main:pve-a:101', - name: 'homeassistant', - type: 'system-container', - node: 'pve-a', - }, - ], - handoffMetadata: { kind: 'resource_context' }, - }), - ); - expect(openSpy.mock.calls[0]?.[0]?.handoffContext).toBeUndefined(); - expect(openSpy.mock.calls[0]?.[0]?.briefing?.detailLines).toContain( - 'Discovery: system-container:101', - ); - openSpy.mockRestore(); - }); - - it('copies the safe agent context bundle using canonical guest identity', async () => { - getResourceContextMock.mockResolvedValue({ - canonicalId: 'pve-main:pve-a:101', - resourceType: 'system-container', - resourceName: 'homeassistant', - activeFindings: [], - pendingApprovals: [], - recentActions: [], - generatedAt: '2026-05-06T14:00:00Z', - contextSections: [ - { - id: 'runtime', - title: 'Runtime', - source: 'discovery', - trustTier: 'pulse-observed', - generatedAt: '2026-05-06T14:00:00Z', - facts: [{ label: 'Service', value: 'Home Assistant' }], - }, - ], - }); - copyToClipboardMock.mockResolvedValue(true); - const guest = makeGuest({ - id: 'legacy-row-id', - instance: 'pve-main', - node: 'pve-a', - vmid: 101, - name: 'homeassistant', - type: 'lxc', - workloadType: 'system-container', - }); - - render(() => ); - - fireEvent.click(screen.getByRole('button', { name: 'Copy Pulse context for homeassistant' })); - - await waitFor(() => { - expect(getResourceContextMock).toHaveBeenCalledWith('pve-main:pve-a:101'); - expect(copyToClipboardMock).toHaveBeenCalledWith( - expect.stringContaining('Pulse resource context: homeassistant'), - ); - expect(notificationSuccessMock).toHaveBeenCalledWith('Resource context copied.'); - }); - }); + expect( + screen.queryByRole('button', { name: 'Ask Pulse Assistant about homeassistant' }), + ).toBeNull(); + expect( + screen.queryByRole('button', { name: 'Copy Pulse context for homeassistant' }), + ).toBeNull(); }); // ── Tabs ── diff --git a/frontend-modern/src/components/Workloads/GuestDrawer.tsx b/frontend-modern/src/components/Workloads/GuestDrawer.tsx index db9c5d21d..e22f434f0 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawer.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawer.tsx @@ -1,13 +1,7 @@ import { Component, Show, Suspense, createMemo } from 'solid-js'; -import CopyIcon from 'lucide-solid/icons/copy'; -import MessageSquareIcon from 'lucide-solid/icons/message-square'; import XIcon from 'lucide-solid/icons/x'; import { DiscoveryTab } from '../Discovery/DiscoveryTab'; -import { - DrawerHeaderActionButton, - DrawerHeaderActionGroup, - DrawerHeaderIconButton, -} from '@/components/shared/Button'; +import { DrawerHeaderActionGroup, DrawerHeaderIconButton } from '@/components/shared/Button'; import { DiscoveryLoadingFallback } from '@/components/shared/DiscoveryLoadingFallback'; import { DrawerSubjectHeading } from '@/components/shared/DrawerSubjectHeading'; import { DiscoveryReadinessBadge } from '@/components/shared/DiscoveryReadinessBadge'; @@ -25,9 +19,7 @@ export const GuestDrawer: Component = (props) => { agentHeading, agentLabel, agentTitle, - agentContextCopied, backupPresentation, - copyingAgentContext, discoveryAgentId, discoveryIdentifiedSummary, discoveryLoadingState, @@ -49,9 +41,6 @@ export const GuestDrawer: Component = (props) => { guestOsSummary, networkInterfaces, normalizedTags, - assistantAvailable, - copyAgentContext, - openAssistantForGuest, setHistoryRange, showInGuestAgentInstallCue, switchTab, @@ -74,25 +63,6 @@ export const GuestDrawer: Component = (props) => { statusLabel={headerIndicator().label} /> - - openAssistantForGuest()} - title={`Ask Pulse Assistant about ${props.guest.name}`} - aria-label={`Ask Pulse Assistant about ${props.guest.name}`} - > - - - void copyAgentContext()} - disabled={copyingAgentContext()} - title={`Copy Pulse context for ${props.guest.name}`} - aria-label={`Copy Pulse context for ${props.guest.name}`} - > - props.onClose()} aria-label="Close guest drawer"> diff --git a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx index a0153bb4c..092eababb 100644 --- a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx @@ -1570,7 +1570,7 @@ describe('Workloads performance contract', () => { expect(guestDrawerSource).toContain('GuestDrawerOverview'); expect(guestDrawerSource).toContain('GuestDrawerHistoryRangeSelect'); expect(guestDrawerSource).toContain('DrawerHeaderActionGroup'); - expect(guestDrawerSource).toContain('DrawerHeaderActionButton'); + expect(guestDrawerSource).not.toContain('DrawerHeaderActionButton'); expect(guestDrawerSource).toContain('DrawerHeaderIconButton'); expect(guestDrawerSource).not.toContain('Open related infrastructure'); expect(guestDrawerHistorySource).toContain('data-testid="guest-history-hover-time"'); diff --git a/frontend-modern/src/components/Workloads/useGuestDrawerState.ts b/frontend-modern/src/components/Workloads/useGuestDrawerState.ts index c0d5c38f0..636931bbd 100644 --- a/frontend-modern/src/components/Workloads/useGuestDrawerState.ts +++ b/frontend-modern/src/components/Workloads/useGuestDrawerState.ts @@ -1,15 +1,10 @@ import { createEffect, createMemo, createSignal } from 'solid-js'; -import { AgentContextAPI } from '@/api/agentContext'; import { getDiscovery } from '@/api/discovery'; import type { HistoryTimeRange } from '@/api/charts'; import { createNonSuspendingQuery } from '@/hooks/createNonSuspendingQuery'; -import { aiChatStore } from '@/stores/aiChat'; import { useAlertsActivation } from '@/stores/alertsActivation'; -import { notificationStore } from '@/stores/notifications'; import type { ResourceDiscovery, ResourceType as DiscoveryResourceType } from '@/types/discovery'; -import { formatAgentResourceContextForClipboard } from '@/utils/agentContextPresentation'; -import { copyToClipboard } from '@/utils/clipboard'; import { getDiscoveryIdentifiedSummary, getDiscoveryLoadingState, @@ -44,7 +39,6 @@ import { type GuestDrawerProps, type GuestDrawerTab, } from './guestDrawerModel'; -import { buildGuestAssistantContext } from './guestAssistantContextModel'; import { getWorkloadActionAgentTitle, hasExplicitWorkloadActionAgent, @@ -63,8 +57,6 @@ export function useGuestDrawerState(props: GuestDrawerProps) { const [historyRange, setHistoryRange] = createSignal( GUEST_DRAWER_HISTORY_DEFAULT_RANGE, ); - const [copyingAgentContext, setCopyingAgentContext] = createSignal(false); - const [agentContextCopied, setAgentContextCopied] = createSignal(false); const guestId = createMemo(() => getCanonicalWorkloadId(props.guest)); const alertThresholdScope = createMemo(() => getWorkloadAlertThresholdScope(props.guest)); @@ -161,35 +153,6 @@ export function useGuestDrawerState(props: GuestDrawerProps) { setActiveTab(tab); }; - const assistantAvailable = () => aiChatStore.enabled === true; - - const openAssistantForGuest = () => { - if (!assistantAvailable()) return; - aiChatStore.open(buildGuestAssistantContext(props.guest)); - }; - - const copyAgentContext = async () => { - if (copyingAgentContext()) return; - setCopyingAgentContext(true); - setAgentContextCopied(false); - - try { - const context = await AgentContextAPI.getResourceContext(guestId()); - const copiedContext = await copyToClipboard(formatAgentResourceContextForClipboard(context)); - if (!copiedContext) { - throw new Error('Clipboard unavailable'); - } - setAgentContextCopied(true); - notificationStore.success('Resource context copied.'); - setTimeout(() => setAgentContextCopied(false), 2000); - } catch { - notificationStore.error('Unable to copy resource context.'); - setAgentContextCopied(false); - } finally { - setCopyingAgentContext(false); - } - }; - createEffect(() => { if (activeTab() === 'discovery' && !hasDiscoverySupport()) { setActiveTab('overview'); @@ -205,8 +168,6 @@ export function useGuestDrawerState(props: GuestDrawerProps) { agentLabel, agentTitle, backupPresentation, - copyingAgentContext, - agentContextCopied, discoveryAgentId, discoveryIdentifiedSummary, discoveryReadinessPresentation, @@ -232,9 +193,6 @@ export function useGuestDrawerState(props: GuestDrawerProps) { osVersion, showInGuestAgentInstallCue, workloadActionAgentTitle, - assistantAvailable, - openAssistantForGuest, - copyAgentContext, switchTab, setHistoryRange, webInterfaceMetadataId, diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 5de184c66..0d09effcc 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -4184,7 +4184,7 @@ describe('shared primitive guardrails', () => { for (const drawerSource of [guestDrawerSource, resourceDetailDrawerSource]) { expect(drawerSource).toContain('@/components/shared/Button'); expect(drawerSource).toContain('DrawerHeaderActionGroup'); - expect(drawerSource).toContain('DrawerHeaderActionButton'); + expect(drawerSource).not.toContain('DrawerHeaderActionButton'); expect(drawerSource).toContain('DrawerHeaderIconButton'); expect(drawerSource).not.toContain( 'inline-flex h-8 min-h-11 min-w-11 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500', @@ -8509,6 +8509,25 @@ describe('shared primitive guardrails', () => { expect(source).toContain('ResourceOperatorStateSection'); } + for (const headerPath of [ + 'src/components/Infrastructure/ResourceDetailDrawer.tsx', + 'src/components/Workloads/GuestDrawer.tsx', + ]) { + const source = readFrontendSource(headerPath); + expect(source).not.toContain('Ask Pulse Assistant about'); + expect(source).not.toContain('Copy Pulse context for'); + expect(source).not.toContain('copyAgentContext'); + } + + for (const statePath of [ + 'src/components/Infrastructure/useResourceDetailDrawerState.ts', + 'src/components/Workloads/useGuestDrawerState.ts', + ]) { + const source = readFrontendSource(statePath); + expect(source).not.toContain('AgentContextAPI'); + expect(source).not.toContain('formatAgentResourceContextForClipboard'); + } + const disclosureSource = readFrontendSource( 'src/components/shared/TechnicalDetailsDisclosure.tsx', );