diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b8f63afa5e..8ccfd78b25 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -95,7 +95,7 @@ All backend code lives under `cmd/`, `internal/`, and `pkg/`. The binary is asse - Gated by the `relay` license feature. 7. **AI Subsystem (`internal/ai`)** - - **Pulse Assistant (Chat)**: Interactive LLM-powered chat with infrastructure context. Supports bring-your-own-key (BYOK) providers (OpenAI, Anthropic, Ollama, etc.) plus Claude OAuth. + - **AI runtime (`internal/ai/chat`)**: The governed tool loop behind Patrol investigations and Pulse Mobile chat (the in-app chat window has been retired). Supports bring-your-own-key (BYOK) providers (OpenAI, Anthropic, Ollama, etc.) plus Claude OAuth. External agents reach the same tools through `pulse-mcp`. - **Pulse Patrol**: Scheduled background analysis that produces findings, predictions, and remediation plans. Autonomy levels: `monitor`, `approval`, `assisted`, `full`. - **Intelligence Services**: Patterns, correlations, anomalies, baselines, forecasts, and incident recording. All surfaced via `/api/ai/intelligence/*`. - **Safety gates**: Command execution disabled by default (`--enable-commands` opt-in); circuit breakers and scoped permissions at every layer. @@ -174,7 +174,7 @@ contracts remain backend building blocks consumed inside platform pages. ### State Management - **WebSocket store** (`stores/websocket.ts`): Manages the live connection, reactive `State` object, reconnection logic, and per-org switching. - **Metrics collector** (`stores/metricsCollector.ts`): Buffers time-series data for sparklines and historical charts. -- **AI stores** (`stores/aiChat.ts`, `stores/aiIntelligence.ts`): Manage chat sessions and patrol findings. +- **AI stores** (`stores/aiAvailability.ts`, `stores/aiIntelligence.ts`): Hold whether AI is enabled for the session and the Patrol findings. - **License store** (`stores/license.ts`): Tracks plan tier, feature flags, and multi-tenant state. - **System settings store** (`stores/systemSettings.ts`): Caches server-side preferences (for example theme and feature toggles). diff --git a/docs/AGENT_SUBSTRATE.md b/docs/AGENT_SUBSTRATE.md index 2c6b296143..391d242da2 100644 --- a/docs/AGENT_SUBSTRATE.md +++ b/docs/AGENT_SUBSTRATE.md @@ -214,6 +214,6 @@ Pulse; verification can be unavailable without proving that nothing ran. - [Configuration](CONFIGURATION.md) for API tokens and their scopes. - [API reference](API.md) for the wider Pulse HTTP surface. -- [AI features](AI.md) for how Patrol and the Assistant use the same context. +- [AI features](AI.md) for how Patrol and external agents use the same context. - `cmd/pulse-mcp/README.md` in the repository for adapter setup examples covering OpenCode, Claude Desktop, and Claude Code. diff --git a/docs/AI.md b/docs/AI.md index bc7bb58eb1..ce947c1f50 100644 --- a/docs/AI.md +++ b/docs/AI.md @@ -18,19 +18,6 @@ That core is deliberately surfaced with Patrol as the primary built-in operator These surfaces are built on the same action-driven architecture: the configured LLM owns diagnosis, prioritization, fix reasoning, and action choice; Pulse supplies context, capabilities, safety gates, approval state, and audit trails. Verification is part of the governed action lifecycle rather than a separate model-owned feature. -### Not Just Another Chatbot - -Pulse Assistant is a **protocol-driven, safety-gated LLM tool surface** that: - -- **Provides governed context** — attaches explicit resource mentions and recent session facts without rewriting user intent -- **Caches session facts** — extracts bounded tool facts to avoid redundant queries during the current conversation -- **Enforces workflow invariants** — FSM prevents dangerous state transitions -- **Supports parallel tool execution** — efficient batch operations with concurrency control -- **Grounds answers in real tool work** — visible tool traces, read-after-write verification, and transcript hygiene prevent unsupported execution claims from being treated as facts -- **Returns structured tool errors** — the model can recover from clear, machine-readable failures - -📖 **For a deep technical dive into the Assistant architecture, see [Assistant architecture](ASSISTANT_ARCHITECTURE.md).** - ### Not Just Another Alerting System Pulse Patrol is a **scheduled and event-triggered governed operator** that: @@ -48,20 +35,25 @@ All while running entirely on your infrastructure with BYOK for complete privacy 🧪 **For independent live-fault qualification, safety gates, model comparison, and release-claim rules, see [AI_PATROL_QUALIFICATION.md](AI_PATROL_QUALIFICATION.md).** -See [Assistant safety architecture](ASSISTANT_SAFETY.md) for the original safety architecture documentation. +See [Assistant safety architecture](ASSISTANT_SAFETY.md) for the safety gates +the shared AI runtime enforces. -### Assistant And MCP +### Using Pulse from Claude Code, Codex, and other agents -Pulse Assistant and `pulse-mcp` are sibling surfaces over Pulse Intelligence, -not competing implementations, and neither replaces the other. Assistant remains -the in-app Pro surface for current resource/finding/run handoffs, approval -cards, governed action status, and operator-friendly timelines. `pulse-mcp` -owns the external-agent bridge: it fetches `/api/agent/capabilities`, projects -those canonical API capabilities as MCP tools, and preserves the same stable -error envelopes and approval/audit contracts. New operational capabilities -should be added to the canonical API manifest first, then consumed by Assistant -or MCP as appropriate; MCP-only actions and Assistant-only copies of the same -business logic are drift. +Pulse has no in-app chat window. To ask about your infrastructure in plain +language, connect an MCP client such as Claude Code, Codex, or OpenCode to +`pulse-mcp`: open **Settings → Pulse Intelligence → External agents** and +choose **Show connector setup**. `pulse-mcp` fetches `/api/agent/capabilities`, +projects those canonical API capabilities as MCP tools, and preserves the same +stable error envelopes and approval/audit contracts as Patrol. New operational +capabilities are added to the canonical API manifest first; MCP-only copies of +the same business logic are drift. + +The tool list is registry-owned at runtime, not hand-maintained here. For the +current inventory, read the live `/api/agent/capabilities` manifest or +`pulse-mcp` `tools/list`. The same manifest carries reusable `workflowPrompts` +metadata, so MCP `prompts/list` clients discover the fleet triage, resource +investigation, and Patrol finding review workflows. --- @@ -136,10 +128,7 @@ create a second finding lifecycle. explicitly. Select an item to see the affected resource, impact, next step, typed evidence, -protection posture, and lifecycle timeline. **Explain with Assistant** appears -only inside selected context and receives policy-shaped summaries and -references. Assistant can explain the item, but it cannot create lifecycle -truth, invent action authority, or hide uncertainty. +protection posture, and lifecycle timeline. Open items can be acknowledged directly. Suppression is temporary and requires an operator reason plus a bounded expiry; it never resolves the detector's @@ -228,7 +217,6 @@ Note: `info` and `watch` level findings are filtered out to reduce noise. Findings can be managed via the UI or API: -- **Open in Assistant**: Open Pulse Assistant with the finding attached to troubleshoot the issue (on Patrol findings it appears under **Resolve or dismiss**) - **Resolve**: Mark as fixed (finding will reappear if the issue resurfaces) - **Dismiss**: Record feedback about this finding; use **Reopen** to undo its dismissal - **Create rule**: Permanently auto-dismiss future findings for the selected resource and category @@ -434,74 +422,25 @@ MaybeInvestigateFinding() --- -## Pulse Assistant (Chat) +## Pulse Assistant chat (retired) -Pulse Assistant is a **tool-driven** chat interface. It does not "guess" system state — it calls live tools and reports their outputs. +The in-app Pulse Assistant chat window and its Settings page have been retired. +Patrol is the built-in operator, and the governed tools the chat used are +available to external agents through `pulse-mcp` (see +[Using Pulse from Claude Code, Codex, and other agents](#using-pulse-from-claude-code-codex-and-other-agents)). +An alert's **Have Patrol investigate** button starts a targeted Patrol check on +the alert's resource; its findings appear on the Patrol page. -### The Model's Workflow (Discover → Investigate → Act) +Pulse Mobile keeps its chat tab until the app retires on 31 March 2027. It uses +the chat action level saved before the retirement, and the chat model override +if one was saved, otherwise the default model from **Provider & Models**. The web +settings no longer set the action level or the override; removing the provider +an override names clears it, and Mobile chat then uses the default model. -1. **Discover**: Uses `pulse_query` to find real resources and IDs -2. **Investigate**: Uses `pulse_read` to run bounded, read-only commands and check status/logs -3. **Act** (optional): Uses `pulse_control` for changes, then verifies with a read - -### Tool Inventory - -The Assistant tool list is registry-owned at runtime, not hand-maintained in -this public overview. Each turn receives an available-tool manifest generated -from Pulse's governed tool registry, including action mode (`read`, `mixed`, -`write`) and approval policy (`scope_only`, `action_plan`). That same registry -feeds the Assistant system prompt, provider tool declarations, tool-result -handling, approval boundaries, and Patrol-only tool filtering. - -For the current source-owned inventory, see the native tool registry and -governance projection in `internal/ai/tools/` and -`internal/agentcapabilities/`. For external agents, use the live -`/api/agent/capabilities` manifest or `pulse-mcp` `tools/list`; those surfaces -project the canonical agent capabilities rather than a separate MCP-only tool -table. -The same manifest also carries reusable `workflowPrompts` metadata so Pulse -Assistant-compatible starters and MCP `prompts/list` clients discover the same -fleet triage, resource investigation, and Patrol finding review workflows. - -### Safety Gates - -The assistant enforces multiple safety gates: - -1. **Resolve Before Action** — Action tools cannot operate on resources that weren't first resolved by a read tool -2. **Verification After Write** — After any write, the model must perform a read/status check before providing a final answer -3. **Read/Write Separation** — Read operations route through `pulse_read` (stays in READING state); write operations route through `pulse_control` (enters VERIFYING state) -4. **Grounded Execution Guardrails** — Visible tool traces and read-after-write checks prevent unsupported execution claims from being treated as facts -5. **Approval Mode** — In Controlled mode, every write requires explicit user approval -6. **Execution Context Binding** — Commands execute within the resolved resource's context, not on parent hosts - -### Control Levels - -| Mode | Behaviour | Availability | -|------|-----------|--------------| -| **Read-only** | Assistant queries and explains; no infrastructure action planning | Community | -| **Ask first** (`controlled`) | Assistant saves infrastructure action plans to **Actions** for your review | Community | - -Assistant chat does not execute these plans. Patrol's own mode and Autopilot -policy are separate; changing Chat action mode grants no Patrol authority. -Older saved `autonomous` preferences display as **Ask first**, not as permission -to execute without approval. The stored preference and its existing entitlement -checks are preserved for older clients. - -To keep Assistant away from a specific guest, lock it: open the guest, choose -**Manage**, turn on **Never auto-remediate** under Operator overrides, and save. -Pulse then refuses typed actions that target that resource, including -approved ones. Older versions also showed a **Protected guests** list here. It -never excluded a guest from action plans, so it has been removed. - -### Reviewing an Assistant action plan - -1. Assistant submits a typed action for a resource advertising that capability. -2. Pulse records the plan in **Actions**; planning is not execution. -3. Open **Actions** to check the target, proposed operation, approval policy and expiry. -4. An authorised operator reviews, approves where required, and runs the plan - through the existing action controls. Chat mode cannot bypass these checks. -5. Read the recorded execution and independent verification result. A saved plan - or successful command alone does not establish recovery. +To keep automated actions away from a specific guest, lock it: open the guest, +choose **Manage**, turn on **Never auto-remediate** under Operator overrides, +and save. Pulse then refuses typed actions that target that resource, including +approved ones. --- @@ -959,7 +898,7 @@ Pulse tracks token usage and costs: | Issue | Solution | |-------|----------| -| Assistant or Patrol not responding | Verify provider credentials in **Settings → Pulse Intelligence → Provider & Models** | +| Patrol not responding | Verify provider credentials in **Settings → Pulse Intelligence → Provider & Models** | | No execution capability | Confirm at least one agent is connected | | Findings not persisting | Check Pulse has write access to `ai_findings.json` in the config directory | | Too many findings | This shouldn't happen — please report if it does | diff --git a/docs/AI_AUTONOMY.md b/docs/AI_AUTONOMY.md index 9385abbd13..f327415c16 100644 --- a/docs/AI_AUTONOMY.md +++ b/docs/AI_AUTONOMY.md @@ -1,19 +1,20 @@ # Pulse Intelligence Modes and Safety Configuration -This guide covers how to configure Patrol mode, Pulse Assistant command access, and the safety guardrails that apply before Pulse can change infrastructure. +This guide covers how to configure Patrol mode and the safety guardrails that apply before Pulse can change infrastructure. For a general overview of Pulse Intelligence, see [AI.md](AI.md). For plan-level feature availability, see [PULSE_PRO.md](PULSE_PRO.md). --- -## Two Axes of Control +## Patrol Mode Is the Control -Pulse separates AI permissions into two independent axes: +**Patrol Mode** decides what Patrol may handle automatically after it finds an +issue: watch only, ask before changes, handle safe fixes, or use policy +autopilot. Patrol mode is configured on the **Patrol** page. -1. **Patrol Mode** — What Patrol may handle automatically after it finds an issue: watch only, ask before changes, handle safe fixes, or use policy autopilot. -2. **Assistant Command Access** — Whether the interactive chat assistant can execute commands during a chat session. - -Patrol mode is configured on the **Patrol** page. Assistant command access is configured in **Settings → Pulse Intelligence → Assistant**. +The in-app Pulse Assistant chat, and its separate chat action setting, have been +retired. Pulse Mobile's chat keeps the level saved before then until the app +retires on 31 March 2027 (see [Pulse Mobile chat control level](#pulse-mobile-chat-control-level)). --- @@ -95,43 +96,41 @@ Without the `ai_autofix` capability, the effective Patrol mode is clamped to `mo --- -## Assistant Control Levels +## Pulse Mobile chat control level -Control levels govern interactive Assistant queries and action planning, not -Patrol's autonomy or permission to execute infrastructure work. +The control level governs Pulse Mobile chat queries and action planning, not +Patrol's autonomy or permission to execute infrastructure work. The web +settings page that edited it was retired with the in-app chat; the saved value +still applies to Pulse Mobile until the app retires on 31 March 2027. | Mode | Key | Query | Plan infrastructure actions | Availability | |------|-----|:-----:|:---------------------------:|--------------| | **Read-only** | `read_only` | Yes | No | Community | | **Ask first** | `controlled` | Yes | For operator review in **Actions** | Community | -Assistant chat does not execute the plans it saves. A legacy saved `autonomous` -preference displays as **Ask first** and keeps its stored value and existing -entitlement checks for older clients. It does not bypass approval. An unrelated -settings save does not rewrite that preference. Unknown values fail closed to -read-only. Patrol mode and Autopilot acknowledgements remain separate. +Chat does not execute the plans it saves. A legacy saved `autonomous` +preference acts as **Ask first** and keeps its stored value and existing +entitlement checks for older clients. It does not bypass approval. Web settings +saves never rewrite that preference. Unknown values fail closed to read-only. +Patrol mode and Autopilot acknowledgements remain separate. -To keep Assistant away from a specific guest, lock it: open the guest, choose -**Manage**, turn on **Never auto-remediate** under Operator overrides, and save. -Pulse then refuses typed actions that target that resource, including +To keep chat and Patrol away from a specific guest, lock it: open the guest, +choose **Manage**, turn on **Never auto-remediate** under Operator overrides, +and save. Pulse then refuses typed actions that target that resource, including approved ones (see [Keep a Resource Off-Limits](#keep-a-resource-off-limits)). -Older versions also showed a **Protected guests** list here. It never excluded a -guest from action plans, so it has been removed. ### Configuration -**UI:** Settings → Pulse Intelligence → Assistant → Chat action mode - -**API:** This intentionally changes Assistant action planning access. Use the same private -administrator header file with `settings:write`; the token must also have -permission to change settings. Prefer the UI for one-off changes, and verify -the saved **Chat action mode** there afterwards. Do not use a write as an -authentication test. +**API only.** This intentionally changes Pulse Mobile chat action planning +access. Use the same private administrator header file with `settings:write`; +the token must also have permission to change settings. Read +`control_level` from `GET /api/settings/ai` afterwards to confirm the saved +value. Do not use a write as an authentication test. Use the same `pulse_api` helper defined above, with its private response file and request limits. Supporting PUT in the helper grants no additional access. -If the response is lost, check the saved **Chat action mode** in the UI before -deciding whether another change is needed; do not repeat the write blindly. +If the response is lost, read the saved `control_level` before deciding +whether another change is needed; do not repeat the write blindly. ```bash pulse_api PUT /api/settings/ai/update <<'JSON' @@ -141,12 +140,12 @@ JSON ### Reviewing plans (Ask first) -1. Assistant requests a typed action for an explicitly identified resource with +1. Chat requests a typed action for an explicitly identified resource with the advertised capability. 2. Pulse saves the canonical plan in **Actions** without requesting execution. 3. Review the target, operation, approval policy and expiry in **Actions**. -4. Use the existing authorised approval and run controls; Chat action mode grants - no additional access or automatic approval. +4. Use the existing authorised approval and run controls; the chat control + level grants no additional access or automatic approval. 5. Read the recorded execution and independent verification outcome before concluding that the operation worked. Do not repeat an uncertain operation. @@ -186,15 +185,15 @@ If the AI provider experiences consecutive failures, the circuit breaker (`inter ### Discovery-Before-Action -The assistant cannot operate on resources it hasn't first discovered. This prevents hallucinated resource IDs from reaching infrastructure commands. +The AI runtime cannot operate on resources it hasn't first discovered. This prevents hallucinated resource IDs from reaching infrastructure commands. ### Verification-After-Write -After executing any control action, the assistant must verify the result with a read operation before reporting success. This is enforced by the FSM — the assistant cannot return to idle state without verification. +After executing any control action, the AI runtime must verify the result with a read operation before reporting success. This is enforced by the FSM — the runtime cannot return to idle state without verification. ### Keep a Resource Off-Limits -To stop Pulse acting on a specific guest, host or container, open its drawer, choose **Manage**, turn on **Never auto-remediate** under Operator overrides, confirm the prompt, and click **Save overrides**. Pulse then refuses typed actions that target that exact resource: the Assistant declines to plan one when it can read the lock, Patrol will not run one, and the action lifecycle checks the lock again before it runs an action that was already planned, even when approved. The lock covers that resource only, not its host or other resources, and it does not stop read-only investigation. Turn it off and save again when you want Pulse to act on the resource again. A resource whose lifecycle is Retired is blocked the same way until the lifecycle is set back to Active. +To stop Pulse acting on a specific guest, host or container, open its drawer, choose **Manage**, turn on **Never auto-remediate** under Operator overrides, confirm the prompt, and click **Save overrides**. Pulse then refuses typed actions that target that exact resource: chat declines to plan one when it can read the lock, Patrol will not run one, and the action lifecycle checks the lock again before it runs an action that was already planned, even when approved. The lock covers that resource only, not its host or other resources, and it does not stop read-only investigation. Turn it off and save again when you want Pulse to act on the resource again. A resource whose lifecycle is Retired is blocked the same way until the lifecycle is set back to Active. --- diff --git a/docs/ASSISTANT_ARCHITECTURE.md b/docs/ASSISTANT_ARCHITECTURE.md index ffa6e3f3de..8258b3feed 100644 --- a/docs/ASSISTANT_ARCHITECTURE.md +++ b/docs/ASSISTANT_ARCHITECTURE.md @@ -1,5 +1,10 @@ # Pulse Assistant deep dive +> **Status:** the in-app Assistant chat window has been retired. This runtime +> still runs Patrol investigations, and Pulse Mobile's chat until the app +> retires on 31 March 2027. External agents reach the same governed tools +> through `pulse-mcp`. + How the Assistant's agentic loop executes tool calls, for readers who want more than the overview in [AI features](AI.md). diff --git a/docs/ASSISTANT_SAFETY.md b/docs/ASSISTANT_SAFETY.md index 6261261c6a..26206ad6d4 100644 --- a/docs/ASSISTANT_SAFETY.md +++ b/docs/ASSISTANT_SAFETY.md @@ -1,5 +1,10 @@ # Pulse Assistant safety architecture +> **Status:** the in-app Assistant chat window has been retired. This runtime +> still runs Patrol investigations, and Pulse Mobile's chat until the app +> retires on 31 March 2027. External agents reach the same governed tools +> through `pulse-mcp`. + Pulse enforces authority at the shared tool and action boundaries. The model owns interpretation, investigation and action judgment within those boundaries. A sequence of tool calls cannot establish that a diagnosis is correct. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 7002eae391..78fc542216 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -1175,7 +1175,7 @@ verification enabled and do not share verbose or trace output. | `agent:config:read` | Read agent config payloads | | `agent:manage` | Agent lifecycle/configuration changes and unregistering; not needed by the reporting preset | | `agent:exec` | Establish agent command WebSocket connections; not a reporting permission | -| `ai:chat` | Use Pulse Assistant chat and read knowledge | +| `ai:chat` | Use Pulse Mobile chat and read knowledge | | `ai:execute` | Use governed Patrol plans, approvals, actions, and history | | `settings:read` | Read configuration | | `settings:write` | Modify configuration, manage tokens and trigger updates | diff --git a/docs/PULSE_PRO.md b/docs/PULSE_PRO.md index e62835c643..809f15f137 100644 --- a/docs/PULSE_PRO.md +++ b/docs/PULSE_PRO.md @@ -176,7 +176,7 @@ keys and old 14-day limit remain in code without changing that policy. ## Patrol Modes -Patrol mode decides how far Pulse can go after Patrol finds something that needs attention. Assistant chat command access is configured separately. +Patrol mode decides how far Pulse can go after Patrol finds something that needs attention. | Mode | Behavior | Plan | |---|---|---| diff --git a/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_ALERTS_AND_ASSISTANT_SPEC.md b/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_ALERTS_AND_ASSISTANT_SPEC.md index 34284c7f42..1ef7e5f179 100644 --- a/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_ALERTS_AND_ASSISTANT_SPEC.md +++ b/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_ALERTS_AND_ASSISTANT_SPEC.md @@ -1,7 +1,7 @@ # VMware vCenter Phase-1 Alerts And Assistant Spec Last updated: 2026-03-30 -Status: IMPLEMENTED (shared alert/history and read-only Assistant paths shipped: `frontend-modern/src/components/Alerts/ThresholdsTableVMwareTab.tsx`, `tests/integration/tests/36-vmware-alert-history-resource-incidents.spec.ts`, `38-vmware-ai-chat-mentions.spec.ts`, `42-vmware-ai-chat-read-recovery.spec.ts`, `internal/ai/tools/control_resource_test.go`; live alarm-fidelity proof on a real `vCenter` is still outstanding). Last reviewed 2026-09-01. +Status: IMPLEMENTED (shared alert/history and read-only Assistant paths shipped: `frontend-modern/src/components/Alerts/ThresholdsTableVMwareTab.tsx`, `tests/integration/tests/36-vmware-alert-history-resource-incidents.spec.ts`, `internal/ai/tools/control_resource_test.go`; the browser chat specs 38 and 42 were retired with the Assistant chat window on 2026-10-22; live alarm-fidelity proof on a real `vCenter` is still outstanding). Last reviewed 2026-09-01. Governance surfaces: - `status.json.candidate_lanes.platform-admission-execution` - `docs/release-control/v6/internal/VMWARE_VSPHERE_PHASE1_EXECUTION_PLAN.md` diff --git a/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_PROOF_MATRIX.md b/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_PROOF_MATRIX.md index 956333aa71..cd2e5b2c40 100644 --- a/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_PROOF_MATRIX.md +++ b/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_PROOF_MATRIX.md @@ -116,20 +116,16 @@ If the automated floor is missing, the manual drill may inform implementation, but it must not be used as the sole basis for a support claim. Current repo-local automated proof already covers part of the read-only -boundary: +boundary. The browser chat specs `38-vmware-ai-chat-mentions` and +`42-vmware-ai-chat-read-recovery` were retired with the Assistant chat window +on 2026-10-22; the Go proofs below still cover the backend chat path. -1. `tests/integration/tests/38-vmware-ai-chat-mentions.spec.ts` proves - canonical VMware-backed Assistant mentions stay on shared resource IDs and - shared Assistant routes. -2. `tests/integration/tests/42-vmware-ai-chat-read-recovery.spec.ts` proves a - failed VMware `pulse_read action=logs resource_id=...` path can recover onto - shared `pulse_query` behavior without VMware-local routes. -3. `internal/ai/chat/context_prefetch_additional_test.go` proves VMware-backed +1. `internal/ai/chat/context_prefetch_additional_test.go` proves VMware-backed guest mentions stay read-only in Assistant context summaries. -4. `internal/ai/chat/service_tooling_test.go` and +2. `internal/ai/chat/service_tooling_test.go` and `internal/ai/tools/control_resource_test.go` prove shared Assistant/tool wording stays capability-gated instead of claiming blanket VM control. -5. `internal/api/route_inventory_test.go` and +3. `internal/api/route_inventory_test.go` and `tests/integration/tests/41-vmware-phase1-exclusion-integrity.spec.ts` prove the shipped route and browser surface stay out of direct `ESXi`, recovery, and VMware admin-plane territory. @@ -292,13 +288,13 @@ Pass when: 3. no control capability or VMware-specific action metadata is exposed as part of the VMware phase-1 floor -Current automated coverage: +Current automated coverage (the browser chat specs `38-vmware-ai-chat-mentions` and +`42-vmware-ai-chat-read-recovery` were retired with the Assistant chat window +on 2026-10-22; the Go proofs below still cover the backend chat path): -1. `tests/integration/tests/38-vmware-ai-chat-mentions.spec.ts` -2. `tests/integration/tests/42-vmware-ai-chat-read-recovery.spec.ts` -3. `internal/ai/chat/context_prefetch_additional_test.go` -4. `internal/ai/chat/service_tooling_test.go` -5. `internal/ai/tools/control_resource_test.go` +1. `internal/ai/chat/context_prefetch_additional_test.go` +2. `internal/ai/chat/service_tooling_test.go` +3. `internal/ai/tools/control_resource_test.go` ### `VC-7` Exclusion Integrity diff --git a/docs/release-control/v6/internal/VMWARE_VSPHERE_PHASE1_EXECUTION_PLAN.md b/docs/release-control/v6/internal/VMWARE_VSPHERE_PHASE1_EXECUTION_PLAN.md index bcda7bf618..5ae30917cc 100644 --- a/docs/release-control/v6/internal/VMWARE_VSPHERE_PHASE1_EXECUTION_PLAN.md +++ b/docs/release-control/v6/internal/VMWARE_VSPHERE_PHASE1_EXECUTION_PLAN.md @@ -190,10 +190,10 @@ Required proof: - assistant control remains read-only in both product wording and runtime capability exposure -Automated proof already landed for part of this slice: +Automated proof already landed for part of this slice. The browser chat specs `38-vmware-ai-chat-mentions` and +`42-vmware-ai-chat-read-recovery` were retired with the Assistant chat window +on 2026-10-22; the Go proofs below still cover the backend chat path. -- `tests/integration/tests/38-vmware-ai-chat-mentions.spec.ts` -- `tests/integration/tests/42-vmware-ai-chat-read-recovery.spec.ts` - `internal/ai/chat/context_prefetch_additional_test.go` - `internal/ai/chat/service_tooling_test.go` - `internal/ai/tools/control_resource_test.go` diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index 2890c3276d..eeea1a4d23 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -2815,18 +2815,13 @@ "test", "--", "--run", - "src/components/AI/Chat/__tests__/AIChat.test.tsx", - "src/components/AI/Chat/__tests__/ChatMessages.test.tsx", "src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts", "src/components/patrol/__tests__/ApprovalSection.test.tsx", "src/components/AI/__tests__/FindingsPanel.test.ts", "src/pages/__tests__/AIIntelligence.test.tsx", "src/utils/__tests__/patrolEmptyStatePresentation.test.ts", "src/utils/__tests__/patrolPagePresentation.test.ts", - "src/utils/__tests__/patrolSummaryPresentation.test.ts", - "src/utils/__tests__/aiChatPresentation.test.ts", - "src/api/__tests__/aiChat.test.ts", - "src/stores/__tests__/aiChat.test.ts" + "src/utils/__tests__/patrolSummaryPresentation.test.ts" ] }, { @@ -2901,21 +2896,6 @@ "path": "frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts", "kind": "file" }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/__tests__/ChatMessages.test.tsx", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/index.tsx", - "kind": "file" - }, { "repo": "pulse", "path": "frontend-modern/src/components/AI/FindingsPanel.tsx", @@ -2951,11 +2931,6 @@ "path": "frontend-modern/src/utils/__tests__/patrolPagePresentation.test.ts", "kind": "file" }, - { - "repo": "pulse", - "path": "frontend-modern/src/utils/aiChatPresentation.ts", - "kind": "file" - }, { "repo": "pulse", "path": "frontend-modern/src/utils/patrolPagePresentation.ts", @@ -3005,7 +2980,7 @@ }, { "id": "RA29", - "summary": "Pulse intelligence comes from the operator-selected large language model using governed Pulse tools over data the model cannot reach on its own: agent-collected machine evidence, canonical APIs, metrics, discovery records, and resource state. Interactive Pulse Assistant chat therefore behaves like a governed LLM tool surface, not a Pulse-authored intent router: the operator's selected model receives the user turn and governed tools, decides whether tools are needed, and Pulse must not use prompt heuristics to force tool_choice, force a named tool, retry because an expected tool was not used, hide tools from the model by keyword detection, rewrite recent-context turns into Pulse-targeted instructions, fuzzy-match plain chat text into resource context before the model acts, synthesize/prefill/auto-submit product-authored Assistant prompts from handoffs, keyword-match prior remediation history into suggested fixes, or generate Patrol handoff attention/decision/remediation guidance before the model reasons. Pulse only provides current context and enforces approvals, canonical resource resolution, explicit budgets, tool policy and independent action verification after that model choice; any model-visible policy block must describe the boundary without naming a required next tool. The chat surface must echo user messages before network/session creation finishes and must not render Pulse-owned explore or internal workflow status cards as assistant output. Generic read/write sequences, proposal acceptance, successful-call counts and failed-call streaks must not stand in for diagnosis, verified recovery or action judgment. Saved history preserves the assistant prose actually streamed.", + "summary": "Pulse intelligence comes from the operator-selected large language model using governed Pulse tools over data the model cannot reach on its own: agent-collected machine evidence, canonical APIs, metrics, discovery records, and resource state. Interactive Pulse Assistant chat therefore behaves like a governed LLM tool surface, not a Pulse-authored intent router: the operator's selected model receives the user turn and governed tools, decides whether tools are needed, and Pulse must not use prompt heuristics to force tool_choice, force a named tool, retry because an expected tool was not used, hide tools from the model by keyword detection, rewrite recent-context turns into Pulse-targeted instructions, fuzzy-match plain chat text into resource context before the model acts, synthesize/prefill/auto-submit product-authored Assistant prompts from handoffs, keyword-match prior remediation history into suggested fixes, or generate Patrol handoff attention/decision/remediation guidance before the model reasons. Pulse only provides current context and enforces approvals, canonical resource resolution, explicit budgets, tool policy and independent action verification after that model choice; any model-visible policy block must describe the boundary without naming a required next tool. Generic read/write sequences, proposal acceptance, successful-call counts and failed-call streaks must not stand in for diagnosis, verified recovery or action judgment. Saved history preserves the assistant prose actually streamed.", "kind": "invariant", "blocking_level": "repo-ready", "proof_type": "automated", @@ -3041,32 +3016,6 @@ "-count=1" ] }, - { - "id": "assistant-chat-optimistic-echo-tests", - "run": [ - "npm", - "--prefix", - "frontend-modern", - "test", - "--", - "--run", - "src/components/AI/Chat/__tests__/useChat.test.ts", - "src/components/AI/Chat/__tests__/MessageItem.test.tsx", - "src/components/AI/Chat/__tests__/activeTurnStatus.test.ts" - ] - }, - { - "id": "assistant-chat-stream-contract-tests", - "run": [ - "npm", - "--prefix", - "frontend-modern", - "test", - "--", - "--run", - "src/api/__tests__/aiChatEvents.test.ts" - ] - }, { "id": "assistant-chat-stream-json-contract-tests", "run": [ @@ -3098,8 +3047,6 @@ "test", "--", "src/components/shared/__tests__/AIModelPicker.test.tsx", - "src/components/AI/Chat/__tests__/ModelSelector.test.tsx", - "src/components/AI/Chat/__tests__/AIChat.test.tsx", "src/components/shared/__tests__/SearchField.test.tsx" ] }, @@ -3134,8 +3081,7 @@ "test", "--", "--run", - "src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts", - "src/components/AI/Chat/__tests__/AIChat.test.tsx" + "src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts" ] }, { @@ -3203,91 +3149,16 @@ "path": "docs/release-control/v6/internal/subsystems/storage-recovery.md", "kind": "file" }, - { - "repo": "pulse", - "path": "frontend-modern/src/api/__tests__/aiChatEvents.test.ts", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/api/generated/aiChatEvents.ts", - "kind": "file" - }, { "repo": "pulse", "path": "frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts", "kind": "file" }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/__tests__/ChatMessages.test.tsx", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/__tests__/MessageItem.test.tsx", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/__tests__/ModelSelector.test.tsx", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/__tests__/useChat.test.ts", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/ChatMessages.tsx", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/hooks/useChat.ts", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/index.tsx", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/MessageItem.tsx", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/ModelSelector.tsx", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/types.ts", - "kind": "file" - }, { "repo": "pulse", "path": "frontend-modern/src/components/AI/FindingsPanel.tsx", "kind": "file" }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts", - "kind": "file" - }, { "repo": "pulse", "path": "frontend-modern/src/components/shared/__tests__/AIModelPicker.test.tsx", @@ -3318,31 +3189,11 @@ "path": "frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts", "kind": "file" }, - { - "repo": "pulse", - "path": "frontend-modern/src/stores/__tests__/aiChat.test.ts", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/stores/aiChat.ts", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/utils/__tests__/aiChatPresentation.test.ts", - "kind": "file" - }, { "repo": "pulse", "path": "frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts", "kind": "file" }, - { - "repo": "pulse", - "path": "frontend-modern/src/utils/aiChatPresentation.ts", - "kind": "file" - }, { "repo": "pulse", "path": "internal/ai/chat/agentic.go", @@ -7503,16 +7354,6 @@ "path": "frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts", "kind": "file" }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx", - "kind": "file" - }, - { - "repo": "pulse", - "path": "frontend-modern/src/components/AI/Chat/index.tsx", - "kind": "file" - }, { "repo": "pulse", "path": "frontend-modern/src/components/AI/FindingsPanel.tsx", diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 267021b175..045699ff64 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -10166,6 +10166,16 @@ facts as a `DetailSectionTable`; the connection id and identity evidence sit in a closed Details disclosure. No diagnosis, repair or credential flow changes: this is presentation only. +The agent profiles settings panel reads AI availability from +`frontend-modern/src/stores/aiAvailability.ts` since 2026-10-22, when the +browser Assistant chat window and its store were retired. +`useAgentProfilesPanelState.ts` still gates only its AI-backed profile +suggestions on that session fact; agent enrolment, profile assignment, and +lifecycle behaviour are unchanged. The same change adds the read-only +`settingsCapabilities.patrolRun` security-status field in +`internal/api/security_status_capabilities.go`, a projection of the Patrol run +route's admin gate; it opens no agent enrolment, command, or lifecycle path. + The Patrol investigation wiring in `internal/api/ai_handlers.go` now passes the typed conclusion an investigation records with `patrol_conclude_investigation` to the private orchestrator. It also seeds the orchestrator's concurrency and diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 05931e26bd..1c5d9f5bf3 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -385,11 +385,6 @@ coalescing or the stored format, and it is not a shutdown flush. `TestIncidentStoreFlushJoinsQueuedSave` proves the join covers a queued save that has not started. -Assistant owns composer registration and focus on every open, rather than only -on component mount. Closing clears the registered input so later keyboard -commands cannot target a detached composer. A handoff must leave Escape and -keyboard input in Assistant, not the underlying alert search. - ### Process-wide memory ID ownership `internal/ai/memory` preserves random UUID incident and incident-event IDs from @@ -1044,11 +1039,11 @@ terminal decision state, not proven operations value; `verified` is the only completed first-party value state and requires the approved, verified outcome plus recorded action history. MCP/external-agent readiness remains optional external-agent setup context. -Native Assistant workflow starters in `frontend-modern/src/components/AI/Chat/` -are a browser projection of that same catalogue, not a second prompt registry: -they may decide which manifest-owned prompt is contextually usable, but display -labels and rendered prompt text must come from shared manifest metadata and the -shared backend renderer on +Any first-party workflow starter surface is a projection of that same +catalogue, not a second prompt registry (the browser Assistant that hosted the +starters was retired on 2026-10-22): it may decide which manifest-owned prompt +is contextually usable, but display labels and rendered prompt text must come +from shared manifest metadata and the shared backend renderer on `POST /api/ai/workflow-prompts/render`. AI provider integration is registry-owned. Provider identity, display names, protocol family, default model, default base URL, credential fields, @@ -1354,29 +1349,25 @@ a measured cost reduction or installed-deployment cache-read rate. 7. `frontend-modern/src/api/agentCapabilities.ts` 7a. `frontend-modern/src/api/generated/agentCapabilities.ts` 8. `frontend-modern/src/api/ai.ts` - 8a. `frontend-modern/src/api/aiChat.ts` + 8a. `frontend-modern/src/api/workflowPrompts.ts` 9. `frontend-modern/src/api/patrol.ts` 10. `frontend-modern/src/components/AI/AICostDashboard.tsx` 11. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` -12. `frontend-modern/src/components/AI/Chat/` -13. `frontend-modern/src/utils/aiChatPresentation.ts` -14. `frontend-modern/src/utils/assistantPageContext.ts` -15. `frontend-modern/src/utils/aiControlLevelPresentation.ts` -16. `frontend-modern/src/utils/aiCostPresentation.ts` -17. `frontend-modern/src/utils/aiProviderHealthPresentation.ts` -18. `frontend-modern/src/utils/aiProviderPresentation.ts` -19. `frontend-modern/src/utils/textPresentation.ts` -20. `frontend-modern/src/stores/aiRuntimeState.ts` -21. `frontend-modern/src/stores/aiChat.ts` -22. `docs/AI.md` -23. `pkg/aicontracts/investigation.go` -24. `pkg/aicontracts/orchestrator_deps.go` -25. `pkg/aicontracts/fix_execution.go` -26. `pkg/extensions/ai_autofix.go` -27. `internal/ai/qualification/` -28. `cmd/patrol-qualify/` -29. `tests/qualification/patrol/` -30. `docs/AI_PATROL_QUALIFICATION.md` +12. `frontend-modern/src/utils/aiCostPresentation.ts` +13. `frontend-modern/src/utils/aiProviderHealthPresentation.ts` +14. `frontend-modern/src/utils/aiProviderPresentation.ts` +15. `frontend-modern/src/utils/textPresentation.ts` +16. `frontend-modern/src/stores/aiRuntimeState.ts` +17. `frontend-modern/src/stores/aiAvailability.ts` +18. `docs/AI.md` +19. `pkg/aicontracts/investigation.go` +20. `pkg/aicontracts/orchestrator_deps.go` +21. `pkg/aicontracts/fix_execution.go` +22. `pkg/extensions/ai_autofix.go` +23. `internal/ai/qualification/` +24. `cmd/patrol-qualify/` +25. `tests/qualification/patrol/` +26. `docs/AI_PATROL_QUALIFICATION.md` ## Shared Boundaries @@ -1388,14 +1379,14 @@ a measured cost reduction or installed-deployment cache-read rate. reintroduce proof-first or activation-loop copy into Assistant, Patrol, or external-agent onboarding surfaces. 4. `frontend-modern/src/api/ai.ts` shared with `api-contracts`: the AI frontend client is both an AI runtime control surface and a canonical API payload contract boundary. -5. `frontend-modern/src/api/aiChat.ts` shared with `api-contracts`: the Assistant chat frontend client is both the first-party Assistant transport surface and a canonical API payload contract boundary. -6. `frontend-modern/src/api/generated/agentCapabilities.ts` shared with `api-contracts`: the generated agent capabilities frontend types are both the Pulse Intelligence manifest TypeScript projection and a canonical API payload contract boundary. -7. `frontend-modern/src/api/patrol.ts` shared with `api-contracts`: the Patrol frontend client is both an AI runtime control surface and a canonical API payload contract boundary. +5. `frontend-modern/src/api/generated/agentCapabilities.ts` shared with `api-contracts`: the generated agent capabilities frontend types are both the Pulse Intelligence manifest TypeScript projection and a canonical API payload contract boundary. +6. `frontend-modern/src/api/patrol.ts` shared with `api-contracts`: the Patrol frontend client is both an AI runtime control surface and a canonical API payload contract boundary. +7. `frontend-modern/src/api/workflowPrompts.ts` shared with `api-contracts`: the workflow prompt activity client is both the AI runtime workflow-prompt telemetry surface and a canonical API payload contract boundary. 8. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` shared with `api-contracts`, `frontend-primitives`: the External agents settings panel is the optional settings-shell projection of Pulse MCP onboarding, the AI runtime connected-agent onboarding surface, and a presentation consumer of the shared agent capabilities frontend client. Its default copy must frame connected clients as optional connector access to Pulse context and Patrol work, with Patrol as the operator that watches, acts within Patrol mode, asks for approval when required, verifies outcomes, - and records history. The normal Assistant settings view must keep setup + and records history. The External agents settings view must keep setup mechanics behind a `Show connector setup` disclosure, with direct setup links opening that disclosure automatically. Default copy must make clear that connected tools do not get separate powers. Tool-contract posture and manifest facts are @@ -1406,13 +1397,13 @@ a measured cost reduction or installed-deployment cache-read rate. manifest details using user-facing labels such as Agent starting points and Agent capabilities. Direct links to - `/settings/pulse-intelligence/assistant#external-agent-setup` must focus this - External agents panel after the Assistant settings layout settles, + `/settings/pulse-intelligence/agents#external-agent-setup` must focus this + External agents panel after the settings layout settles, because connected agents are optional access to Patrol work and should not strand users at the generic token inventory. Legacy `/settings/security/api#external-agent-setup` and `/settings/security/api#pulse-mcp-setup` links must remain accepted and - redirect to the canonical Pulse Intelligence Assistant route. + redirect to the canonical Pulse Intelligence External agents route. The `Choose Patrol mode` handoff must target the Patrol operator surface at `/patrol#patrol-control`, where the mode is configured, while API Access remains the token and external-client setup surface. The expanded @@ -1421,7 +1412,7 @@ a measured cost reduction or installed-deployment cache-read rate. The visible full-surface token preset for that setup is `Patrol external agent`; `pulse_intelligence_agent` remains only the compatibility preset id used by the route and token model. -9. `frontend-modern/src/stores/aiChat.ts` shared with `frontend-primitives`: the assistant drawer and session store is both an AI runtime control surface and a canonical app-shell presentation boundary. +9. `frontend-modern/src/stores/aiAvailability.ts` shared with `frontend-primitives`: the AI availability store is both the AI runtime session capability and the app-shell gate for Patrol entry points. 10. `internal/agentcapabilities/action_target.go` shared with `api-contracts`: the Pulse Intelligence governed action target type and resource-to-action-target mapping vocabulary are both the Assistant approval/runtime routing contract and the canonical API/agent target contract for governed actions. 11. `internal/agentcapabilities/control_level.go` shared with `api-contracts`: the Pulse Intelligence control-level vocabulary and control-tool availability predicate are both the Assistant runtime gating contract and the canonical API/agent permission posture for governed action tools. 12. `internal/agentcapabilities/errors.go` shared with `api-contracts`: the Pulse Intelligence agent error envelope is both the canonical API failure payload contract and the AI runtime adapter error-parsing contract for Assistant and external-agent surfaces. @@ -1594,12 +1585,11 @@ decisions must still continue through execution and verification before the verified-outcome stage is complete. Runtime Assistant availability remains an authenticated Assistant concern, served by `GET /api/ai/assistant/surface-tools`, not a static fact in the -public agent-capabilities manifest. When the Pulse Assistant shell displays -live capability availability, it must load that authenticated endpoint through -`AIChatAPI.getAssistantSurfaceTools()` and format the posture through -`frontend-modern/src/api/agentCapabilities.ts`; the shell must not keep a local -registry-tool catalogue or reuse MCP manifest capability names as native -Assistant inventory. The External agents settings panel may show the +public agent-capabilities manifest. A client that displays live capability +availability must load that authenticated endpoint, and a browser client formats +the posture through `frontend-modern/src/api/agentCapabilities.ts`; no client +may keep a local registry-tool catalogue or reuse MCP manifest capability names +as native Assistant inventory. The External agents settings panel may show the external Pulse MCP tool posture, but it must get the normalized static `SurfaceToolContract` from `manifest.surfaceToolContracts` through `getAgentManifestSurfaceToolContract(manifest, AGENT_SURFACE_ID_PULSE_MCP)` @@ -1629,13 +1619,6 @@ context, and findings before treating an outcome as verified. Successful render that starter is activation evidence for the Patrol issue-handling journey, not proof of contextual collaboration, governed action execution, verification, or finding resolution. -Native Assistant callers may request a preferred workflow prompt through safe -browser context, but the drawer must resolve that request against the -manifest-owned workflow prompt catalogue and render it through -`/api/ai/workflow-prompts/render`; it must not inline prompt templates or -silently append to an existing composer draft. Preferred workflow prompt -requests are one-shot browser seeds and must be cleared with the scoped handoff -payload after the first successful send. The MCP `tools/call` execution helper must accept the manifest and surface id, apply the same affordance and surface-tool @@ -1850,14 +1833,11 @@ than owning string literals in `internal/ai/tools`. Shared Pulse Intelligence workflow prompt definitions, manifest-owned `workflowPrompts` catalogue selection, prompt display labels, MCP prompt title projection, presentation kind hints, prompt argument validation, and prompt -rendering rules live in the neutral workflow-prompt core so native Assistant -starters, frontend manifest consumers, and external MCP prompts cannot drift. +rendering rules live in the neutral workflow-prompt core so frontend manifest +consumers and external MCP prompts cannot drift. When `workflowPrompts` is present on a manifest it is authoritative; capability projection is compatibility plumbing for older manifests, not a second prompt -catalogue. The native Assistant starter row must therefore derive starter -availability from `workflowPrompts` and the attached Assistant context, then call -the AI/API-owned render route before inserting any prompt text into the composer; -it must not copy prompt labels, prompt bodies, or MCP `prompts/get` rendering +catalogue. Frontend consumers must not copy prompt labels, prompt bodies, or MCP `prompts/get` rendering logic into frontend state. MCP prompt list/get helpers must likewise accept the manifest-owned workflow prompt catalogue through `ManifestPulseWorkflowPrompts`, `ProjectMCPWorkflowPrompts`, `GetMCPPromptFromManifestSurface`, @@ -2303,55 +2283,6 @@ handoff contract inherits the same `agentcapabilities` input-map, thought-signature, and provider-result normalization as Assistant and MCP — `TestOrchestratorAndChatAdaptersMapTheirMessageContracts` enforces it. -Assistant frontend presentation changes under -`frontend-modern/src/components/AI/Chat/` must keep live tool activity aligned -with OpenCode's mutable tool-part pattern without losing Pulse's operator audit -trail. The current OpenCode reference at fetched `origin/dev` commit -`233427f08eb8b2a0627123816e807123fbbe5b26` keeps tool input/progress/result as -a single mutable part in `packages/tui/src/context/sync-v2.tsx` lines 276-345, -updates running tool metadata from the execution context in -`packages/opencode/src/session/tools.ts` lines 53-66, and suppresses completed -successful tool detail when `showDetails` is off in -`packages/tui/src/routes/session/index.tsx` lines 1704-1779. Pulse adapts that -by keeping successful completed tool rows command/status visible, compacting -them while the same Assistant turn is still streaming and newer concrete -activity has arrived, and keeping successful raw output behind the details -disclosure by default once the turn completes. Pending tools, skipped/canceled -tools, failed tools, and completed-turn tool details must remain -visible/inspectable; failed output may render inline because it is actionable. -AI Chat icon-only actions own Assistant/session semantics, labels, and -handlers, but their visible button chrome must compose the shared -`ActionIconButton` primitive from `frontend-modern/src/components/shared/Button.tsx`. -Header utilities, session row controls, transcript fallback close/download -controls, activity-dock queued follow-up actions, composer send, footer -help/route actions, and compact dismiss controls must not recreate local -h-5/h-6/h-7/h-8/h-9 icon-button class shells. Message and tool transcript copy -controls own Assistant copy targets and copied-state timing, but their visible -copied-state icon chrome must compose `CopyValueButton` from the same shared -button primitive boundary rather than message- or tool-local copy/check shells. - -Assistant live workflow status follows OpenCode's current-state timeline -model. CANONICAL (supersedes the per-row transcript-status rules elsewhere in -this contract): live workflow status is FOOTER-OWNED and must never be narrated -into the scrolling transcript. OpenCode keeps "the model is doing X" in a single -pinned footer line and puts only durable artifacts (user text, reasoning, tool -calls, the answer) in the timeline; Pulse mirrors that by rendering the latest -replacing workflow status ONLY in the activity dock above the composer -(`currentStatus` / `assistant-activity-dock` in -`frontend-modern/src/components/AI/Chat/index.tsx`) and never as a transcript -row or message-header chip — `shouldRenderWorkflowStatusEvent` and -`shouldShowHeaderWorkflowStatus` in -`frontend-modern/src/components/AI/Chat/MessageItem.tsx` are hard-`false`. The -dock must stay up for the whole turn: it is gated on the streaming assistant -message (`assistantTurnActive`), not `chat.isLoading()`, because loading flips -false at visible-turn-complete and would make the footer flash and vanish. -History may persist for audit/session state, but the dock renders the latest -replacing status immediately. Pulse must not replay stale status history through -artificial timers when the stream already has a newer workflow/tool state. -Frontend display code may refresh elapsed-time suffixes while a turn is active, -but status selection must read the current workflow status directly rather than -deriving an older display status from `workflowStatusHistory`. - 1. Add or change chat runtime, Patrol orchestration, findings generation, or remediation behavior through `internal/ai/` Patrol deterministic disk-health evidence changes in `internal/ai/patrol_signals.go` and `internal/ai/patrol_ai.go` must read @@ -2426,7 +2357,12 @@ deriving an older display status from `workflowStatusHistory`. ("a withheld or placeholder label") and must instruct the model not to repeat a withheld placeholder back to the user as the resource identity; the `current_resource` handle remains the authoritative target. -3. Add or change Pulse Assistant request flow through `internal/api/ai_handler.go`, `frontend-modern/src/api/ai.ts`, and `frontend-modern/src/api/aiChat.ts` +3. Add or change Assistant chat request flow through `internal/api/ai_handler.go` and `internal/ai/chat/` + The browser Assistant chat window was retired on 2026-10-22. This request + flow still runs Patrol investigations and serves Pulse Mobile chat, so the + session, stream, and provider rules below stay in force. The drawer, + composer, slash-command, transcript-rendering, and browser dev-fixture + rules that used to live in this item left with the window. Assistant session compaction is a runtime-backed session workflow, not a local waiting message, transcript-only UI action, or stubbed summarize endpoint. The referenced OpenCode source at fetched `origin/dev` commit @@ -2442,10 +2378,6 @@ deriving an older display status from `workflowStatusHistory`. visible assistant summary row plus the latest bounded turns, clear redo state, and never stream or persist raw provider tool-call artifacts, hidden reasoning, secrets, or raw provider responses as the compacted transcript. - The drawer header action and `/compact` slash command must call the same - endpoint, surface compacting progress, reload the compacted session and - session list, keep focus in the composer after completion, and treat - `not_needed` results as quiet operator feedback instead of an error. OpenRouter-routed Assistant chat requests must set a bounded default completion budget when the runtime request does not specify `MaxTokens`. OpenRouter preflights affordability against the requested maximum completion @@ -2467,404 +2399,35 @@ deriving an older display status from `workflowStatusHistory`. provider JSON bodies, request URLs, request methods, dashboard links, and key-management links must not be streamed or persisted as chat-visible assistant output. - Assistant provider readiness is part of that same request flow: when the - drawer opens or the selected chat model changes, the frontend must start a - background verification of the selected provider/model through - `/api/ai/test/{provider}`. That check must use neutral provider diagnostic - copy owned by `internal/ai/`, not Patrol runtime-finding wording, and the - drawer may surface the result as actionable retry/settings status plus - same-model configured-provider alternatives without converting it into - assistant-authored output. Provider checking is a background diagnostic and - must not delay the user's first useful chat turn; the drawer should keep - routine selected-route health visible in compact composer chrome while - reserving the larger provider-status banner for requested checks and - actionable failures. A confirmed selected-route provider error must keep - typed text and focus without blocking normal chat dispatch, but the send path - must still use the currently selected route until the operator explicitly - chooses a different route. Same-model configured-provider alternatives remain - visible one-click route changes and failed-turn retry choices, not automatic - send-time recovery or hidden provider fallback. The OpenCode reference - behavior for this slice is retries on the selected provider/model, not - silent fallback to another provider or route; Pulse must adapt that as - explicit route choice plus visible route search/recovery. Frontend stream - reducers must ignore retired `provider_fallback` workflow phases entirely - instead of rendering compatibility "fallback rejected" rows, because the - selected-route retry/error contract is the only live recovery path until an - operator explicitly chooses another route. The Assistant - `/model` command therefore has two owned paths: an exact `provider:model-id` - argument selects that route directly, and an OpenCode-style - `provider/model-id` argument is accepted only when the provider prefix is a - known Pulse provider before being normalized back to canonical - `provider:model-id` storage. Partial or unknown slash text opens the shared - model picker with the search text prefilled instead of becoming a hidden - route guess. - Assistant model-selection defaults are settings-owned: the drawer may persist - explicit model selections only for concrete session IDs, while blank-session - chat defaults must flow from `/api/settings/ai` `chat_model` or `model`. - Browser storage must not keep a hidden `__default__` model route that - overrides the configured chat model after the operator changes provider - settings; stale blank-session defaults must be ignored and cleaned on mount - so routes such as OpenRouter become visible and effective immediately. - Assistant session model continuity is transcript-owned, not ambient UI state. - `internal/ai/chat` must persist the effective selected model route on the - user turn as well as final assistant turns, and the browser chat runtime must - restore the latest explicit `provider:model` route from loaded session - messages before publishing the loaded session ID to drawer/session storage. - During an active Assistant turn, the drawer must keep the effective model - route visible next to the live turn status using the route recorded on the - streaming assistant message first and the selected route only as a fallback; - live progress text remains the activity description, while route identity is - separate compact chrome so provider/model state stays inspectable without - polluting status copy. Provider-start copy must stay route-neutral - ("Waiting for assistant.") because selected-route identity belongs in the - route chrome and stream metadata, not in transient activity text; retry copy - must say Pulse is retrying the selected route, not imply automatic provider - or model fallback. - Legacy bare model names, provider response IDs without a Pulse route prefix, - URLs, and malformed route strings must be ignored so loading older sessions - cannot corrupt the active selector. The referenced OpenCode source at fetched - `origin/dev` commit `2006259a02a87edf9e37f253cbddf3188309026b` restores - local session model state from the latest user message in - `packages/app/src/pages/session.tsx` lines 361-368 through - `packages/app/src/pages/session/session-model-helpers.ts`; Pulse adapts that - pattern by using its persisted explicit route contract rather than terminal - provider/model objects. - Follow-up sends during an active Assistant response are chat-runtime queue - state that steers the running response by default. The drawer must accept - and echo the user's follow-up as a queued user turn without aborting or - replacing the active model stream, must show an itemized composer-adjacent - queue with per-follow-up edit/remove controls plus clear-all, and must - offer each follow-up to the running loop through - `POST /api/ai/sessions/{id}/steer` (mid-turn steering). A steering-accepted - follow-up joins the loop at its next turn boundary (the abort-check site in - `agentic.go executeWithTools`) as a plain user message, never mid-provider- - stream and never mid-tool-batch; the loop announces the injection with a - `steer_applied` stream event carrying the client row id so the drawer - settles the pending row into a delivered user turn, splices the message - into `resultMessages` at its true position, and persists it through the - end-of-run save (whose skip-user-messages rule exempts `Steered` messages). - Steering carries prompt text only: it cannot change the model route, - control level, or autonomous mode of the running turn, does not extend the - turn budget or reset wrap-up brakes, and is rejected for system sessions. - Once accepted for steering, a follow-up row loses its edit/remove - affordances (the text is in the loop's hands). Delivery is not guaranteed: - a run that finishes before a boundary discards unconsumed steers without - persisting them, and the drawer, which keeps the row queued until - `steer_applied` arrives, drains it in order after the active stream - becomes idle exactly as before — the pre-steering queue semantics remain - the fallback path, and paused queues do not steer. Queued follow-ups must - snapshot the effective - model route at enqueue time so a later model/provider switch cannot silently - reroute an already-queued user turn, and both the transcript queued-user row - and composer-adjacent queue row must surface that snapshotted route label when - it exists; explicit failed-turn recovery actions labelled from a configured - provider route must pass that selected route as an override instead of - relying on ambient selector state. The referenced - OpenCode source at fetched `origin/dev` commit - `1399323b78a04229d9bfe00c7436d7f41770fda8` keeps current, recent, and - selected models as structured `{ providerID, modelID }` values in - `packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx` and - `packages/opencode/src/provider/provider.ts`, so Pulse must preserve the - equivalent route identity for queued and retried turns. Queue-drain UX - verification must have a deterministic local fixture path so agents can - test queued follow-up echo, hold, drain, and tool-row replacement without - opening a real provider stream. The referenced OpenCode source at fetched - `origin/dev` commit `31c099be435d59bd6749ace7a9f2bb2245e6d3fa` emits - local demo tool events through - `packages/opencode/src/cli/cmd/run/demo.ts` (`startTool` and `doneTool`, - lines 451-525; demo command routing, lines 1039-1076); Pulse adapts that by - keeping `/fixture queue-hold` and `/fixture queued-follow-up` fully local - browser fixtures for queue interaction proof. The referenced OpenCode source - at fetched `origin/dev` commit - `e82542b8023a8374f29c23b70ec019c8f256354e` exposes queued prompts through - `RunQueuedPromptSelectBody` in - `packages/opencode/src/cli/cmd/run/footer.command.tsx` lines 588-650: - queued rows are searchable and keyboard-operable, with Enter/Ctrl+E editing - the selected prompt and Delete/Ctrl+D removing it. Pulse adapts that pattern - to the browser drawer by making each composer-adjacent queued follow-up row - focusable and directly operable with Enter to edit and Delete/Backspace to - remove, while retaining the visible icon buttons and avoiding terminal-only - shortcuts that collide with browser chrome. OpenCode's `DialogModel` feeds - current, recent, favorite, and provider model rows into - `DialogSelect`, while `DialogSelect` maintains a selected row and handles - up/down/page/home/end/return navigation. The fetched OpenCode - `origin/dev` commit `31c099be435d59bd6749ace7a9f2bb2245e6d3fa` keeps those - bindings in `packages/tui/src/ui/dialog-select.tsx` lines 288-378. Pulse's - browser model picker must expose the same route identity as a labelled - search/listbox surface, focus search on open, move keyboard focus from - search into the current or filtered model row, and keep every actionable - list row, including catalog-disclosure rows, navigable without requiring - mouse clicks. The 2026-06-07 model-provider slice rechecked OpenCode - `origin/dev` commit `914a643`: `packages/app/src/components/dialog-select-model.tsx` - exposes provider connection and model management actions directly inside - the model picker, while `packages/tui/src/component/dialog-model.tsx` - exposes a provider action in the same dialog. Pulse adapts that by wiring - the Assistant model chooser's provider action to the governed Assistant & - Patrol provider settings route instead of hiding provider repair behind a - separate slash command or silently switching model routes. The same - 2026-06-07 activity-visibility pass rechecked OpenCode - `packages/app/src/pages/session/message-timeline.tsx` and - `message-timeline.data.ts`: active turns are represented as session timeline - rows, with busy/retry state kept visible while assistant parts continue to - arrive. Pulse adapts that by keeping fast command/tool completions in a - transient running state long enough for the compact transcript row to paint, - while still collapsing large command output behind the existing tool-details - affordance. Stop is the explicit - interruption path: it must abort the active stream, clear queued follow-ups - and pending tool/approval/question affordances, preserve any partial model - text, return focus to the composer, and render a neutral transcript marker - rather than persisting synthetic assistant answer text or surfacing the - interruption as a retryable provider failure. - The referenced OpenCode source at commit - `9ed17da55ab1f7360cc0e01075f763e27fa899e9` routes prompt interruption - through the prompt command in - `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx` - (`session.interrupt`, lines 379-399) and handles stream interruption in - `packages/opencode/src/session/processor.ts` (`Effect.onInterrupt`, lines - 977-983; aborted tool state finalization, lines 888-915). Pulse adapts that - terminal active-part behavior by clearing unresolved pending tool, - approval, and question transcript rows on explicit Stop while preserving - already-streamed answer text and completed tool evidence before showing the - neutral stopped marker. - The same terminal cleanup applies to non-interrupt terminal paths. When a - stream reaches `done`, emits a structured `error`, or rejects before an SSE - terminal event, the drawer must not leave pending tool, approval, or - question controls live in the transcript. The referenced OpenCode source at - commit `9ed17da55ab1f7360cc0e01075f763e27fa899e9` mutates active tool - parts into terminal `success` or `failed` rows in - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx` - (`session.next.tool.success`, lines 328-348; - `session.next.tool.failed`, lines 350-371), and the processor marks - interrupted active tools terminal before completing the assistant message in - `packages/opencode/src/session/processor.ts` (lines 888-917). Pulse adapts - that by retaining completed tool rows and assistant text while removing - unresolved interactive rows from terminal browser transcript state. - Completed tool rows must preserve timing once a pending row resolves. The - referenced OpenCode source at fetched `origin/dev` commit - `effd27b23900720a53e965396ff1a105c1f7e9c8` carries tool identity and - state through `toolCommit` / `startTool` / `doneTool` in - `packages/opencode/src/cli/cmd/run/session-data.ts` (lines 622-733) and - formats elapsed tool state with `span` in - `packages/opencode/src/cli/cmd/run/tool.ts` (lines 191-200). Pulse adapts - that by carrying the stream event start/end timestamps into the completed - browser tool row, so the visible activity timeline does not collapse when a - running command is replaced by its completed result. When a live browser - stream receives a `tool_start` and successful `tool_end` in the same - buffered burst, the completed row may briefly present as running before it - settles to completed, matching OpenCode's perceptible pending-tool motion - at fetched `origin/dev` commit - `1025540fcc2a69609a0131a7168300205656d728` in - `packages/ui/src/components/basic-tool.tsx` (`pending`, lines 90 and - 195-205) and `packages/ui/src/components/tool-status-title.tsx` while - keeping Pulse's typed completed tool row, persisted status, answer stream, - and failure state authoritative. That settle interval must be bounded, - live-only, success-only, and must not delay assistant content, terminal - cleanup, or failed-tool visibility. Completed browser tool - rows must also preserve output trust when a raw preview is intentionally - suppressed: the referenced OpenCode source at fetched `origin/dev` commit - `1025540fcc2a69609a0131a7168300205656d728` keeps output visibility as an - explicit tool-view policy in `packages/opencode/src/cli/cmd/run/tool.ts` - (`ToolView.output`, lines 40-44; tool display hooks, lines 117-123), and - commits allowed tool output into scrollback in - `packages/opencode/src/cli/cmd/run/session-data.ts` (lines 960-964). Pulse - completed rows must expose safe bounded plain-text output previews when they - can be rendered without turning the transcript into a raw terminal dump, - while keeping the full raw output behind the existing details disclosure. - Structured, binary, unavailable, or otherwise noisy output may collapse to - compact output metadata. Raw input/output details must remain copyable from - the disclosure without expanding the default transcript. - Completed Assistant context summaries must use the same operator-facing - tool-input presentation as the visible tool rows, not raw internal tool - identifiers. A completed `pulse_read` device inspection should therefore - summarize as the inspected resource/action, while raw tool names such as - `pulse_read` or bare fallback labels such as `read` remain fallback-only - debug/detail vocabulary. - Live workflow activity is also active turn state, not disposable waiting - copy. The referenced OpenCode source at fetched `origin/dev` commit - `1025540fcc2a69609a0131a7168300205656d728` defines model, shell, step, - and tool lifecycle events in `packages/core/src/session/event.ts` (model - switched lines 62-70; shell started/ended lines 151-174; tool lifecycle - lines 340-392), projects them into durable message/part rows in - `packages/core/src/session/message-updater.ts` (step started lines - 189-210; tool called/progress/success/failed lines 269-335), and renders - model-switch plus tool rows in - `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx` - (model switch lines 120-122 and 262-274; tool rows lines 455-500). - Pulse adapts that by keeping the latest browser `workflow_status` activity - visible in the live stream event sequence across content, tool, approval, - and question boundaries instead of dropping it the instant a richer row - arrives. Burst workflow statuses may still replace one another until a - durable stream boundary appears, and terminal cleanup on done/error/Stop - may remove transient workflow-status rows while preserving typed - model-switch and tool evidence. When the selected model route is already - known at turn creation, Pulse must seed a selected-model stream event before - backend/provider activity arrives so the transcript and active-turn footer - show concrete route motion instead of a generic request-start wait. - If backend/provider startup immediately confirms a different selected route - before any durable assistant content, thinking, tool, approval, or question - evidence arrives, that confirmed route replaces the provisional local - selected-model row rather than stacking multiple "Using ..." rows. Once a - durable boundary exists, later route events are preserved chronologically. - Rendered workflow-status rows, active-turn footer text, and transcript - export status lines must normalize internal tool identifiers such as - `pulse_query`, `pulse_read`, and `pulse_exec` into operator-facing activity - labels, while preserving the raw typed event payload for Details/debug paths. - Provider retry states are a distinct live activity kind: a `provider_retry` - workflow status must render as retrying in the active-turn footer and live - workflow row so the operator sees selected-route recovery in progress rather - than another generic waiting/thinking state. - Assistant-authored visible answer prose is part of the same boundary: model - text may describe the action in operator vocabulary such as "read command" - or "inventory lookup", but raw internal identifiers such as `pulse_read` - must not appear in the normal transcript, copied answer text, or transcript - export unless the operator explicitly opens raw details/debug content. - Selected-model route rows are typed route evidence, not live activity. They - may render immediately while the backend/provider stream is still quiet, but - they must not hide or outrank workflow progress once a `request_start`, - provider, tool, thinking, approval, question, or content activity is - available. This follows the referenced OpenCode separation between session - status and model/tool transcript events at fetched `origin/dev` commit - `31c099be435d59bd6749ace7a9f2bb2245e6d3fa`: Pulse keeps the selected route - visible as durable evidence while the active-turn footer and live workflow - rows answer what the assistant is doing now. The generated frontend SSE - contract must include workflow `provider` and `model` fields so - selected-route activity is typed end to end. - The transcript viewport is part of the same live activity contract. The - referenced OpenCode source at fetched `origin/dev` commit - `1025540fcc2a69609a0131a7168300205656d728` forwards reducer commits into - terminal scrollback in - `packages/opencode/src/cli/cmd/run/stream.ts` (lines 140-145) and renders - bottom-sticky activity in - `packages/opencode/src/cli/cmd/run/footer.subagent.tsx` (lines 148-153) - with `stickyScroll={true}` and `stickyStart="bottom"`. Pulse adapts that - by tracking whether the browser transcript is pinned to the live bottom - before streamed content or tool rows grow, continuing to follow active - output while pinned, and preserving the operator's scroll position after - they intentionally scroll away from live activity. When the transcript is - unpinned while messages exist, the drawer must expose an accessible - jump-to-latest command that returns the operator to the live bottom without - requiring a manual scroll gesture. - Composer prompt history is also drawer-local chat-runtime state: the drawer - may persist a bounded local history of submitted prompt text and structured - mentions for ArrowUp/ArrowDown recall, but that history must not persist or - replay one-shot finding handoff, approval, autonomous-mode, or other scoped - send options. The referenced OpenCode source at fetched `origin/dev` commit - `ba57718b0516c7a8670d1e820b1a24146a8b8262` binds prompt history navigation - in `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx` to the - input cursor boundary: previous history is available at the start of the - current draft, and next history restores the saved draft at the end. Pulse's - Assistant drawer follows that draft-safe recall model rather than limiting - history recall to an empty composer. Once a history entry is loaded, the - same boundary ownership remains direction-specific: ArrowUp may move to an - older prompt only from the start edge, and ArrowDown may move newer or - restore the saved draft only from the end edge, so normal textarea movement - is not stolen at the opposite boundary. Scoped context replay remains owned - by explicit session handoff metadata or queued follow-up edit state. - Unsent composer drafts are recoverable remount state, not prompt history: - closing or remounting the Assistant drawer must restore the current prompt, - structured mentions, cursor position, and any queued-follow-up edit metadata - without writing the draft into submitted prompt history. The referenced - OpenCode source at commit `9ed17da55ab1f7360cc0e01075f763e27fa899e9` - keeps a module-local `stashed` prompt in - `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx`, restores it - in `Prompt` `onMount` (lines 595-604), and captures it in `onCleanup` - (lines 607-610). Pulse adapts that source behavior with a drawer-local - composer stash so normal close/reopen does not drop an unsent user thought - or downgrade a scoped queued follow-up into a plain prompt. - Composer submission must read the live input buffer at submit time and - suppress only overlapping dispatches from the same browser interaction. The - referenced OpenCode source at commit - `9ed17da55ab1f7360cc0e01075f763e27fa899e9` - `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx` guards - `Prompt.submit()` with local `submitting` state (lines 910-924) and syncs - the live input `plainText` into prompt state before downstream reads - (lines 930-936) so double-dispatch and IME/composition races do not send a - stale or phantom prompt. Pulse adapts that by reading the live textarea - value before trimming and by using a one-dispatch composer guard that still - releases immediately for intentional queued follow-ups during an active - turn. - Assistant drawer controls are part of that same prompt command surface. The - referenced OpenCode prompt source exposes command-named actions such as - prompt submit, session interrupt, prompt history movement, and prompt stash - operations through explicit command titles before those actions reach the - terminal UI. Pulse's browser drawer must therefore expose command-specific - accessible names and selected/expanded state for always-visible chat actions - such as New session, session history, collapse/close, autonomous-warning - recovery, and the control-mode selector instead of depending on title-only - icon controls or ambiguous short labels. - The slash command autocomplete is a command-discovery surface, not just a - text shortcut hint. The referenced OpenCode source at fetched `origin/dev` - commit `914a643` advertises session - commands such as `/models`, `/new`, `/sessions`, `/compact`, `/export`, and - `/connect` in `packages/tui/src/feature-plugins/home/tips-view.tsx` lines - 179-192, while each command registration carries a command title, category, - and slash metadata in `packages/tui/src/routes/session/index.tsx` (for - `/compact`, lines 548-570). Pulse adapts that by showing the full local - Assistant command set in the scrollable slash list instead of silently - capping discovery to the first few commands, and every listed command row - must carry a matching icon so `/compact`, `/undo`, `/redo`, and other - lower-frequency actions do not appear as unfinished blank rows. OpenCode's - current app prompt popover keeps unmatched slash searches inside the prompt - surface and renders an empty-command state in - `packages/app/src/components/prompt-input/slash-popover.tsx` (fetched - `origin/dev` commit `914a643`, lines 96-100); Pulse adapts that by keeping - genuinely unmatched `/...` drafts open with a clear empty state while still - hiding disabled-only command matches so manual Enter can explain why the local - command is unavailable. The slash list must avoid persistent visible keyboard - shortcut instruction footers; owned key handling is proven by behavior and - accessible command state instead of extra instructional chrome. - Slash autocomplete close semantics are owned by that same prompt command - surface. The referenced OpenCode source at fetched `origin/dev` commit - `4519a1da329c1a4fc384054e7203ba7d06928205` clears a transient slash - command query when `hide()` closes prompt autocomplete in - `packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx` - (lines 668-678), so Pulse's browser slash command popup must close Escape - and outside-click locally, clear only slash-only command drafts, return focus - to the composer, and leave ordinary prompts or already-submitted local - command actions untouched. Closing the popup must not leave `/mo`, `/new`, or - another executable command token behind for the next Enter press. - Prompt-owned popups must consume their local keyboard commands before global - drawer handlers can reinterpret them. The same OpenCode autocomplete source - wraps previous/next selection at list boundaries in - `packages/tui/src/component/prompt/autocomplete.tsx` lines 552-559 at fetched - `origin/dev` commit `31c099be435d59bd6749ace7a9f2bb2245e6d3fa`, and - registers prompt-scoped previous/next/hide/select/complete commands in - `packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx` - (lines 600-658), and OpenCode's help dialog binds Escape as a dialog-local - close command in - `packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx` (lines 11-16). - Pulse adapts that by having slash autocomplete, resource mention - autocomplete, and Assistant command help prevent default handling and stop - later document-level keyboard handlers for their owned navigation, - selection, and close keys. Slash and resource mention autocomplete must wrap - up/down movement across the visible options, and resource mentions must expose - the same selected-row state as a labelled listbox rather than only a visual - hover fill. Escape inside those local surfaces must close the local - popup/dialog only; it must not also close the Assistant drawer. - Failed-turn retry is part of that same local chat-runtime boundary: a - retryable in-memory assistant error may replay the original user turn's - structured mentions, finding id, model route, handoff context, handoff - resources, handoff actions, and handoff metadata (execution mode is not - part of that request; `/api/ai/chat` sets it), but must not reconstruct scoped - context from prompt history or saved transcript prose. Provider/model route - recovery is explicit user-visible recovery, not hidden chat execution. A - failed turn may expose route-and-retry actions through the existing drawer - model selector so operators can move from a blocked direct provider route to a - configured gateway or alternate model without losing the draft or creating a - parallel picker, but `/api/ai/chat` must not automatically switch provider or - model routes inside a single Assistant turn after the selected route fails. - That prohibition includes same-model gateway equivalents and configured - provider defaults: those routes may be offered as explicit recovery actions, - but the selected turn owns its selected route until it completes or fails. - The referenced OpenCode source at fetched `dev` commit - `e82542b8023a8374f29c23b70ec019c8f256354e` models this boundary by keeping - same-route retry visible in `packages/opencode/src/session/retry.ts`, - publishing retry status from `packages/opencode/src/session/processor.ts`, - rendering retry state in `packages/ui/src/components/session-retry.tsx`, and - treating provider/model changes as explicit session events in - `packages/core/src/session/event.ts` and - `packages/opencode/src/session/prompt.ts`. Pulse adapts that behavior for the - drawer instead of implementing automatic cross-provider route switching. + Provider readiness checks through `/api/ai/test/{provider}` must use + neutral provider diagnostic copy owned by `internal/ai/`, not Patrol + runtime-finding wording. Chat turns retry on the selected provider/model + route: `/api/ai/chat` must not switch provider or model routes inside a + single Assistant turn after the selected route fails, and that prohibition + includes same-model gateway equivalents and configured provider defaults. + Those routes may be offered as explicit recovery actions, but the selected + turn owns its selected route until it completes or fails. + Assistant session model continuity is transcript-owned, not ambient client + state. `internal/ai/chat` must persist the effective selected model route on + the user turn as well as final assistant turns, so a client that reloads a + session can restore the latest explicit `provider:model` route from the + loaded messages. Assistant completion events must carry the effective model + route that actually completed the selected turn, and provider retry copy + must say Pulse is retrying the selected route rather than imply automatic + provider or model fallback. + Follow-up steering runs through `POST /api/ai/sessions/{id}/steer`. A + steering-accepted follow-up joins the loop at its next turn boundary (the + abort-check site in `agentic.go executeWithTools`) as a plain user message, + never mid-provider-stream and never mid-tool-batch; the loop announces the + injection with a `steer_applied` stream event carrying the client row id, + splices the message into `resultMessages` at its true position, and + persists it through the end-of-run save (whose skip-user-messages rule + exempts `Steered` messages). Steering carries prompt text only: it cannot + change the model route, control level, or autonomous mode of the running + turn, does not extend the turn budget or reset wrap-up brakes, and is + rejected for system sessions. Delivery is not guaranteed: a run that + finishes before a boundary discards unconsumed steers without persisting + them. Primary interactive chat model resolution must use the explicit configured chat route or, when no explicit chat route exists, a stable provider default without calling provider model catalogs before the selected stream starts. If @@ -2881,34 +2444,17 @@ deriving an older display status from `workflowStatusHistory`. `provider_fallback`, `failed_provider`, `failed_model`, `next_provider`, or `next_model` workflow metadata. Once visible output has streamed, Pulse must also keep the failure on that visible attempt rather than silently changing - route mid-turn. Frontend reducers must discard stale or nonconforming - `provider_fallback` workflow metadata without rendering a rejected-route - status, switching routes, or inferring a model switch from `next_model`. - Provider retry - workflow states that include `retry_after_ms` - must render as live countdowns derived from the workflow `started_at` - timestamp while the turn is active, matching OpenCode's visible retry wait - behavior instead of freezing the first retry label. - Assistant completion events must carry the effective model route that actually - completed the selected turn. The drawer must render the initial - `provider_start` workflow state as a selected-model row (`Using ...`) before - visible content or tool activity when the event arrives. Later model-route - changes may still be rendered as typed `model_switch` transcript rows when - they come from explicit route recovery or restored historical data, but the UI - must label them neutrally as selected or switched routes rather than as - automatic provider fallback, and completing a streamed route-switch row must - not promote that route into the active model selection without an explicit - user action. + route mid-turn. Interactive Assistant streams must establish the session ID and emit the `session` event once as soon as the HTTP SSE writer is ready, before finding handoff recovery, model resolution, selected-provider startup, handoff/context prefetch, recent-session injection, inventory summary reads, tool scoping, or provider startup. The chat service then persists/ensures - that same session ID while suppressing duplicate session events. This keeps - the drawer's visible turn anchored while backend preparation continues and + that same session ID while suppressing duplicate session events. This keeps the + client's visible turn anchored while backend preparation continues and prevents simple prompts from appearing stuck in an opaque thinking state. Stored finding or handoff recovery is eligible only for a session ID supplied - by the browser as an existing conversation. A session ID generated at the + by the client as an existing conversation. A session ID generated at the HTTP stream boundary for a new turn must not trigger persisted handoff lookups or legacy session discovery before the service creates the session, because that turns a new prompt into a large-history storage scan before @@ -2919,29 +2465,13 @@ deriving an older display status from `workflowStatusHistory`. turns, and selected-provider startup. These events are runtime progress, not Assistant-authored analysis, and they must not become keyword routers, explore pre-passes, or instructions that choose the model's next action. - Backend-emitted `workflow_state` rows that explain real provider, context, or - tool progress must remain visible while the turn is active, but they are - transient runtime state rather than completed Assistant evidence. Terminal - `done`, terminal `error`, and explicit Stop must clear workflow-status rows - together with local prompt placeholders and unresolved interactive rows such - as approvals, questions, and pending tools, while preserving durable - model-route, content, reasoning, completed-tool, canceled-tool, and error - evidence. The referenced OpenCode - source at fetched `origin/dev` commit - `914a643ab26053df463ef78a1deadcdb223e8783` writes step, tool, and text parts - as soon as they arrive in `packages/opencode/src/session/processor.ts` lines - 151-162, 322-339, 443-466, and 787-806, then renders completed text parts - from stored part state in `packages/ui/src/components/message-part.tsx` lines - 1545-1551; Pulse adapts that part-style visibility by keeping live workflow - activity visible without storing stale provider-start or waiting labels as - completed transcript evidence. Selected-provider startup copy must describe the provider route as actively starting the response (for example, ` is starting the response.`) rather than as a passive wait label, while preserving the same `provider_start` phase and concrete model-route payload. The HTTP chat stream handler also owns visible idle progress for silent intervals after the stream has opened: if no client-visible Assistant event - reaches the browser for the governed idle interval while execution is still + reaches the client for the governed idle interval while execution is still request-bound, the handler must emit a neutral `workflow_state` with phase `stream_idle` rather than relying only on hidden SSE comment heartbeats. Legacy Assistant SSE routes that still use the older execute event envelope, @@ -2953,220 +2483,6 @@ deriving an older display status from `workflowStatusHistory`. transport liveness signal only; it must replace the active workflow status, stop before terminal `done`/`error`, and must not become persisted assistant prose or raw provider reasoning. - The drawer transcript owns detailed in-flight progress for the active turn: - a new assistant row must start with a neutral local preparation status until - stream progress arrives, must show the current effective model route while - the turn is still streaming, and must keep late workflow progress visible - through the composer footer while transcript rows continue to render typed - Assistant evidence. Workflow status is live - progress, not answer content or a delayed walkthrough; each new - `workflow_state` replaces the canonical active status immediately. The - browser may retain a bounded in-flight history of those replaced labels for - state continuity. When several statuses arrive in one backend/network burst, - the live transcript row and active-turn composer footer must use the same - paced presentation helper so the operator sees the row move through - preparation, context, provider, and tool states instead of jumping to the - final burst label. Single fresh status updates still render immediately, and - separated transcript workflow rows stay tied to their own streamed event - when a durable content/tool/approval/question boundary exists. Older neutral - statuses may disappear when replaced by newer status, tool, reasoning, or - answer evidence; that replacement is the intended motion signal. - That history must stay live-only: once visible assistant text, tool progress, - approvals, questions, terminal `done`, terminal `error`, or explicit Stop - take over, stale workflow text and the presentation history must clear so - the row does not keep saying it is waiting on a phase that has already been - superseded. Live workflow and pending-tool activity must retain a per-state - start timestamp so the drawer can show elapsed wait/run time for long - provider starts and tool calls instead of repeating a timeless waiting label. - Completed plain-text tool output must keep a bounded transcript preview - visible by default, with full input/output available through expansion, so - the operator sees evidence of what completed without receiving a raw output - dump. Structured, binary, unavailable, or otherwise noisy output may stay - collapsed behind details, but a successful text command must not degrade to - an opaque `output available` badge when a safe preview can be rendered. - If the UI enters a loading turn before the assistant shell or first - `workflow_state` exists, the active-turn footer must show the active startup - status `Sending prompt.` and derive elapsed time from the submitted, non-queued - user turn or assistant shell timestamp. Queued follow-up messages must not - reset that active-turn startup clock. The same active-turn footer owns the - Stop affordance while a response is running, so interruption remains attached - to the live status row instead of hiding only in the composer action cluster. - The referenced OpenCode source at fetched `origin/dev` commit - `0875203a6c726d7a37b5ffbb770cc433c98e7cd6` mutates typed message parts as - events arrive in `packages/opencode/src/cli/cmd/tui/context/sync.tsx` - (`message.part.updated` and `message.part.delta`) and represents session - activity as separately updated status in - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx` - (`session.next.step.started`, `session.next.text.delta`, and tool state - events). Pulse adapts that event-by-event activity model with browser-safe - `workflow_state` and typed tool rows, including replacing `stream_idle` - status while no provider event has arrived, rather than exposing terminal - UI internals or chain-of-thought. - The referenced OpenCode source at fetched `origin/dev` commit - `1399323b78a04229d9bfe00c7436d7f41770fda8` applies each typed event to the - active assistant message in - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx`; Pulse adapts that - precedence by letting typed content, model-switch, tool, approval, and - question evidence own the visible row once it exists, while later neutral - workflow states such as provider reasoning still replace the live footer - heartbeat instead of being hidden behind generic "generating" copy. - The referenced OpenCode source at commit - `9ed17da55ab1f7360cc0e01075f763e27fa899e9` renders active assistant work - as session-owned parts in - `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` - (`Session`, line 185; `ToolPart` mapping, lines 1788-1796; - `InlineToolRow`, line 1926) and mutates the matching active tool/text state - in `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx` - (`apply(event)`, line 120; `session.next.tool.*`, lines 276-372); - Pulse's browser drawer adapts that model by keeping the transcript rows - typed and stable while the active-turn footer remains a replacing live - status slot for provider and workflow progress between visible parts. - The same OpenCode commit applies each streamed session event as its own - state mutation in `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx` - (`apply(event)`, line 120; `session.next.tool.input.started`, line 276; - `session.next.tool.progress`, line 317; `session.next.tool.success`, line - 328; `session.next.tool.failed`, line 350), so Pulse's shared browser SSE - consumer must treat opted-in Assistant progress events as paint checkpoints. - Pulse's active-turn status ranking must also treat terminal `tool_end` - events as fresh visible activity, adapting OpenCode's in-place - completed/error tool-part mutation by replacing stale provider-start footer - copy with completed or failed tool activity while still preferring any - separate unresolved pending tool over a terminal sibling tool. - Read-only shell-backed Assistant tools must expose the redacted `$ ...` - command preview in the active-turn status while the tool is running and when - its terminal row replaces stale workflow copy; friendly intent labels may - remain inside tool cards, but the live dock must not hide a known read-only - command behind generic "running command" text. Write/control commands remain - governed by approval and action surfaces rather than gaining a free-form live - shell shortcut. The referenced OpenCode source at fetched `origin/dev` commit - `e82542b8023a8374f29c23b70ec019c8f256354e` records shell commands as visible - commits in `packages/opencode/src/cli/cmd/run/session-data.ts` - (`startShell`, lines 690-702; `session.next.shell.started`, lines 783-795; - `session.next.shell.ended`, lines 803-821) and keeps non-shell tool status - visible through `toolStatus` on running tool parts (lines 932-942). Pulse - adapts that shell visibility by reusing its redacted read-only command - preview in the browser-safe active-turn status instead of exposing raw - outputs, secrets, or unrestricted control commands. - `frontend-modern/src/api/streaming.ts` must yield through an - animation-frame-backed browser paint checkpoint, with a bounded timer - fallback for inactive tabs, before draining the next opted-in event; a plain - synchronous loop or microtask-only pause is insufficient because it can still - render workflow/tool steps only after a batch has already finished. Chat text - and reasoning deltas must paint immediately for the first delta in a run and - then periodically so coalesced provider chunks do not arrive as one burst, - while avoiding a frame wait for every token. - Pulse's reducer must also keep the same keyed-part behavior: repeated - `tool_start` or `tool_progress` events for one backend tool ID, or the same - normalized tool name when an older server omits IDs, upsert one pending tool - row and collapse stale duplicate pending rows instead of replaying several - near-identical steps in the transcript. - A supplied invocation ID is authoritative. Distinct IDs must not merge by - tool name during start, progress, cancellation, completion or approval - cleanup. Completing one invocation must leave its sibling approvals intact. - Message rendering keys rows by message ID and reads immutable transcript - values. It must not deeply reconcile shared tool objects: removing a status - row must never mutate evidence retained in another event or in toolCalls. - `frontend-modern/src/api/aiChat.ts` owns the Assistant predicate: token - content and hidden reasoning may continue to opt out of those checkpoints so - answer streaming remains fast, while session, workflow, model-switch, tool, - approval, and question events remain user-visible progress checkpoints. - That same frontend stream boundary owns the deterministic dev/test fixture - used for fast Assistant UX iteration: `frontend-modern/src/api/aiChat.ts` - may short-circuit an explicit local fixture prompt only when - `import.meta.env.DEV` or test mode is active, and the fixture must emit the - same typed `StreamEvent` objects (`session`, `workflow_state`, `thinking`, - `tool_start`, `tool_progress`, `tool_cancel`, `tool_end`, `content`, - `done`) through the normal `useChat` reducer rather than rendering a - separate mock transcript. The local `provider-retry` fixture must exercise - the real `provider_retry` workflow state, including attempt metadata and - `retry_after_ms`, so browser proof of retry countdown behavior does not - depend on external provider availability or API spend. - The local `tool-chain` fixture must exercise consecutive tool activity with - paced `tool_start` -> `tool_progress` -> `tool_end` transitions for one - tool followed by a second live tool, so browser proof can verify completed - rows compact while the next tool row replaces the active motion. - Dev/test fixture prompts are also part of the Assistant command-discovery - surface: slash autocomplete and Assistant command help must expose an - insertable `/fixture` command in development/test builds, search it by - canonical fixture names such as `provider-retry`, and submit completed - `/fixture ` prompts through the normal chat send path so - `maybeRunAIChatDevStreamFixture` remains the single local execution boundary. - Production command surfaces must omit that dev command instead of advertising - a fixture mode that the runtime will not intercept. - The local `stream-idle` fixture must exercise the real `stream_idle` - workflow state after selected-provider startup so browser proof of visible - idle liveness does not depend on making a real provider pause on demand. - The local `send-hold` fixture must exercise the local prompt-send boundary - by emitting the session event and then holding before the first backend - `workflow_state`, so browser proof can capture `Sending prompt.` without - depending on real provider latency. - This fixture is a local development primitive, not a product mode: it must - not bypass backend governance in production, persist as a server session, or - introduce any UI-only event shape that real providers cannot emit. - Live answer text must be paced in the presentation layer, not throttled or - mutated in the canonical stream state: appended content is revealed in short - readable slices while the turn is streaming, and completed/restored messages - render immediately from the full stored content. This adapts OpenCode's - `createPacedValue` live markdown pattern in - `packages/ui/src/components/message-part.tsx` so Pulse feels active without - delaying copy/export, tool boundaries, approvals, or audit evidence. - Live workflow labels follow that same presentation-layer constraint: the - canonical SSE/store state remains the latest typed `workflow_state`, and the - browser must render single fresh status updates immediately while using - bounded live-only history only to pace backend/network burst replacements. - This follows the current OpenCode TUI event mutation model at fetched - `origin/dev` commit - `06d7840d1d42c9815d2d2e45e7fa4090ca4e3577`, especially - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx`, where - `session.next.step.started`, text deltas, and tool progress mutate the - current assistant state event-by-event. Pulse uses the bounded status - history only to preserve reducer continuity, not to delay audit evidence, - approvals, tool boundaries, transcript export, or completed/restored - messages. - Completed Assistant tool rows follow the same source-anchored display policy: - the referenced OpenCode commit - `9ed17da55ab1f7360cc0e01075f763e27fa899e9` keeps ordinary tool activity terse - through `GenericTool`/`InlineToolRow` in - `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx`, hides generic - output unless `showGenericToolOutput` is enabled, and bounds output-heavy - blocks with `packages/opencode/src/cli/cmd/tui/util/collapse-tool-output.ts`. - Pulse adapts that by keeping the completed tool action visible in the - transcript, rendering only short safe plain-text output previews inline, and - keeping structured JSON, unavailable output, noisy output, and the full raw - payload behind Details. This preserves evidence for inspection without - letting command output compete with the Assistant answer flow. - The row itself owns the details trigger, not a tiny adjacent text action: - completed tool rows with inspectable input or output must expose - `aria-expanded`, keyboard activation, and a chevron affordance on the row - header so opening evidence feels like expanding the tool part. That adapts - OpenCode's `BasicTool` collapsible trigger in - `packages/ui/src/components/basic-tool.tsx`, where the trigger row and - arrow own expansion while pending/running tools remain locked. - Tool-row summaries are part of that same source-anchored contract, not - assistant prose cleanup: when provider or backend transport supplies a - function-style call such as `pulse_read(...)` or a friendly string backed by - structured raw input, Pulse must derive the visible row from the parsed tool - arguments and keep the raw transport form behind Details. This adapts - OpenCode's `ToolPart`/`InlineToolRow` rendering in - `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` and the - `session.next.tool.input.*` mutations in - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx`: typed tool state - owns the action row, while raw invocation syntax is inspection detail. - The live active-turn footer is part of the same contract: pending tools must - reuse the tool-row action/command summary instead of falling back to generic - tool names such as `read` or `query`. This keeps the always-visible progress - surface aligned with OpenCode's visible `ToolPart` state rather than hiding - the concrete command until the transcript row catches up. - Streaming thinking rows follow the same source-anchored reasoning-display - contract: OpenCode commit - `9ed17da55ab1f7360cc0e01075f763e27fa899e9` renders reasoning through - `ReasoningPart`/`ReasoningHeader` in - `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` and extracts only - provider summary metadata with `reasoningSummary` in - `packages/opencode/src/cli/cmd/tui/context/thinking.ts`. Pulse adapts that by - showing a live `Thinking:`/completed `Thought:` row with duration and optional - provider summary title while keeping the raw reasoning body out of the - transcript. Provider reasoning must not become fallback assistant answer text when an OpenAI-compatible non-stream response contains `reasoning` or `reasoning_content` without visible `content`. The referenced OpenCode @@ -3183,367 +2499,28 @@ deriving an older display status from `workflowStatusHistory`. `message.content` and storing OpenAI-compatible reasoning only in `ReasoningContent`, so hidden provider reasoning cannot be rendered as the final answer by any Pulse surface that consumes non-stream chat responses. - The active-turn status strip follows the same source-backed part freshness - rule: OpenCode commit `9ed17da55ab1f7360cc0e01075f763e27fa899e9` - updates live assistant parts in place through - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx` - (`latestTool`, `latestText`, `latestReasoning`, and `apply(event)`), - renders reasoning headers with `ReasoningPart`/`ReasoningHeader` in - `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx`, and keeps the - prompt footer as the replacing live status surface in - `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx`. Pulse adapts - that by ranking workflow, content, hidden reasoning, and pending-tool footer - copy by the freshest activity timestamp: a later answer token can replace an - older tool status, and a later in-place tool progress patch can replace the - answer status again without moving the transcript's chronological row. - The active OpenCode-parity Assistant goal requires future parity slices to - reference OpenCode's actual source implementation before changing Pulse - behavior. Each slice must record the inspected OpenCode commit SHA plus - source file, symbol/function, and line anchor for the behavior being adapted, - including message parts, tool-state mutation, progress rendering, - model/session selection, and prompt/footer interaction. Parity means adapting - the proven interaction model, not guessing from screenshots or observed - behavior alone. - The referenced OpenCode source at fetched `origin/dev` commit - `54f4974546104bb72f7a0be2b75b92f9defc009b` builds the model dialog from - provider metadata, favorites, recent models, and provider sections in - `packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx`, keeps - structured `{ providerID, modelID }` current/recent/favorite state with - provider-catalog validation in - `packages/opencode/src/cli/cmd/tui/context/local.tsx`, and formats the - effective model label from provider/model metadata in - `packages/opencode/src/cli/cmd/run/variant.shared.ts`. Pulse's Assistant - drawer adapts that selector workflow by keeping explicit recent model routes - above the provider catalog, routing chat-specific default/override options - through the shared model picker, preserving selected older models in the - visible list, and accepting custom model entries only when they are explicit - `provider:model` routes that the backend chat stream can execute. Unknown - recent or custom Pulse routes may survive catalog hydration only when they - still have a valid provider/model shape; malformed route strings must not - become selectable chat routes. The 2026-06-07 fast-model-route slice - rechecked OpenCode `origin/dev` commit - `31c099be435d59bd6749ace7a9f2bb2245e6d3fa`: the TUI command registry - exposes `model.list` as `/models` plus `/mo` and hidden - `model.cycle_recent` / `model.cycle_recent_reverse` commands in - `packages/tui/src/app.tsx` lines 725-751, the app command registry exposes - `model.choose` as `/model` with keybind `mod+'` in - `packages/app/src/pages/session/use-session-commands.tsx` lines 514-522, - and `DialogModel` promotes favorites/recents before provider catalog rows - in `packages/tui/src/component/dialog-model.tsx` lines 23-54. Pulse adapts - that command model by letting the composer consume `/model ` - as a local route switch, `/model default` as an inherited-default reset, and - `/model next` / `/model previous` as recent-route cycling without sending - those command strings to the provider. Malformed `/model ...` arguments must - remain editable in the composer with a local correction message rather than - being silently cleared or treated as assistant prompts. The referenced - OpenCode source at commit - `9ed17da55ab1f7360cc0e01075f763e27fa899e9` - `packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx` passes the - current structured model route into `DialogSelect` at lines 146-171, and - `packages/opencode/src/cli/cmd/run/footer.command.tsx` marks the current - direct-mode model row with a `current` footer at lines 796-804. Pulse adapts - that by making the shared model picker mark the selected catalog, recent, - override, custom, or inherited-default route as a visible `Current` - `role="option"` row with `aria-selected`, instead of relying on background - color alone. - The referenced OpenCode source at fetched `origin/dev` commit - `147169e9b78bdd8430800f883af6b6485e5156e4` runs ordinary follow-up - prompts through a serial local queue in - `packages/opencode/src/cli/cmd/run/runtime.queue.ts`: prompts submitted - behind an active ordinary turn remain visible as queued prompts, expose - removal through `FooterApi.onQueuedRemove`, and are removed from the visible - queue before their own turn begins. Pulse's Assistant drawer adapts that - behavior by keeping queued follow-ups in the transcript, showing queue - position, snapshotted model-route identity, and edit/remove controls on each - queued user row, and draining those rows through the existing chat-runtime - queue without aborting the active model stream. - The referenced OpenCode source at fetched `origin/dev` commit - `09d9cf01f93798939c1284fbe974b6e1f4d2759d` registers the - `session.interrupt` command while a turn is non-idle in - `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx`, and its - direct-run footer implements the same two-press interrupt guard in - `packages/opencode/src/cli/cmd/run/footer.ts` while rendering the armed - state in `packages/opencode/src/cli/cmd/run/footer.view.tsx`. Pulse's - Assistant drawer adapts that ergonomics model by letting Escape from the - focused composer arm the visible Stop control first and letting the next - Escape confirm the same governed `chat.stop()` path as the Stop button, - including aborting the active stream, clearing queued follow-ups, preserving - partial text, and returning focus to the composer. The App-level Assistant - drawer Escape guard must not close the drawer when Escape originates inside - the shared model picker; model-picker Escape is a local search/listbox close - path that returns focus to the picker trigger while the drawer remains open. - The referenced OpenCode source at fetched `origin/dev` commit - `fa2b63f850fc0a23bec2bdff9e660450d3fe7913` keeps prompt/footer status visible - only while the session is non-idle in - `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx`, and maps - `turn.send`, `turn.wait`, and tool status patches into a live footer in - `packages/opencode/src/cli/cmd/run/footer.ts` plus - `packages/opencode/src/cli/cmd/run/session-data.ts`. Pulse's drawer adapts - that as an active-turn status strip: no idle diagnostics, but while a turn is - loading it must show waiting, current tool/workflow progress, or generating - status even when the transcript already contains an in-flight assistant row. - The referenced OpenCode source at fetched `origin/dev` commit - `09d9cf01f93798939c1284fbe974b6e1f4d2759d` resolves the direct-run wait when - `packages/opencode/src/cli/cmd/run/stream.transport.ts` verifies the session - is idle, maps `turn.idle` to an idle footer phase in - `packages/opencode/src/cli/cmd/run/footer.ts`, and then flushes scrollback - separately from later state sync. Pulse's Assistant drawer adapts that - completion boundary by clearing the visible active turn as soon as the chat - stream has processed its terminal `done` or `error` event; async - conversation/session-list refresh may continue afterward, but it must not - keep the composer footer or transcript row saying the assistant is still - generating. - When multiple governed tools are pending, completed tools must not blank the - status while another tool is still running, and the status heartbeat should - follow the latest progressed pending tool without reordering the transcript's - chronological tool rows. Pulse adapts this with a mutable footer status and - inline pending rows that show the current activity plus elapsed time while - keeping large command output collapsed. - The referenced OpenCode source at fetched `origin/dev` commit - `1399323b78a04229d9bfe00c7436d7f41770fda8` updates the existing typed tool - part through `updateToolCall` in - `packages/opencode/src/session/processor.ts` and renders ordered assistant - content parts through `AssistantMessage`/`AssistantTool` in - `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx`; - Pulse pending tool progress must therefore mutate the matching row in place - and carry separate activity freshness metadata for the footer instead of - moving an earlier active tool row below later tools when progress arrives. - Assistant experience parity claims against OpenCode must cite the inspected - OpenCode commit, source file, source symbol/function, and line anchor that - defines the behavior being adapted; observed UI behavior alone is not - sufficient governance evidence for future parity slices. - The referenced OpenCode source at commit - `f750deaa3e95098fdde5fb00305b273e43c5b2cd` mutates a single tool part from - `pending` input through `running` progress to completed/error in - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx`, with tool metadata - updates flowing through `packages/opencode/src/session/tools.ts`. Pulse's - stream contract adapts that model with `tool_start`, `tool_progress`, and - `tool_end` events that update the same visible pending tool row in place. - The referenced OpenCode source at fetched `origin/dev` commit - `09d9cf01f93798939c1284fbe974b6e1f4d2759d` creates and mutates tool parts as - input arrives in `packages/opencode/src/session/processor.ts` - (`ensureToolCall`, `tool-input-start`, `tool-input-delta`, - `tool-input-end`), applies those input/running/completion updates in - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx` - (`session.next.tool.input.started`, `.delta`, `.ended`, `.called`, - `.progress`, `.success`, `.failed`), and renders the same live tool part in - `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx`. Pulse's - Assistant stream must mirror that timing: model-selected tools become - visible when the tool name is known, streamed function-argument deltas, - including Anthropic `input_json_delta` chunks, surface as `tool_progress`, - later argument/progress updates mutate the pending row, and completion - replaces that row rather than appending a delayed batch of steps. Waiting - until `[DONE]`, `message_stop`, or execution completion recreates the - delayed batch feeling this contract is meant to prevent. Policy-hidden - placeholder attempts remain governed by the runtime and may resolve the - pending row into a durable skipped `tool_cancel` row instead of either - persisting a failed tool card or hiding the activity. If a terminal - `tool_end` or `tool_cancel` reaches the browser without a matching pending - row, the frontend must still append terminal tool evidence instead of - dropping the only visible activity. The 2026-06-07 Pulse slice rechecked - OpenCode `origin/dev` commit - `1025540fcc2a69609a0131a7168300205656d728`, where - `packages/core/src/session/event.ts` and - `packages/core/src/session/message-updater.ts` keep tool lifecycle events as - typed message parts through terminal success/failed states; Pulse adapts - that lifecycle shape by making skipped policy/runtime tool calls visible as - terminal transcript rows. - The 2026-06-07 live-tool-evidence slice rechecked OpenCode `origin/dev` - commit `31c099be435d59bd6749ace7a9f2bb2245e6d3fa`: - `packages/core/src/session/event.ts` lines 307-366 define live tool input - and bounded progress events, `packages/core/src/session/message-updater.ts` - lines 247-293 mutates the same assistant tool part from pending input to - running progress, and `packages/ui/src/components/basic-tool.tsx` lines - 90 and 246-253 keep pending/running tools as active compact rows rather than - expanding details by default. Pulse adapts that source model for - infrastructure operations by keeping the compact live row primary while - allowing an operator to expand pending/running tool rows for copyable raw - input and current progress evidence; raw provider envelopes stay behind - Details and are used there when structured arguments are still placeholder - `{}` / `[]`. The local dev `/fixture pending-tool` stream is the canonical - no-provider browser proof fixture for this live pending-tool Details window - and must keep a paced running state long enough to exercise the row before - it resolves. - The 2026-06-07 skipped-evidence slice rechecked OpenCode `origin/dev` - commit `31c099be435d59bd6749ace7a9f2bb2245e6d3fa`: - `packages/core/src/session/event.ts` lines 307-336 define replayable tool - input start/delta/end events and lines 386-399 define terminal failed tool - events, `packages/core/src/session/message-updater.ts` lines 247-267 and - 318-339 preserve and update the same assistant tool part through input and - failure, and `packages/opencode/src/session/tools.ts` lines 48-65 and - 180-200 keep terminal tool input/output evidence. Pulse skipped/canceled - tool rows must therefore keep raw tool input and skip reason behind the same - expandable, copyable Details panel used for completed terminal tool rows, - while the compact row remains the operator-readable action summary. - While streamed arguments are still invalid, incomplete JSON, or incomplete - provider-style function-call input, the frontend must use the `raw_input` - fragment to show a safe partial command/path/query summary instead of a - blank `{}` request row, then replace it with the structured summary once - parsing succeeds. The 2026-06-07 Pulse slice rechecked OpenCode - `origin/dev` commit `effd27b23900720a53e965396ff1a105c1f7e9c8`: - `packages/opencode/src/session/processor.ts` lines 431-451 append - `tool-input-delta` text to the same call, and - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx` lines 276-294 - render and mutate that pending tool part as input arrives. - The referenced OpenCode source at fetched `origin/dev` commit - `1399323b78a04229d9bfe00c7436d7f41770fda8` keeps tool invocations as durable - message parts in `packages/opencode/src/session/processor.ts` - (`ensureToolCall`, `updateToolCall`, `completeToolCall`) and renders the - typed part immediately in - `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx` - (`AssistantTool`, `pendingInput`, `toolComplete`). Pulse's Assistant chat - stream must also preserve that event-by-event feel when HTTP or browser - buffering delivers several SSE messages in one network chunk: the frontend - stream consumer must let the browser paint between buffered Assistant - progress/tool/status events for chat streams instead of synchronously - draining a coalesced `tool_start`/`tool_progress`/`tool_end` batch in one - JavaScript task. Ordinary content-token streaming must also get an immediate - first-delta paint checkpoint and periodic checkpoints for long coalesced - chunks, but must not be frame-throttled on every token by that progress-batch - safeguard. - Mock-mode Assistant streaming is a canonical fixture for that same runtime - contract, not a frontend-only demo. When `mockmode.IsEnabled()` is true, + Mock-mode Assistant streaming is a canonical backend fixture, not a + client-only demo. When `mockmode.IsEnabled()` is true, `chat.Service.createProvider` must return the internal mock Assistant provider before invoking injected provider factories or configured external providers, and `chat.Service.ExecuteStream` must short-circuit the user turn only after the durable session is ensured and the user message is persisted. - The fixture must emit the same browser-facing event family used by real + The fixture must emit the same client-facing event family used by real turns: anchored `session`/prepare events from the normal entry path followed by mock `workflow_state`, `tool_start`, `tool_progress`, `tool_end`, response `workflow_state`, content chunks, and terminal `done`. It must also - persist a real assistant message with model route `pulse:mock-assistant`, - a successful `pulse_query` tool call, and a successful `pulse_read` tool call + persist a real assistant message with model route `pulse:mock-assistant`, a + successful `pulse_query` tool call, and a successful `pulse_read` tool call whose progress event includes provider-style raw input such as - `pulse_read(...)`. That fixture keeps session restore, transcript tooling, - stream-contract tests, and live browser checks exercising the same - backend-to-browser shape as a real provider/tool turn, including the raw - function-call syntax Pulse must summarize instead of rendering. Pulse-owned - local Assistant routes such as `pulse:local-inventory` and - `pulse:mock-assistant` must render through named labels (`Pulse inventory`, - `Pulse mock Assistant`) in shared model picker and transcript chrome; the - raw route IDs are implementation details, unlike external provider route IDs - that may remain visible for disambiguation. Mock mode is - also an effective runtime-config - overlay: `internal/api/ai_handler.go` startup, restart, config sync, and - tenant service initialization must treat Assistant as enabled in memory when - mock mode is active, without saving or mutating the persisted `AIConfig`, so - `/api/ai/chat` and Assistant sessions are available with zero configured - external providers or models. The live fixture must pace its status, tool, - and content events enough for the browser to paint each state in sequence; - unit tests may disable that pace, but the dev fixture must not collapse into - a single final completed row. This fixture must not emit `provider_fallback` - in non-mock mode and must not become Pulse-authored remediation or routing - behavior. The referenced OpenCode source at fetched `origin/dev` commit - `4519a1da329c1a4fc384054e7203ba7d06928205` publishes tool-call and - tool-result events incrementally in - `packages/core/src/session/runner/publish-llm-event.ts`, mutates assistant - tool parts through pending/running/completed states in - `packages/core/src/session/message-updater.ts`, and renders the current tool - part state in `packages/ui/src/components/message-part.tsx` through - `packages/ui/src/components/basic-tool.tsx`. Pulse adapts that source model - by making the mock fixture prove the same visible state sequence without - copying OpenCode's editor-specific tool catalog. - The referenced OpenCode source at fetched `origin/dev` commit - `09d9cf01f93798939c1284fbe974b6e1f4d2759d` renders tool-specific inline - labels such as `Read `, `Grep ""`, `WebFetch `, and bash - commands in - `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx`. - Pulse's compact tool rows must follow that operator-language model: the row - should summarize the actual governed action (`search "prowlarr"`, `list -active alerts`, `topology summary`, `Inspect devices on current resource`, - or exact governed write commands where the command itself is the operator - decision) instead of exposing only internal action names such as `QUERY -search`, `exec`, provider function calls, or raw JSON; raw input and full - output stay available behind Details. The referenced OpenCode source at - fetched `origin/dev` commit - `06d7840d1d42c9815d2d2e45e7fa4090ca4e3577` renders Bash tool state through - `Bash` in - `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx` - (lines 701-735), where the pending row is replaced by the concrete command - and completed block output keeps `$ ` visible above bounded output. - Pulse adapts that for read-style infrastructure tools by keeping the - operator action label as the primary row and adding a sanitized `$ ...` - command preview below it when the tool input carries a shell command; raw - provider envelopes such as `pulse_read(...)`, raw JSON, and full command - output remain Details-only. Any visible command-copy affordance must copy - that same sanitized command preview rather than the raw provider/tool input. - The referenced OpenCode source at - commit - `9ed17da55ab1f7360cc0e01075f763e27fa899e9` also renders pending tool rows - through `InlineTool` pending text in - `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx` - (`InlineTool`, lines 563-651; `Bash`, lines 701-735; `Read`, lines - 758-770; `Grep`, lines 788-790). Pulse adapts that by using - action-specific pending copy such as `Writing command...`, `Preparing -query...`, and `Reading storage...` before streamed tool arguments are - parseable, then replacing that copy with the governed command/query/resource - summary as soon as input arrives. - The referenced OpenCode source at fetched `origin/dev` commit - `4519a1da329c1a4fc384054e7203ba7d06928205` renders completed bash and - generic tool output inside the tool block and uses `collapseToolOutput` in - `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx` - (`GenericTool`, lines 526-559; `Bash`, lines 701-731) to keep long output bounded. - Pulse's completed tool rows must adapt that by previewing short actionable - plain-text output for successful and failed tools while keeping long output - bounded behind Details. Structured JSON, unavailable output, truncated - successful output, and the full raw payload stay behind Details. The browser - implementation owner is - `frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx`; completed - `tool_end` events preserve streamed `raw_input` for the same readable command - summary used while the tool is pending. - The 2026-06-07 completed-output badge slice rechecked OpenCode `origin/dev` - commit `e82542b8023a8374f29c23b70ec019c8f256354e` in the same - `session-v2.tsx` `GenericTool` / `Bash` renderers (lines 527-559 and - 702-735): overflow is communicated through the expandable tool block, not - through transport-sized copy in the compact row. Pulse adapts that by keeping - exact output type/size in the accessible label and Details panel while the - visible compact badge says only `output available`. - The 2026-06-07 transcript command-evidence slice rechecked OpenCode - `origin/dev` commit `e82542b8023a8374f29c23b70ec019c8f256354e` in - `packages/opencode/src/cli/cmd/run/session-data.ts`: OpenCode keeps tool - activity readable in session history while stripping shell echo noise from - assistant text. Pulse adapts that by including the same sanitized `$ command` - preview shown in live read-tool rows in copied/exported Assistant transcripts. - Transcripts must still exclude raw provider call syntax such as - `pulse_read(...)`, raw JSON tool payloads, obvious command secrets, and - unbounded raw tool output by default. - The 2026-06-07 active-dock pacing slice rechecked current OpenCode - `origin/dev` commit `e82542b8023a8374f29c23b70ec019c8f256354e` in - `packages/tui/src/feature-plugins/system/session-v2.tsx`: `InlineTool` - (lines 564-651) keeps one visible activity row that moves from pending copy - to completed copy, while `Bash` (lines 702-740) keeps shell activity visible - through the same inline/block transition. Pulse adapts that for the - Assistant dock by pacing burst-arriving workflow status history locally so - `Preparing context`, `Reading inventory`, and provider wait states appear in - sequence; statuses that arrive one at a time still replace immediately. - The referenced OpenCode source at fetched `origin/dev` commit - `fa2b63f850fc0a23bec2bdff9e660450d3fe7913` also keeps assistant text, - reasoning, and tool invocation as typed message parts in - `packages/opencode/src/session/message.ts`, while - `packages/opencode/src/session/processor.ts` updates text and reasoning - parts through `*-delta` events instead of rendering raw provider tool-call - syntax as assistant prose. The referenced OpenCode source at fetched - `origin/dev` commit `1399323b78a04229d9bfe00c7436d7f41770fda8` renders - reasoning with `AssistantReasoning` and `ReasoningHeader` in - `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx`, - separate from `AssistantText` and `AssistantTool`. Pulse's frontend stream - reducer must preserve the same user-facing invariant: visible transcript - content is typed assistant text, a neutral typed thinking-progress row, or a - typed Pulse tool/approval/question row. The thinking-progress row may expose - live activity state such as `Thinking...` / `Thinking complete`, but it must - not render raw provider reasoning text. Suspicious compacted provider - prelude text that looks like tool-call narration must be buffered until it is - proven to be normal answer text or stripped when a raw tool-call marker - arrives; it must not flash as run-on prose such as - `I'llcheckthedevicenodes...` while the actual governed tool row is still - being assembled, and it must not be promoted to final assistant answer text - when the stream reaches `done` without proving a normal answer. The - referenced OpenCode source at fetched `origin/dev` commit - `1025540fcc2a69609a0131a7168300205656d728` keeps rendered answer text as - typed text parts in `packages/ui/src/components/message-part.tsx` and uses - `createPacedValue` only as a presentation-layer reveal; Pulse adapts that by - preserving typed content exactly while suppressing malformed compacted - tool-prelude residue rather than inventing spaces or storing it as prose. + `pulse_read(...)`, so session restore and stream-contract tests exercise the + same shape as a real provider/tool turn. Mock mode is also an effective + runtime-config overlay: `internal/api/ai_handler.go` startup, restart, + config sync, and tenant service initialization must treat Assistant as + enabled in memory when mock mode is active, without saving or mutating the + persisted `AIConfig`, so `/api/ai/chat` and Assistant sessions are + available with zero configured external providers or models. The fixture + must not emit `provider_fallback` and must not become Pulse-authored + remediation or routing behavior. A pre-tool assistant preamble such as "I'll check that" does not satisfy the final-answer contract after Pulse executes tools. The final-response guard must require non-empty assistant text after the latest user/tool-result @@ -3643,77 +2620,25 @@ query...`, and `Reading storage...` before streamed tool arguments are 247-318), by preserving the operator's explicit tool intent until Pulse can stream a visible `tool_start` / `tool_progress` / `tool_end` path instead of collapsing the turn into unrelated inventory prose. - The Assistant transcript should show compact - activity as work happens: local context reads, governed tool names, provider - routing, fallback, and first-token waiting must appear as the latest - replacing live status/header status rather than a dead-looking wait state, a - dumped list of transient phases, or a delayed replay of stale status history. - This adapts the referenced OpenCode source at fetched `origin/dev` commit - `914a643`, where the app session timeline derives visible activity from the - current session status and streamed message/tool parts instead of pacing old - status labels back through the UI. Hide or collapse unsafe details and bulky - outputs, but do not hide the fact that Pulse is running a local read, - invoking a governed tool, or waiting on a provider. Stream-idle heartbeats - must inherit the selected provider/model route when Pulse has that route, so - the visible row reads as route-specific liveness instead of falling back to - generic Assistant waiting copy. Pulse must not rewrite the inputs of a - model-chosen tool call (the former summary-only `pulse_query` topology - input default is removed with the query-only scope); `pulse_query` - defaults are owned by the tool definition the model sees, not by - post-hoc input mutation. Assistant answer style should stay operational - and must not use emoji, warning icons, or decorative symbols unless the user - explicitly asks for that tone. + Stream-idle heartbeats must inherit the selected provider/model route when + Pulse has that route. Pulse must not rewrite the inputs of a model-chosen + tool call (the former summary-only `pulse_query` topology input default is + removed with the query-only scope); `pulse_query` defaults are owned by the + tool definition the model sees, not by post-hoc input mutation. Assistant + answer style should stay operational and must not use emoji, warning icons, + or decorative symbols unless the user explicitly asks for that tone. The system prompt for a turn must describe only the tools actually offered to the provider for that turn; generic tool-governance prose must not name tools hidden from the current manifest. Deterministic provider-request capture is the primary proof harness for this fast path; live OpenRouter/browser checks are final smoke tests, not the iteration loop. - Restored Assistant sessions must hydrate saved assistant content and - persisted tool calls into the same transcript event shape used by live - streams so switching sessions does not hide prior tool evidence or collapse - the resumed conversation into a text-only transcript. Saved Assistant - message history is part of that same output contract: service reads and - `GET /api/ai/sessions/{id}/messages` must return a client-safe projection - that normalizes collection fields, strips `reasoning_content`, and sanitizes - stored assistant prose before the browser restores a transcript. Provider - reasoning may remain in runtime-only history for model continuity, but it - must not serialize through public session history or resumed drawer state. - The referenced OpenCode source at fetched `origin/dev` commit - `09d9cf01f93798939c1284fbe974b6e1f4d2759d` stores message parts as typed - text, reasoning, and `tool-invocation` entries whose tool invocation state - is `call`, `partial-call`, or `result` in - `packages/opencode/src/session/message.ts`. Pulse adapts that restored - transcript contract by folding persisted internal tool-result transport - messages back into the browser-safe assistant `tool_calls` projection as - completed `output` plus `success` state, preserving the assistant message's - effective model route, and omitting the internal tool-result messages from - the restored browser transcript. - Pending tool progress is part of the same transcript contract: pending tool - stream events must render inline as compact rows in the assistant turn, - transition to `running` or `waiting` through `tool_progress`, and resolve in - place on `tool_end` instead of disappearing until completion or relying only - on the drawer-level status bar. The referenced OpenCode source at fetched - `origin/dev` commit `ba57718b0516c7a8670d1e820b1a24146a8b8262` emits an ACP - `tool_call` when a tool part is known and then sends `tool_call_update` - patches for pending, running, completed, and failed states through - `packages/opencode/src/acp/event.ts` and - `packages/opencode/src/acp/tool.ts`; Pulse adapts that by keeping the live - pending row's current progress text visible inside the transcript at drawer - width, not only in the footer/status strip. - The drawer transcript must treat those in-place progress patches as fresh - visible activity even when the message count and stream-event count do not - change. The referenced OpenCode source at commit - `9ed17da55ab1f7360cc0e01075f763e27fa899e9` applies - `session.next.tool.progress` by mutating the latest matching tool part in - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx` - (`apply(event)`, line 120; `session.next.tool.progress`, lines 317-326) - and renders the same session-owned tool part through - `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` - (`ToolPart` mapping, lines 1788-1796; `InlineToolRow`, line 1926). - Pulse adapts that by deriving transcript scroll/freshness from the latest - message's activity fingerprint, including pending-tool progress and - `updatedAt`, instead of using appended-event count as the only streamed - activity signal. + Saved Assistant message history is part of the same output contract: + service reads and `GET /api/ai/sessions/{id}/messages` must return a + client-safe projection, including persisted tool calls, that normalizes + collection fields, strips `reasoning_content`, and sanitizes stored + assistant prose before a client restores a transcript. Provider reasoning + may remain in runtime-only history for model continuity, but it must not + serialize through public session history. Assistant session listing is a history-browsing path, not a send-path lock. The session store must not hold its shared mutex while scanning and parsing every persisted session file for `/api/ai/sessions`; writes must remain @@ -3733,34 +2658,9 @@ query...`, and `Reading storage...` before streamed tool arguments are Pulse-owned background sessions are not resumable chats. The session store owns the well-known background session IDs (`patrol-main`, `patrol-eval`, `investigation-*`) through `chat.IsSystemSessionID` and marks their - summaries `system: true` in every `Session` projection. The Assistant - empty-state quick-resume list (`selectQuickResumeSessions` in - `frontend-modern/src/components/AI/Chat/recentSessionsModel.ts`) must - exclude system sessions — a Patrol detection log titled with the raw triage - seed is forensic material, not a conversation to resume — while the - drawer-owned session picker and the Settings sessions panel keep listing - them for inspection. The canonical Patrol forensic surface remains the + summaries `system: true` in every `Session` projection, so clients can keep + them out of resume lists. The canonical Patrol forensic surface remains the `PatrolRunRecord` history at `/api/ai/patrol/runs`. - Assistant session navigation must provide a searchable history path in the - drawer-owned picker, using the canonical `/api/ai/sessions` contract rather - than a separate recent-chat store. Search is applied before result limiting - so the picker can narrow older sessions without hiding matches behind a - recency cap, the picker must open immediately with cached sessions while any - refresh/search is still running, and loading/error states must stay inside - the picker instead of making the main send path look busy. The referenced - OpenCode source at fetched `origin/dev` commit - `4519a1da329c1a4fc384054e7203ba7d06928205` implements - `SessionSwitcherDialog` in - `packages/opencode/src/cli/cmd/tui/feature-plugins/session/dialog.tsx` with - `createDebouncedSignal("", 150)`, - `sdk.client.session.list({ search: input.query, limit: 30, ...input.filter })`, - recency ordering through - `orderByRecency`, and pinned/current session options before navigation. - Pulse adapts that source workflow with a debounced Assistant session search - field, bounded recent-session refreshes with - `GET /api/ai/sessions?limit=30`, searched refreshes with - `GET /api/ai/sessions?search=...&limit=30`, and searched result rows that - resume through the same session-loading path as the normal recent list. Assistant session forking is a runtime-backed session workflow, not a header-only UI affordance. The referenced OpenCode source in `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` registers @@ -3769,49 +2669,22 @@ query...`, and `Reading storage...` before streamed tool arguments are source behavior through `POST /api/ai/sessions/{id}/fork`: the local `chat.Service` must route to `SessionStore.Fork`, and the store must create a fresh durable session ID with fresh create/update timestamps while cloning - the saved transcript and browser-safe model handoff summary. Copied + the saved transcript and client-safe model handoff summary. Copied messages keep their per-session message/tool IDs so assistant tool-call and tool-result relationships remain intact inside the fork. Forking must not mutate the source session, must continue through normal session list/load contracts, and must not return `not_implemented` from the local runtime. OpenCode-style session file diff/revert workflows do not apply to Pulse Assistant sessions: Pulse does not own local code-file edits, and - infrastructure mutations are governed through approval/action history. The - Settings maintenance surface must not advertise session file changes or - session revert actions, and legacy direct calls to + infrastructure mutations are governed through approval/action history. Legacy direct calls to `/api/ai/sessions/{id}/diff`, `/revert`, and `/unrevert` must fail explicitly with `501 Not Implemented` rather than returning a success-shaped placeholder. - The picker trigger and result rows are part of that source-backed workflow: - the trigger must expose its accessible name and expanded state, opening the - picker must focus search, and each result must be a keyboard-addressable - option with up/down, page, home/end, and Escape navigation from both the - focused search field and focused result rows. Session-picker Escape is a - local close path that returns focus to the session trigger and must consume - the keypress so the App-level Assistant drawer Escape guard does not also - close the whole drawer. Delete remains a named row action instead of a - mouse-only affordance. - Message-level copy is part of the same OpenCode-aligned message action - surface, but Pulse must keep it browser-safe and role-neutral rather than - implying unsupported message-level fork/revert semantics. The referenced - OpenCode source in `packages/tui/src/routes/session/dialog-message.tsx` - exposes `message.copy` by collecting non-synthetic text parts for the - selected message. Pulse adapts that by allowing visible user and completed - assistant transcript rows to copy their own visible text through the shared - clipboard fallback helper; hidden reasoning, raw tool input/output, provider - envelopes, and scoped handoff metadata remain excluded unless the operator - explicitly opens a raw Details surface. - Assistant session rename is part of that same source-backed session - workflow. The referenced OpenCode source in - `packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx` - imports `DialogSessionRename` and invokes the runtime session rename action - from the session list, so Pulse must not leave rename as local picker state. - Pulse owns the equivalent through `PATCH /api/ai/sessions/{id}`, - `chat.Service.RenameSession`, and `SessionStore.Rename`; the drawer picker - may render the edit inline, but saving must persist the normalized title, - return the updated `ChatSession`, update the visible row without closing the - picker, and leave messages, handoff context, approvals, and tool evidence - unchanged. + Assistant session rename is part of the same session workflow. Pulse owns it + through `PATCH /api/ai/sessions/{id}`, `chat.Service.RenameSession`, and + `SessionStore.Rename`; saving must persist the normalized title, return the + updated `ChatSession`, and leave messages, handoff context, approvals, and + tool evidence unchanged. Session titles follow OpenCode's generated-title model: after a session's first exchange completes, the chat runtime upgrades the auto-truncated first-prompt placeholder to a short model-generated title @@ -3824,91 +2697,49 @@ query...`, and `Reading storage...` before streamed tool arguments are record usage as `assistant_session_title`, and a failed or timed-out title call leaves the placeholder in place rather than failing anything user-visible. - Assistant turn undo/redo is also a source-backed session repair workflow, - not a transcript-local delete button. The referenced OpenCode source in - `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` registers - `session.undo` and `session.redo` as first-class session commands and uses - runtime revert/unrevert responses to restore the user's prompt into the - composer. Pulse adapts that behavior through - `POST /api/ai/sessions/{id}/undo` and - `POST /api/ai/sessions/{id}/redo`: the backend session store must remove the - latest user-authored turn plus following assistant/tool messages as one - durable turn, persist a bounded redo stack, expose `can_redo` on the - browser-safe session summary, clear redo state when a new message or trim - changes the transcript, and keep forked sessions independent from the source - redo stack. The drawer must restore the removed prompt, structured mentions, - finding id, handoff context and metadata, and selected - model route when available so the operator can edit and resend without - reconstructing hidden context from transcript prose. + Assistant turn undo/redo is a session repair workflow, not a + transcript-local delete. Pulse owns it through + `POST /api/ai/sessions/{id}/undo` and `POST /api/ai/sessions/{id}/redo`: + the backend session store must remove the latest user-authored turn plus + following assistant/tool messages as one durable turn, persist a bounded + redo stack, expose `can_redo` on the client-safe session summary, clear redo + state when a new message or trim changes the transcript, and keep forked + sessions independent from the source redo stack. Retry and regenerate are turn re-runs over the same undo boundary, not a parallel history mutation path. `POST /api/ai/sessions/{id}/undo` accepts an optional JSON body (`chat.SessionTurnUndoOptions`) whose `expected_prompt` field, when non-empty, makes the removal conditional on the latest user-authored prompt matching the prompt being re-run, so a retry whose failed send never reached the session cannot remove a different turn. - Frontend retry ("Try again" on a failed turn, model-route switch retry) and - regenerate must remove the replaced turn through this guarded undo before - re-sending so session history never records the prompt twice; a failed - guard or transport error degrades to a plain re-send rather than blocking - the retry. The removed turn stays redoable until the re-sent message lands, - at which point the normal new-message rule clears redo state. The - regenerate affordance is offered only on the latest settled assistant - answer (nothing streaming, no error block, no pending approval or - question, and a user prompt available to re-send) because the undo - boundary only operates on the last durable turn. Session mutations stay POST-only at the handler: abort, summarize, fork, undo, redo, and steer refuse every other method with `405` and `Allow: POST` before resolving the Assistant service. A `GET` or `HEAD` that the demo-mode read-only guard or the CSRF check admits as a read therefore cannot cancel a running turn, compact or rewind a transcript, or persist a fork. - The empty Assistant drawer may surface recent non-empty sessions as direct - resume actions using the backend session list already owned by the drawer; - it must not create a parallel recent-chat store or product-authored prompt - shortcut path. Assistant output hygiene is part of the same boundary: provider reasoning - and raw serialized tool-call artifacts must never render as assistant - transcript prose or serialize into client-facing chat streams. Browser-bound + and raw serialized tool-call artifacts must never serialize into + client-facing chat streams or persisted assistant prose. Client-bound Assistant stream events must pass through `chat.StreamEvent.ClientSafe()`; provider `thinking` chunks are runtime-only and may be retained internally - for model continuity, but they are dropped before the browser/API boundary. - The referenced OpenCode source at fetched `origin/dev` commit - `1399323b78a04229d9bfe00c7436d7f41770fda8` stores assistant output as typed - text, reasoning, and `tool-invocation` parts in - `packages/opencode/src/session/message.ts` and mutates those typed parts - through explicit stream events in - `packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx`; Pulse adapts that - invariant by stripping operational decorative status glyphs, warning icons, - and check/cross badges from browser-safe assistant prose while preserving - ordinary Unicode answer text such as units. - The agentic stream may translate the first private provider reasoning delta - before visible output into a neutral `model_thinking` workflow status so the - drawer shows live activity without exposing chain-of-thought. Neutral - progress comes from the active stream state, workflow status, or governed - tool/question/approval events, not from a reasoning body. Pulse tool - invocations must surface only through governed `tool_start` / `tool_end`, - approval, or question blocks; if a provider emits `pulse_*` / `patrol_*` - calls, DSML, XML/function-call envelopes, or JSON tool-call shapes as text - content, the chat runtime must strip them before streaming, persistence, and - frontend rendering. Compacted no-whitespace internal prelude text attached - to a leaked tool invocation is part of that same artifact and must be - suppressed or retracted from the current stream segment instead of rendered - as assistant prose; if that compacted prelude is still pending when the - stream closes, terminal flush must drop it rather than turning unreadable - provider residue into the final answer. Drawer-level loading/progress status - must stay scoped to the active turn and must collapse back to quiet when the - turn is idle; it may mirror the latest active tool/workflow state as a - compact heartbeat, but raw outputs, restored evidence, errors, and completed - details stay in the transcript row that owns the turn. Completed tool rows in - the drawer may show - compact tool name, action summary, status, and an - explicit details affordance, but raw tool input/output JSON must not render - in the default transcript. Token accounting and other provider metadata - remain runtime/accounting data, not normal transcript prose. - Background provider-readiness checks are diagnostics and must stay quiet - while idle unless they produce an actionable issue; checking status may - surface during an active send, but an open empty drawer must not look busy - just because Pulse is verifying the selected route. + for model continuity, but they are dropped before the client/API boundary. + Client-safe assistant prose has operational decorative status glyphs, + warning icons, and check/cross badges stripped while ordinary Unicode + answer text such as units is preserved. The agentic stream may translate + the first private provider reasoning delta before visible output into a + neutral `model_thinking` workflow status so clients can show live activity + without exposing chain-of-thought. Pulse tool invocations must surface only + through governed `tool_start` / `tool_end`, approval, or question blocks; if + a provider emits `pulse_*` / `patrol_*` calls, DSML, XML/function-call + envelopes, or JSON tool-call shapes as text content, the chat runtime must + strip them before streaming and persistence. Compacted no-whitespace + internal prelude text attached to a leaked tool invocation is part of that + same artifact and must be suppressed or retracted from the current stream + segment instead of streamed as assistant prose; if that compacted prelude + is still pending when the stream closes, terminal flush must drop it rather + than turning unreadable provider residue into the final answer. Token + accounting and other provider metadata remain runtime/accounting data, not + transcript prose. 4. Add or change Patrol, alert-analysis, or remediation transport through `internal/api/ai_handlers.go`, `internal/api/ai_intelligence_handlers.go`, and `frontend-modern/src/api/patrol.ts` Provider preflight diagnostics returned from `internal/api/ai_handlers.go` must reuse the Patrol runtime failure classifier in `internal/ai/` and @@ -3933,216 +2764,76 @@ query...`, and `Reading storage...` before streamed tool arguments are require an explicit `allow_broad_scope` request from a dedicated rule management surface. 5. Add or change AI usage/cost dashboard presentation through `frontend-modern/src/components/AI/AICostDashboard.tsx` and `frontend-modern/src/utils/aiCostPresentation.ts` -6. Add or change AI provider, control-level, or chat/session presentation through `frontend-modern/src/components/AI/Chat/`, `frontend-modern/src/utils/aiProviderPresentation.ts`, `frontend-modern/src/utils/aiProviderHealthPresentation.ts`, `frontend-modern/src/utils/aiControlLevelPresentation.ts`, `frontend-modern/src/utils/assistantPageContext.ts`, and `frontend-modern/src/utils/aiChatPresentation.ts` +6. Add or change AI provider and provider-health presentation through `frontend-modern/src/utils/aiProviderPresentation.ts` and `frontend-modern/src/utils/aiProviderHealthPresentation.ts` AI provider/model presentation must preserve the model transport route when the selected provider is a gateway. OpenRouter-routed model IDs such as `openrouter:deepseek/...` must render with an explicit `via OpenRouter` label in the shared picker, System AI settings status, and inherited default descriptions unless the server-supplied model name already carries that - route. Assistant transcript rows must carry and render the effective model - route that produced each live or restored response so route recovery and - mixed-provider sessions remain auditable. Direct provider models must not - gain a gateway label, but explicit direct route strings such as - `deepseek:deepseek-v4-pro` must still render with provider identity rather - than collapsing to a bare-looking model payload. Gateway route strings must - still render as readable provider/model labels before catalog hydration; this - mirrors OpenCode's source pattern of retaining structured - `providerID`/`modelID` metadata and presenting catalog names with ID - fallbacks rather than leaking raw route storage IDs into primary chat chrome. - Assistant model selector actions must - remain route-distinct: if the configured chat override resolves to the same - route as the effective default or the already selected session model, the - drawer must not render a duplicate override action. - Assistant provider-readiness actions must send operators to the canonical + route. Direct provider models must not gain a gateway label, but explicit + direct route strings such as `deepseek:deepseek-v4-pro` must still render + with provider identity rather than collapsing to a bare-looking model + payload. Gateway route strings must still render as readable provider/model + labels before catalog hydration, presenting catalog names with ID fallbacks + rather than leaking raw route storage IDs into primary settings chrome. + The shared model picker separates selected model labels from badges such as + `default` in both visible text and the button accessible name, so an + OpenRouter default route renders as + `Qwen: Qwen3.7 Plus via OpenRouter · default` rather than + `OpenRouterdefault`. + Provider-readiness repair actions must send operators to the canonical Pulse Intelligence > Provider & Models route `/settings/pulse-intelligence/provider`; `/settings/system-ai` remains a compatibility alias for old deep links rather than the href emitted by new - Assistant repair actions. - Assistant discovery-context hints must use the same product language as - Pulse Intelligence settings: the visible hint is `Discovery is off`, while - the body explains that enabling it gives Assistant real service, version, - and command context instead of generic guidance. The hint must not - reintroduce the old `Workload Discovery` label as a separate product concept. - Authenticated shell Assistant launches must use - `frontend-modern/src/utils/assistantPageContext.ts` to attach factual - current-route context before the drawer opens. That helper may identify the - current platform, Patrol, Alerts, Settings, or unknown Pulse view and provide - neutral briefing metadata, but it must not synthesize a prompt, require a - tool choice, or turn Assistant back into the primary operations front door. - Assistant route and control chrome belongs with the prompt surface, not the - drawer title row. The referenced OpenCode source at commit - `9ed17da55ab1f7360cc0e01075f763e27fa899e9` - `packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx` derives the - current provider label in `Prompt` lines 196-197 and renders agent, model, - provider, and variant metadata in the prompt footer at lines 1452-1479; the - model metadata row uses an explicit `·` separator before the model at line - 1462 and before variants at line 1471, so adjacent metadata must not collapse - into fused labels. Pulse's shared model picker adapts that by separating - selected model labels from badges such as `default` in both visible text and - the button accessible name, so an OpenRouter default route renders as - `Qwen: Qwen3.7 Plus via OpenRouter · default` rather than - `OpenRouterdefault`. - The session route injects that prompt through - `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` - `session_prompt` lines 1313-1333. Pulse adapts that by keeping the active - model route selector, control-mode selector, and last-turn summary in - composer chrome while the drawer header stays limited to drawer/session - commands. - Empty Assistant sessions are prompt-first transcript surfaces, not product - marketing or instruction panels. The referenced OpenCode source at commit - `9ed17da55ab1f7360cc0e01075f763e27fa899e9` - `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` derives - session messages in `Session` lines 185-203, renders only the message - stream inside the scrollbox at lines 1188-1295, and renders `Prompt` in the - `session_prompt` slot at lines 1313-1333 without inserting a centered - no-message title or subtitle. Pulse adapts that by leaving a blank - transcript blank, keeping scoped handoff context in the drawer context - strip, and limiting no-message transcript affordances to real - recent-session resume actions. - The referenced OpenCode source at fetched `origin/dev` commit - `1399323b78a04229d9bfe00c7436d7f41770fda8` renders the completed assistant - footer in - `packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx` - (`AssistantMessage`) with agent, provider/model, and turn duration rather - than token counts. Pulse Assistant rows adapt that by keeping visible token - accounting out of the transcript while showing a compact completed-turn - duration beside the effective model label once a turn reaches `done`, - `error`, or user interruption. Runtime token usage may surface in - low-priority composer chrome instead, but the completed-turn surface is a - route summary rather than a token counter: the referenced OpenCode source at - fetched `dev` commit `3867fa2bad0e644166e360e2e99cfe426fe71105` imports - `turnSummaryCommit` in - `packages/opencode/src/cli/cmd/run/scrollback.surface.ts` lines 19-20, and - `packages/opencode/src/cli/cmd/run/turn-summary.ts` lines 5-46 renders a - final system summary from agent, model, and duration. Pulse adapts that by - deriving the latest non-streaming assistant `ChatMessage`, rendering the - effective provider/model route, completed-turn duration, and backend-proven - total token count in the composer footer. The input/output split remains in - the footer title/accessibility text, not the visible transcript or composer - copy, so shorthand such as `4358 in · 943 out` cannot read like assistant - output. If provider usage is missing, the route and duration remain visible; - active streaming turns never become the completed-turn summary. Cost and - context-limit percentages stay absent until the runtime exposes those values - through a governed contract. - Assistant tool activity is visible transcript activity, not a hidden context - footer. The referenced OpenCode source at fetched `origin/dev` commit - `e82542b8023a8374f29c23b70ec019c8f256354e` - `packages/opencode/src/cli/cmd/run/types.ts` defines append-only - `StreamCommit` rows for assistant, reasoning, tool, and system sources at - lines 284-312, while - `packages/opencode/src/cli/cmd/run/footer.ts` queues commits and only - coalesces consecutive progress chunks for the same part/tool at lines - 512-545. Pulse adapts that contract by rendering each Assistant - `pending_tool`, completed `tool`, and `tool_cancel` event as its own - chronological transcript row. Tool inputs, command previews, progress text, - and outputs may remain collapsed inside the row, but context/read/query - tools must not be replaced by a generic grouped footer that makes several - operations appear all at once. - Queued follow-up turns are active session pressure, not hidden backlog. - The referenced OpenCode source at fetched `origin/dev` commit - `e82542b8023a8374f29c23b70ec019c8f256354e` - `packages/opencode/src/tool/todowrite.txt` describes status tracking as - surfacing progress to the user at line 1 and requires status updates in real - time rather than batched completion at line 25, while - `packages/opencode/src/session/tools.ts` updates running tool metadata with - title, status, input, and start time at lines 54-63. Pulse adapts that - session-activity pattern by keeping queued follow-ups visible in the - Assistant activity dock and appending the queued count to the active-turn - headline while the current response is streaming. The queued row may retain - edit, remove, promote, resume, and clear controls, but the primary headline - must not imply the user's just-submitted follow-up disappeared behind a - generic `Generating response` state. - Restored Patrol mode save-failure handoffs may preserve compatible - summary kinds such as `patrol_configuration_failure`, but Assistant drawer - titles, subjects, action labels, and safety notes must describe Patrol mode - rather than reviving generic configuration wording. Stable route markers, - telemetry fields, and API wire names may retain `patrol_control`; - user-facing Assistant, external-agent, generated MCP, and frontend copy must - call the operator's choice `Patrol mode`. -7. Keep AI chat presentation helpers aligned through `frontend-modern/src/components/AI/Chat/` and the shared `frontend-modern/src/utils/textPresentation.ts` -8. Keep assistant drawer context, session, and org-switch reset state aligned through the shared `frontend-modern/src/stores/aiChat.ts` boundary instead of letting `frontend-modern/src/App.tsx`, `frontend-modern/src/AppLayout.tsx`, or feature callers fork their own assistant shell state + repair actions. + Stable route markers, telemetry fields, and API wire names may retain + `patrol_control`; user-facing external-agent, generated MCP, and frontend + copy must call the operator's choice `Patrol mode`. +7. Keep AI presentation text helpers aligned through the shared `frontend-modern/src/utils/textPresentation.ts` +8. Keep the AI availability session capability aligned through the shared `frontend-modern/src/stores/aiAvailability.ts` boundary instead of letting `frontend-modern/src/App.tsx`, `frontend-modern/src/AppLayout.tsx`, or feature callers fork their own AI-enabled state The shared app shell may also mount the deployment-installability-owned post-update release highlights card. That card must consume published release notes through the update surface and must not read, reset, or - otherwise couple itself to Assistant sessions, Patrol state, model routing, - or AI-provider readiness. - That shared drawer ownership also covers passive resource reads while the - shell is mounted but closed. `frontend-modern/src/components/AI/Chat/` - may consume the live websocket snapshot or the existing unified-resource - cache for assistant context and suggestions, but it must not reopen - `useResources()` or trigger a second unfiltered `all-resources` REST fetch - just because the drawer component is present in the app shell. - The same app-shell boundary keeps Patrol/Assistant/Actions utility navigation + otherwise couple itself to AI availability, Patrol state, model routing, or + AI-provider readiness. + The same app-shell boundary keeps Patrol and Actions utility navigation accessible-name safe: labelled icon SVGs may remain meaningful when rendered standalone, but `frontend-modern/src/AppLayout.tsx` must treat them as decorative inside tabs so the announced tab name comes from product chrome and meaningful badge text rather than icon title duplication. Global - governed operations use the peer `/actions` utility destination and stable - Assistant page context. Actions owns its selected state and pending-approval - badge while Patrol owns only its operational-attention count. This - navigation relationship must not merge Actions state into Assistant drawer - state or allow the drawer to become an alternate action review client. - Scoped - approval handoffs sourced from Patrol, active alerts, or alert incident - timelines must render as source-named investigation handoffs in the drawer - instead of generic dashboard briefs. Source-owned handoff helpers may attach - bounded model-only context, resources, action references, and metadata, but - ordinary context attachment must not synthesize, prefill, or submit a user - prompt. An explicit labelled Explain action is a user-selected task and - dispatches the shared evidence-first explanation request through - `frontend-modern/src/components/AI/Chat/hooks/useExplanationRequest.ts`. - It preserves drafts and current work, captures the selected context before - asynchronous initialization, and uses normal send, queue, and retry handling. - The store marks that context `autonomousMode:false` so the drawer discloses - approval-required posture; the request itself carries no execution-mode - field, because `/api/ai/chat` sets that mode server-side. It must not prescribe a diagnosis - or a tool sequence or grant action authority. The drawer - presentation must stay compact: source, status, one primary subject, and an - optional safe route link. It must not render Patrol-authored remediation - steps, evidence chips, command summaries, recommendations, or suggested-prompt - chips as the answer. Restored Patrol assessment sessions must use the - `patrol-assessment` target identity rather than inheriting retired dashboard - context; Patrol no longer opens live assessment handoffs. While such a - session is attached, the Assistant empty - message state must also remain source-named and must not fall back to generic - cluster/system starter prompts that compete with the attached briefing. - Structured Patrol run handoffs may render bounded, redacted diagnostic lines - in the drawer when they are opened directly from Patrol run history, but the - attached headline must remain source-owned (`Patrol run attached`) - and the drawer must still exclude raw provider payloads, commands, and - Patrol-authored remediation steps. + governed operations use the peer `/actions` utility destination. Actions + owns its selected state and pending-approval badge while Patrol owns only + its operational-attention count. Platform tabs in that same `frontend-modern/src/App.tsx` shell may remain admitted after a current authenticated REST bootstrap or from retained server-owned WebSocket state during stream recovery. That navigation - evidence must remain independent from Assistant session, handoff, and drawer - state, and browser-local Assistant or resource metadata must not resolve - platform navigation on an otherwise evidence-free first load. The app - shell's global GitHub-star prompt stays behind the kiosk/settings-access - gate and outside every AI runtime surface. Assistant, Patrol, Actions, and - Discovery chrome must not host commercial prompts. - Reloaded Assistant sessions may consume the backend-owned + evidence must remain independent from AI availability, and browser-local + resource metadata must not resolve platform navigation on an otherwise + evidence-free first load. The app shell's global GitHub-star prompt stays + behind the kiosk/settings-access gate and outside every AI runtime surface. + Patrol and Actions chrome must not host commercial prompts. + Clients that reload Assistant sessions may consume the backend-owned `handoff_summary` only as safe presentation state and a Patrol finding pointer; hidden model context, command payloads, preflight data, and action - results stay backend-owned and must not be reconstructed in the browser. + results stay backend-owned and must not be reconstructed in a client. 9. Add or change public Pulse Intelligence overview wording through `docs/AI.md`; it may - describe Assistant and Patrol capabilities, but it must not revive legacy + describe Patrol and external-agent capabilities, but it must not revive legacy commercial shorthand such as `incident memory` as a current product promise. The public overview must preserve the product architecture: Pulse Intelligence Core owns canonical context, governed actions, safety gates, approval state, action audit, and verification; Pulse Patrol is the primary built-in operator on that core; its public summary must stay concise as watching, investigating, acting within the chosen Patrol mode, verifying outcomes, - and recording what happened; and Pulse Assistant plus Pulse MCP are - contextual and external-agent access paths over the same governed - capabilities. + and recording what happened; and Pulse MCP is the external-agent access + path over the same governed capabilities. Public overview, safety, and onboarding copy must use the same visible Patrol mode labels as the product (`Watch only`, `Ask first`, `Safe auto-fix`, `Autopilot`) and must not reintroduce the retired `Only watch`, `Ask before changes`, `Fix safe issues`, `Auto-fix safe issues`, `Full control`, or `Policy autopilot` labels as customer-facing names. - The public overview must also preserve the model-owned AI boundary: Pulse - Assistant and Patrol provide governed context, tools, safety gates, + The public overview must also preserve the model-owned AI boundary: Patrol + and the external-agent path provide governed context, tools, safety gates, approval state, and audit trails, while the configured LLM owns diagnosis, prioritization, fix reasoning, and tool choice. Public copy must not present Pulse code as the intelligence engine, a prompt-keyword router, a @@ -4157,50 +2848,47 @@ query...`, and `Reading storage...` before streamed tool arguments are External-agent setup metadata follows the same rule: Pulse MCP server name, command, base URL flag/default, token environment variable, and supported client config families are manifest-owned in - `/api/agent/capabilities.mcpAdapter`. Assistant, Pulse Intelligence - settings, README, and MCP surfaces may present that setup, but must not + `/api/agent/capabilities.mcpAdapter`. Pulse Intelligence settings, + README, and MCP surfaces may present that setup, but must not maintain separate MCP setup constants. Security API settings may only host the scoped-token creation link used by that setup. 10. Platform/runtime top-level pages registered in `frontend-modern/src/App.tsx` and the primary tab list in - `frontend-modern/src/AppLayout.tsx` must keep the AI launcher chrome - intact: they extend `aiChatStore`-aware route shells rather than mounting - bespoke layouts that suppress the Assistant launcher, Patrol entry, or - the AI keyboard-shortcut handlers. New per-platform sub-routes inherit - the shared AI-aware chrome by virtue of routing through `AppLayout`, and - must not introduce a parallel chat surface, launcher button, or model - picker on the platform page itself; cross-platform AI guidance stays - routed through Assistant and Patrol. The Patrol utility destination in - `AppLayout`'s tab lists shares the nav identity-stability contract owned - by `frontend-primitives` (`stableNavTabs.ts`): a websocket state frame - with structurally unchanged tab content must not recreate the Patrol - button DOM or its attention-count badge element. Patrol and the peer + `frontend-modern/src/AppLayout.tsx` must keep the Patrol entry chrome + intact: they extend `aiAvailabilityStore`-aware route shells rather than + mounting bespoke layouts that suppress the Patrol entry or its keyboard + shortcut. New per-platform sub-routes inherit the shared chrome by virtue + of routing through `AppLayout`, and must not introduce a chat surface, + launcher button, or model picker on the platform page itself; + cross-platform AI work stays routed through Patrol. The Patrol utility + destination in `AppLayout`'s tab lists shares the nav identity-stability + contract owned by `frontend-primitives` (`stableNavTabs.ts`): a websocket + state frame with structurally unchanged tab content must not recreate the + Patrol button DOM or its attention-count badge element. Patrol and the peer utility destinations commit navigation without awaiting best-effort module - warming, so lazy-route work cannot make the shared AI-aware shell drop or - defer a tap. + warming, so lazy-route work cannot make the shared shell drop or defer a + tap. The primary-nav demotion of Infrastructure / Workloads / Storage / - Recovery does not change Patrol or Assistant addressability: both - surfaces remain reachable through the utility tab strip in - `AppLayout.tsx` and via the canonical Patrol path, and platform pages - must not replicate Patrol findings, Assistant prompts, or AI launcher - affordances inside their own chrome. + Recovery does not change Patrol addressability: Patrol remains reachable + through the utility tab strip in `AppLayout.tsx` and via the canonical + Patrol path, and platform pages must not replicate Patrol findings, AI + prompts, or launcher affordances inside their own chrome. The vSphere Networks sub-route follows the same AI runtime boundary as the - vSphere overview, datastore, health, and activity routes. Network rows may - seed Assistant or Patrol context only as shared `network` unified-resource - references read through `/api/resources*` and the common handoff payloads; - the VMware page must not introduce VMware-local AI prompts, a provider - model picker, or a vSphere-specific chat/runtime route just because - networks are now rendered as a first-class API-native table. + vSphere overview, datastore, health, and activity routes. Network rows + reach Patrol only as shared `network` unified-resource references read + through `/api/resources*`; the VMware page must not introduce VMware-local + AI prompts, a provider model picker, or a vSphere-specific chat/runtime + route just because networks are now rendered as a first-class API-native + table. The frontend-primitives-owned Machines surface follows the same AI runtime boundary while retaining the internal `standalone` route/id contract: route registration and primary-tab chrome may expose the support-manifest `agent` - platform plus agentless availability endpoints, but the page must not add an - agent-specific Assistant prompt surface, AI launcher, model picker, or - browser-owned model context. Machine rows may seed Assistant or Patrol only - through the shared unified resource handoff contracts. Its IA, primary - shell position, and landing eligibility belong to `frontend-primitives`; - those changes must not alter Patrol or Assistant utility-tab ordering, - launcher visibility, or shared keyboard handling in + platform plus agentless availability endpoints, but the page must not add + an agent-specific AI prompt surface, launcher, model picker, or + browser-owned model context. Machine rows reach Patrol only through the + shared unified resource contracts. Its IA, primary shell position, and + landing eligibility belong to `frontend-primitives`; those changes must not + alter Patrol utility-tab ordering or shared keyboard handling in `frontend-modern/src/AppLayout.tsx`. ### Platform navigation admission @@ -4561,11 +3249,12 @@ resolve canonical/source IDs and unique aliases before collection, reject but frontend comments, API denial messages, runtime logs, status labels, CLI help, and commercial prompts that describe the capability must use safe remediation or remediation wording. -12. Keep AI control-level presentation runtime-owned rather than tier-owned. - `frontend-modern/src/utils/aiControlLevelPresentation.ts` and +12. Keep AI runtime controls runtime-owned rather than tier-owned. `frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx` - may describe approval posture, but must not add Pro-badge suffixes or - local commercial tracking around those runtime controls. + owns the cost budget and request timeout, and must not add Pro-badge + suffixes or local commercial tracking around those runtime controls. The + Assistant chat control level has no browser surface since the chat window + was retired; it remains an API setting for Pulse Mobile chat. 13. Keep Assistant control and Patrol paid runtime settings entitlement-effective at every runtime boundary. Stored config may preserve autonomous, Patrol auto-remediation, and alert-triggered analysis preferences so they come @@ -4843,55 +3532,24 @@ resolve canonical/source IDs and unique aliases before collection, reject approval states may set it, but approved, denied, rejected, executing, completed, failed, expired, or otherwise historical action references must remain action context without being relabeled as requiring approval. - When the Assistant drawer restores any session from that `handoff_summary`, - it must restore the scoped approval-required disclosure (the drawer-only - `autonomousMode:false` context flag) as well as the safe visible briefing, - even when the summary is context-only and has no queued action. The next - chat turn carries no execution-mode field; `/api/ai/chat` runs it - approval-required server-side. The visible badge/action copy must still reflect the - actual last-known action state or Patrol assessment context instead of - inventing a pending approval or restoring a Patrol recommendation. That - restoration is success-bound: if the underlying session message load fails, - the drawer must leave the current context untouched instead of applying - summary-derived Patrol or approval state for a session the operator is not - actually viewing. Before `/api/ai/sessions` returns summaries with stored handoff action references, the chat runtime must refresh their safe approval/action status from the canonical approval store and action-audit store. Session listing is an operator decision surface, so it must not leave stale pending/approval - labels in the drawer after the governed action moved on, and it must still - omit raw commands, preflight bodies, and execution output. That refresh is - bounded, not exhaustive: the list is newest-first and only the - `maxSessionHandoffRefreshPerList` (20) most recent action-carrying - sessions are re-checked per list call, which covers every summary the - recent-sessions surface and picker top present; deeper history self-heals - because the send path refreshes the active session's handoff actions on - load before the turn executes. - The Assistant drawer must also fetch that current session list before - opening the session picker instead of presenting mount-time cached - summaries as the operator's decision surface. For restored Patrol - assessment or finding sessions, that picker must present only the safe - handoff kind, source, resource/action counts, and approval/action status - from `handoff_summary`; it must not restore Patrol recommended - next-step title/detail/action labels, route-owned hrefs, or disabled-action - reasons as visible or hidden context. - Browser-originated `handoff_context`, `handoff_resources`, and + labels after the governed action moved on, and it must still omit raw + commands, preflight bodies, and execution output. That refresh is bounded, + not exhaustive: the list is newest-first and only the + `maxSessionHandoffRefreshPerList` (20) most recent action-carrying sessions + are re-checked per list call; deeper history self-heals because the send + path refreshes the active session's handoff actions on load before the + turn executes. + Client-originated `handoff_context`, `handoff_resources`, and `handoff_actions` plus safe `handoff_metadata` are one-shot request seeds for the first successful chat turn. Patrol next-step titles, details, labels, and route-owned hrefs do not belong in `handoff_metadata`, and model-context text parsing must not resurrect them as a legacy fallback. - After that send succeeds, the drawer - must clear those request payloads while preserving the safe visible - briefing and the drawer's - approval-required disclosure; later turns must rely on backend-owned session - model-context hydration and current canonical stores instead of resending - stale browser handoff payloads. Patrol approval-row Assistant entries are - still Patrol finding handoffs, not local prompt-only shortcuts: live - approval rows, expired action-artifact rows, and missing-detail queued-fix - recovery rows must route through the shared Patrol finding handoff builder - so the backend receives the same bounded model-only finding context, - resource reference, and safe action reference posture that the main finding - handoff uses. + Later turns rely on backend-owned session model-context hydration and + current canonical stores rather than resent handoff payloads. Proposed-fix command text must stay out of both the persisted chat message and the model-only handoff context, and command payloads remain approval-context data, not conversational copy. @@ -4900,7 +3558,7 @@ resolve canonical/source IDs and unique aliases before collection, reject caller JSON (a stale client's `autonomous_mode:true` included) is dropped at decode, and `HandleChat` passes `AutonomousMode:false` into the chat service whether or not the request carries a `finding_id` or model-only briefing, - resource, or action context. The browser sends no execution-mode field. + resource, or action context. Clients send no execution-mode field. That clamp belongs to the backend/API execution boundary, does not mutate the user's persistent AI control setting, and prevents product-originated Patrol action context from @@ -4914,22 +3572,6 @@ resolve canonical/source IDs and unique aliases before collection, reject effective control level to controlled for that request only, so even policy-allowed diagnostic commands require operator approval in public chat without mutating the user's saved setting. - The Assistant drawer may also render an attached context briefing for that - handoff, but the briefing is runtime context visibility only: it must not - mutate chat control settings, execute tools, or reveal raw command payloads. - Resource-drawer Assistant entries use that same briefing path with - `handoff_metadata.kind=resource_context`, a structured `handoff_resources` - reference, and the drawer-only `autonomousMode:false` approval disclosure; - they must not prefill or submit a - browser-authored prompt, and any rich resource facts must be hydrated by the - backend context-pack path rather than reconstructed in the browser. - Safe route-owned briefing actions may render as app links when the handoff - includes an `actionHref`, but those links are navigation guidance only and - do not grant tool execution or approval authority. - Scoped handoffs must present the approval-required boundary - through compact source-named drawer state and the effective control label, - so Patrol approval/finding handoffs and alert-investigation handoffs are - named by their source rather than as generic dashboard briefs. ### Operational attention projection boundary @@ -5098,23 +3740,20 @@ diagnostic evidence only; the shared manifest remains the error-vocabulary owner, and canonical Actions remains the sole mutation plane for both human and automatic policy execution. -First-session assistant discoverability is now a contract concern. Successful -first-time provider setup (the Provider & Models setup modal) must open the -Assistant drawer and refresh the session `assistantEnabled` capability in -place, so the launcher and per-surface handoff buttons appear without a page -reload; the enable/save toasts point at the Assistant rather than back at -settings. The Assistant's empty transcript owns a plain-language welcome plus -static suggested prompts (`ASSISTANT_SUGGESTED_PROMPTS` in `ChatMessages.tsx`) -that dispatch as real chat turns; server-driven workflow starters remain a -separate composer affordance. Patrol static readiness treats the blessed -Ollama Patrol model (`config.OllamaSuggestedPatrolModel`) as Ready — the -recommended free path must not report itself degraded — while other Ollama -models keep the unverified-tool-support warning, which now names the selected -model instead of instructing the operator to pull the model they already -selected. `pulse_summarize` fleet-mode argument errors instruct the model to -enumerate resources itself rather than interrogate the operator for resource -IDs; assistant-facing tool errors must prefer self-recovery guidance over -operator questions. +First-session Patrol discoverability is a contract concern. Successful +first-time provider setup (the Provider & Models setup modal) must refresh the +session `assistantEnabled` capability in place through +`frontend-modern/src/stores/aiAvailability.ts`, so the Patrol tab and alert +Investigate actions appear without a page reload; the enable toast points at +Patrol. Patrol static readiness treats the blessed Ollama Patrol model +(`config.OllamaSuggestedPatrolModel`) as Ready — the recommended free path +must not report itself degraded — while other Ollama models keep the +unverified-tool-support warning, which names the selected model instead of +instructing the operator to pull the model they already selected. +`pulse_summarize` fleet-mode argument errors instruct the model to enumerate +resources itself rather than interrogate the operator for resource IDs; +assistant-facing tool errors must prefer self-recovery guidance over operator +questions. Automatic Watch detection now carries a declining 7,000-token run allowance: normal structured turns receive at most 2,048 output tokens, terminal summaries @@ -5218,7 +3857,7 @@ new Assistant provider-repair actions. Primary platform route memory in `frontend-modern/src/AppLayout.tsx` may preserve platform-local query state such as workload filters, but that memory -remains navigation chrome only. It must not fork Assistant drawer state, Patrol +remains navigation chrome only. It must not fork AI availability state, Patrol utility route state, prompt context, resource reads, commercial posture, or cross-platform query parameters. @@ -5268,7 +3907,7 @@ spend for the session summed from the operator cost ledger turns, compaction, and title calls, all of which stamp `session_id` on their `cost.UsageEvent`. The figure is omitted whenever any of the session's events prices against an unknown model, and free local models -(Ollama) price known-at-zero, so the drawer's last-turn summary appends +(Ollama) price known-at-zero, so a client's last-turn summary may append "$0.12 session" only when the whole session is honestly priceable and non-zero. The summary never invents a client-side pricing table; the backend ledger is the only cost authority. @@ -5366,62 +4005,6 @@ constructed at service start, so anything that must stay fresh per turn carries no PII and is therefore safe on cloud-routed turns regardless of the operational-context sharing opt-in. -The streaming transcript list reconciles messages by `id` rather than rendering -the raw message array by reference. `useChat` rebuilds its message array -immutably on every stream event (each content chunk, workflow-status change, and -tool update spreads a brand-new message object), so a reference-keyed `` -would tear down and recreate the whole `MessageItem` on every event — the -visible flashing, rows popping in and out, and transcript jumping up and down -during a turn. `ChatMessages` (`frontend-modern/src/components/AI/Chat/ChatMessages.tsx`) -reconciles the incoming array into a keyed `solid-js/store` mirror so each -message keeps a stable identity across updates; `MessageItem` already reads every -field through accessors, so only the genuinely changed text/rows update in place. -This keeps the live transcript stable the way OpenCode's timeline is. - -The same stability rule applies one level down, to the per-message -stream-event/tool-row list inside `MessageItem`. `groupStreamEventsForDisplay` -remaps blocks to fresh objects on every tick and `StreamDisplayEvent` carries no -id, but the grouped list is strictly append-ordered (the grouper only pushes new -blocks or mutates the open content/thinking block in place — it never inserts -mid-list or reorders). It therefore renders through `` (positional -keying), not a reference-keyed ``: each row keeps its DOM node across -event-object rebuilds at a stable position, so the streaming answer block and -completed tool rows update in place instead of re-mounting (and re-parsing -markdown) on every content delta. Positional keying is correct here precisely -because the list is append-only; a list that could reorder or key by identity -must not use ``. - -The streaming answer block itself renders by morphing the DOM, not by replacing -`innerHTML` on every tick. `renderMarkdown` re-parses the full (DOMPurify- -sanitized) answer on each paced reveal; assigning that to `innerHTML` rebuilds -the entire prose subtree, so a multi-paragraph or list/table answer flickers and -reflows every earlier line as it streams. `AssistantMarkdownBlock` instead feeds -the sanitized HTML to `morphMarkdownInto` -(`frontend-modern/src/components/AI/Chat/markdownMorph.ts`), which reconciles old -and new trees in place: structurally-identical nodes are left untouched, same-tag -nodes are morphed (recursing into children so a growing `
    `/`` keeps -its earlier `
  1. `/`
  2. ` and only the last one updates), and the growing tail -block updates its text node rather than being rebuilt. The morph is a rendering -optimization only — `renderMarkdown` remains the sole sanitization gate and the -morph must never be fed unsanitized HTML. - -Live workflow status is footer-owned, not a transcript artifact (the OpenCode -restraint model — see the canonical statement in Extension Points, which -supersedes the older per-row transcript-status rules in this contract). Earlier -Pulse rendered "Preparing context / Reading inventory / Counting / Waiting for -assistant / retrying" both as transcript rows AND in the activity dock, so the -chat narrated every internal step into the timeline. Now `MessageItem` never -renders workflow status — neither the per-event `role="status"` row -(`shouldRenderWorkflowStatusEvent` is `false`) nor the early-phase header chip -(`shouldShowHeaderWorkflowStatus` is `false`). The transcript carries only -durable artifacts (reasoning, tool calls, model-route rows, the answer). The -single live "assistant is working" indicator is the activity dock above the -composer, which shows a spinner + the current status + the model route + Stop. -The dock is gated on `assistantTurnActive` (loading OR a streaming assistant -message) rather than `chat.isLoading()` so it persists for the whole turn: -`isLoading` flips false at visible-turn-complete, which previously made the dock -flash its status for a frame and vanish. - Prompt wording must not gate tool availability at all. The greeting/meta text-only classifier (`assistantPromptLooksConversational`) is removed along with the rest of the `assistantToolScopeForPrompt` keyword router: even "hi" @@ -5485,82 +4068,12 @@ The earlier form ("I completed N successful check(s) using call_27f0f389…, … automatic summary. Latest successful result snippet: {…raw JSON…}") is forbidden: provider call ids and raw tool JSON are not operator-facing answer content. -Assistant slash-command availability is part of the command runtime contract, -not only visual polish. The OpenCode reference at fetched `origin/dev` commit -`c495635` filters prompt slash commands through the registered command catalog -and omits disabled builtin commands from the prompt popover in -`packages/app/src/components/prompt-input.tsx` (slash command list creation, -lines 677-699), while command metadata owns `disabled` and `slash` in -`packages/app/src/context/command.tsx`. Pulse adapts that by deriving command -availability from the same predicates as the drawer toolbar: prompt slash -autocomplete hides unavailable local commands, command help may show them -disabled with a reason, and manual slash submissions respect the same -availability before running local session actions. -Assistant provider-route recovery must read as explicit operator action, not -automatic fallback. The OpenCode reference at fetched `origin/dev` commit -`914a643` retries the selected provider/model route through -`packages/opencode/src/session/retry.ts` and -`packages/opencode/src/session/processor.ts` by passing -`input.model.providerID` into `SessionRetry.policy`, renders retry state in -`packages/ui/src/components/session-retry.tsx`, and records provider/model -changes as explicit `ModelSwitched` session events in -`packages/core/src/session/event.ts` plus `packages/opencode/src/session/prompt.ts`. -Pulse adapts that by keeping same-route retry visible, ignoring obsolete -provider route-switch metadata without changing the selected route, and labeling -failed-turn or readiness recovery buttons as explicit route/model-route choices -instead of implying automatic route adoption. - -Assistant model-catalog failure is selector-local state, not a drawer -initialization failure. The OpenCode reference at fetched `dev` commit -`3867fa2bad0e644166e360e2e99cfe426fe71105` -`packages/opencode/src/cli/error.ts` lines 58-69 formats missing model/catalog -state as an operator-facing model-selection problem with a list-models hint, -while `packages/opencode/src/session/llm.ts` lines 96-104 resolves the -selected provider/model route independently for the stream. Pulse adapts that -by letting Assistant sessions, settings, route health, and the composer finish -opening when `/api/ai/models` fails; the catalog error stays attached to the -model selector and may be refreshed explicitly, but startup must not log or -render a broad Assistant initialization failure, and it must not replace the -selected model route. - -Assistant completed-turn chrome is route-owned summary, not raw usage output. -The OpenCode reference at fetched `dev` commit -`3867fa2bad0e644166e360e2e99cfe426fe71105` imports `turnSummaryCommit` in -`packages/opencode/src/cli/cmd/run/scrollback.surface.ts` lines 19-20, while -`packages/opencode/src/cli/cmd/run/turn-summary.ts` lines 5-46 emits a final -system summary with agent, model, and duration. Pulse adapts that by deriving -the latest completed, non-streaming assistant turn and surfacing its effective -provider/model route, duration, and backend-proven token total in low-priority -composer chrome. Input/output usage remains title/accessibility detail only, -missing provider usage does not hide route and duration, and active streaming -turns never appear as the completed-turn summary. - -Assistant tool activity now follows an OpenCode-referenced chronological row -model where appropriate for Pulse. Consecutive context/read/query tools render -as visible transcript rows in arrival order instead of being replaced by a -grouped context footer, while command previews, inputs, progress, and large -outputs remain contained inside each tool row. This keeps the user-facing stream -feeling active without dumping large command output into the default answer. -Fast tool completions must also stay visibly live long enough to be perceived: -the frontend stream reducer stamps sub-420ms successful tool completions with a -transient settle deadline, and the row renders that deadline as a running state -even if the turn's `done` event has already arrived. The referenced OpenCode -source at fetched `dev` commit `e82542b8023a8374f29c23b70ec019c8f256354e` -implements the same user-visible principle in -`packages/opencode/src/cli/cmd/run/session-data.ts` by emitting a `start` commit -for running tools and a later completed/error commit instead of only surfacing a -batched terminal transcript. Pulse adapts that as an in-memory UI settle window -because Pulse transcripts persist completed tool facts, not OpenCode scrollback -commit phases. -The 2026-06-08 live-tool-start slice rechecked OpenCode source at fetched `dev` -commit `3867fa2bad0e644166e360e2e99cfe426fe71105`: -`packages/opencode/src/session/processor.ts` creates the visible tool part in -`ensureToolCall` (lines 299-349) and moves that same part to `running` on -`tool-call` (lines 507-521). Pulse adapts that by making `ToolStartData` -self-describing live activity: `tool_start` must carry a running phase when -execution is visible, and the frontend must render a new start row as running -immediately even if `tool_progress` arrives in a later paint. +`tool_start` carries a running phase when execution is visible, so a client can +render a new start row as running immediately even if `tool_progress` arrives +later. This adapts OpenCode `packages/opencode/src/session/processor.ts` +(`ensureToolCall`, dev commit `3867fa2bad0e644166e360e2e99cfe426fe71105`), +which moves the visible tool part to `running` on `tool-call`. Assistant provider retries are a first-class visible workflow state, not a hidden server log. The referenced OpenCode source at fetched `dev` commit @@ -5574,108 +4087,8 @@ hidden server log. The referenced OpenCode source at fetched `dev` commit that contract at the owning stream boundary: `WorkflowStateData` carries `attempt`, `max_attempts`, and `retry_after_ms`, and `AgenticLoop` emits `provider_retry` before sleeping between transient pre-output provider -attempts. The frontend active-turn footer and live transcript workflow row -render that same typed workflow state immediately as compact attempt/backoff -progress, counting down `retry_after_ms` from `started_at` while the turn -remains active, so the user sees Pulse moving through a retry instead of -staring at an obsolete provider-wait message. Retry workflow states are not -held behind workflow-history pacing. - -Assistant workflow progress is also a live typed activity row while a turn is -in flight, not only hidden footer state. The referenced OpenCode source at -fetched `dev` commit `7ae856a9e97130f664f6f11fa5871a2795de9902` stores -session status separately from message parts in -`packages/opencode/src/cli/cmd/tui/context/sync.tsx` (`session_status`, -`session.status` event handling) and renders a session from live messages, -permissions, questions, and running tool parts in -`packages/opencode/src/cli/cmd/tui/routes/session/index.tsx` (`Session`, -`messages`, `permissions`, `questions`, foreground `ToolPart` selection, and -`session.status` handling). Pulse adapts that source pattern with a transient -frontend `workflow_status` display event: each incoming backend -`workflow_state` replaces the prior workflow row, the active footer reads the -same typed status, and visible assistant content, reasoning, tool, -approval/question, terminal `done`, and terminal `error` events clear that row. -The frontend now also keeps a bounded live-only `workflowStatusHistory` for the -active assistant message so the reducer preserves state continuity while -backend preparation, context, and provider-start labels are being replaced. The -shared workflow-status presentation helper owns both the transcript row and the -active-turn composer footer so the two live surfaces show the latest canonical -workflow state immediately. Retry/backoff and stream-idle liveness states cut -through pacing because they are current route health, not neutral setup motion. -The transcript and footer therefore show current motion while the provider is -starting, retrying, or reasoning, but completed answers do not retain stale -internal-progress prose. -Stream-idle heartbeats are part of that same visible workflow state. When the -frontend knows the selected provider/model route from the backend workflow event -or the streaming assistant message, the idle heartbeat must render as -route-specific liveness such as `OpenRouter is still working; waiting for more -response data.` instead of reverting to generic Assistant waiting copy. -Prompt dispatch itself is also a visible live state: before any backend -workflow event returns, the frontend seeds the active assistant turn with a -local `request_send` / `Sending prompt.` workflow row after any selected-model -row. That row is explicitly transient: it is the current live state but does -not enter the paced backend workflow history, the first backend workflow state -may replace it, and content, thinking, tool, approval, question, model-route, -done, or error evidence removes it from the durable transcript. This adapts the -referenced OpenCode source at fetched `dev` commit -`e82542b8023a8374f29c23b70ec019c8f256354e`, where -`packages/opencode/src/cli/cmd/run/runtime.queue.ts` emits `turn.send` with -`sending prompt` at lines 174-184, `packages/opencode/src/cli/cmd/run/footer.ts` -maps `turn.send` and `turn.wait` to live footer status at lines 133-145, and -`packages/opencode/src/cli/cmd/run/stream.transport.ts` promotes quiet -post-send periods to `waiting for assistant` at lines 1350-1357. Pulse keeps -that send/wait distinction in the live chat turn by promoting an unreplaced -local `request_send` status to local `request_wait` / `Waiting for assistant` -when the browser API client receives an open SSE response but before any parsed -backend-visible SSE activity arrives, while preserving the Pulse transcript -rule that only backend workflow, route, content, and tool evidence remain once -real activity exists. - -Assistant drawer shell status follows the same OpenCode-referenced separation -between prompt-adjacent operational status and transcript content. The -referenced OpenCode source at fetched `dev` commit -`e82542b8023a8374f29c23b70ec019c8f256354e` renders agent/model metadata beneath -the prompt and active/retry/interrupt state in the prompt footer in -`packages/tui/src/component/prompt/index.tsx`, with broader system status kept -in `packages/tui/src/routes/session/footer.tsx`. Pulse adapts that pattern for -the web drawer by keeping model route, recent-route cycling, control mode, -last-turn usage, active workflow progress, queued follow-ups, route-recovery -notices, and the autonomous control warning in the input-adjacent -composer/status rail. -Those items stay visible and actionable, but they do not compete with the -transcript as separate top-of-drawer banners unless they are provider readiness -or scoped handoff context surfaces with their own governed content. -Queued follow-up pressure is part of that active-turn state: the frontend -derives the count from queued user transcript turns, appends it to the primary -active Assistant headline while a turn is running, and keeps the separate -queued-follow-up row actionable for reorder/cancel operations. -That suffix must compose with live workflow pacing rather than disabling it: -when backend workflow states arrive in a burst, the active headline still walks -through the paced status sequence while retaining the queued follow-up count. -This preserves the OpenCode-referenced live-part feel from -`packages/tui/src/routes/session/index.tsx` lines 1492-1503 and the running -tool metadata updates in `packages/opencode/src/session/tools.ts` lines 54-63, -while adapting the interaction to Pulse's single active-turn plus queued -follow-up safety model. - -Assistant chat actions follow the same OpenCode-referenced footer principle: -actions are reachable from prompt-adjacent chrome, not only from a hidden slash -draft. The referenced OpenCode source at fetched `dev` commit -`e82542b8023a8374f29c23b70ec019c8f256354e` builds the direct command surface in -`packages/opencode/src/cli/cmd/run/footer.command.tsx` (`RunCommandMenuBody` -with suggested actions, slash commands, and search) and keeps the footer command -entry discoverable from `packages/opencode/src/cli/cmd/run/footer.view.tsx`. -Pulse adapts that pattern for the web drawer by exposing the existing Assistant -commands dialog from the composer chrome and making that dialog searchable and -keyboard-selectable. The button, slash-triggered help, dialog search, and -keyboard selection must use the same `ASSISTANT_SLASH_COMMANDS` registry, -`filterAssistantSlashCommands`, `AssistantCommandHelpDialog`, and -`executeSlashCommand` path as `/help` and `/commands`; they must not introduce -a second command registry or a separate provider-bound prompt path. The command -dialog search box must compose the frontend-primitives `SearchField` so the -shared search-control registry owns clear-button behavior, input chrome, -keyboard forwarding, and native-search-input drift prevention while AI runtime -keeps ownership of command filtering and execution semantics. +attempts, so a client can show attempt/backoff progress instead of an obsolete +provider-wait message. Primary nav moved to governed platform/runtime destinations on 2026-05-16 and was clarified on 2026-05-25 and 2026-06-25 through `frontend-modern/src/App.tsx` and @@ -5697,14 +4110,13 @@ was retired alongside its page wrapper, and `/workloads`, `/storage`, top-level routes. Primary platform tab `settingsRoute` handoffs must also point to the canonical `/settings/infrastructure` workspace rather than retired settings aliases such as `/settings/workloads/docker` or nested -`/settings/infrastructure/platforms/*` paths. The AI Chat launcher, Patrol surfaces, and -`AssistantHandoffPayload` deep links must use canonical platform or runtime +`/settings/infrastructure/platforms/*` paths. Patrol surfaces and their deep links must use canonical platform or runtime routes (`/proxmox/overview`, `/proxmox/storage`, `/kubernetes/workloads`, etc.) rather than reviving retired Infrastructure or aggregate workspace paths; -adding a platform tab through the same shell files must not fork Assistant or -Patrol shell state or smuggle in AI-owned platform reads. +adding a platform tab through the same shell files must not fork AI availability +or Patrol shell state or smuggle in AI-owned platform reads. Post-auth `/` and `/login` resolution follows the frontend-primitives-owned -provider-first platform landing contract, so the assistant-capable shell never +provider-first platform landing contract, so the authenticated shell never overrides Machines-surface eligibility or revives legacy Infrastructure as the default estate surface. The user-facing Machines label is an app-shell presentation label for the existing `standalone` route/id and must not create a @@ -6090,103 +4502,76 @@ That same runtime-backed contract now governs AI settings enablement too: unconfigured installs open provider setup, while stale managed-credit or activation-required states are treated as compatibility metadata rather than a direct-enable path. -That same AI/runtime boundary now also owns the server-side assistant -availability fact used by the app shell. `internal/api/ai_handlers.go`, +That same AI/runtime boundary now also owns the server-side AI availability +fact used by the app shell. `internal/api/ai_handlers.go`, `internal/api/security_status_capabilities.go`, and `internal/api/router_routes_auth_security.go` must expose one canonical `/api/security/status.sessionCapabilities.assistantEnabled` signal for the -closed assistant affordance, so unrelated shells do not probe -`/api/settings/ai` or `/api/ai/sessions` during ordinary route bootstrap just -to decide whether the assistant drawer may be opened. +shell's AI entry points, so unrelated shells do not probe `/api/settings/ai` +or `/api/ai/sessions` during ordinary route bootstrap just to decide whether +Patrol entry points render. That same frontend runtime boundary now also owns the shared AI read model for AI-owned surfaces. `frontend-modern/src/stores/aiRuntimeState.ts` is the canonical frontend owner for shared `/api/settings/ai` and `/api/ai/models` -reads used by chat, Patrol, and AI usage surfaces, while +reads used by Patrol and AI usage surfaces, while `frontend-modern/src/components/Settings/useAISettingsState.ts` remains the write-side settings owner. AI-owned surfaces must not fork their own mount-time settings/model fetch loops once this store exists. -The assistant drawer/session shell is a separate shared boundary: -`frontend-modern/src/stores/aiChat.ts` owns open state, focused-input handoff, -context accumulation, and org-switch clearing for the assistant drawer, while +The AI availability fact is a separate shared boundary: +`frontend-modern/src/stores/aiAvailability.ts` holds the session +`assistantEnabled` capability for the shell, while `frontend-modern/src/stores/aiRuntimeState.ts` owns the shared backend-backed -settings and model catalog reads. AI runtime consumers must not move drawer -shell state into page-local signals or teach `aiChat.ts` to bootstrap its own +settings and model catalog reads. Consumers must not move that fact into +page-local signals or teach `aiAvailability.ts` to bootstrap its own `/api/settings/ai` or `/api/ai/models` reads. -That same drawer boundary owns responsive presentation too. The canonical -assistant drawer may dock and push the authenticated shell only when the -viewport is wide enough to preserve a usable primary operating surface; once -the available viewport drops below that shell threshold, the drawer must -become an overlay owned by `frontend-modern/src/components/AI/Chat/index.tsx` -instead of compressing Infrastructure, Workloads, Storage, or other primary -runtime pages into an unusable narrow column or forking page-local layout -exceptions. -On that mobile overlay path, both the drawer and its backdrop must stop above -the mobile navigation safe-area boundary so closing or changing routes remains -available; the full-height desktop drawer boundary resumes only with the -desktop shell. -The closed assistant launcher follows the same shared-shell rule. While the -mobile navigation shell is active, `frontend-modern/src/AppLayout.tsx` must -present the launcher in the authenticated header flow, where it cannot cover -the operating surface or the mobile nav, instead of restoring the desktop -right-edge rail at an earlier breakpoint. The edge-mounted launcher is only -valid at the desktop shell breakpoint where the primary navigation and page -chrome are also desktop-mode. Non-AI shell notices may coexist in `frontend-modern/src/AppLayout.tsx`, but they must remain presentation-only. Prerelease banners, billing callouts, or -other header-adjacent notices must not fork assistant open state, gate on AI -runtime fetches, or move assistant availability logic out of -`frontend-modern/src/stores/aiChat.ts` and `frontend-modern/src/useAppRuntimeState.ts` -just because they share the same authenticated shell. The remaining -prerelease-shell treatment is the compact `Preview` badge on rc-channel -builds; `frontend-modern/src/AppLayout.tsx` must not revive a standalone -release-candidate banner, release-notes CTA, or feedback CTA that starts -participating in assistant-shell state or modal ownership. +other header-adjacent notices must not gate on AI runtime fetches or move AI +availability logic out of `frontend-modern/src/stores/aiAvailability.ts` and +`frontend-modern/src/useAppRuntimeState.ts` just because they share the same +authenticated shell. The remaining prerelease-shell treatment is the compact +`Preview` badge on rc-channel builds; `frontend-modern/src/AppLayout.tsx` must +not revive a standalone release-candidate banner, release-notes CTA, or +feedback CTA that starts participating in AI availability state or modal +ownership. The retired monitored-system capacity banner follows the same shell rule: `frontend-modern/src/App.tsx` must not reintroduce app-shell commercial volume warnings just because settings or support surfaces still expose -monitored-system grouping data. Assistant state and shell notices stay +monitored-system grouping data. AI availability and shell notices stay independent from retired infrastructure-volume commerce. The global update progress watcher in `frontend-modern/src/App.tsx` is -likewise server-updater shell chrome, not assistant surface: its in-progress +likewise server-updater shell chrome, not an AI surface: its in-progress stage vocabulary mirrors the backend updater pipeline (including the -`restoring` stage emitted by update rollback), and assistant state, drawer -ownership, and AI runtime surfaces must not key off those update stages. +`restoring` stage emitted by update rollback), and AI availability and AI +runtime surfaces must not key off those update stages. On authenticated installations that watcher also mounts only when the served `settingsCapabilities.systemSettingsRead` value mirrors the update-status route's `RequireAdmin` plus `settings:read` gate. Auth-free installations keep the watcher because those routes remain reachable there. Neither branch may -become an Assistant capability, provider-readiness signal, or AI bootstrap +become an AI availability signal, provider-readiness signal, or AI bootstrap probe. -That same shared shell boundary must respect blocking modal ownership. -`frontend-modern/src/App.tsx` and `frontend-modern/src/AppLayout.tsx` may use -the shared dialog runtime to hide the closed assistant launcher and close the -drawer while a blocking shared dialog is open, but they must not leave Pulse -Assistant interactive behind a modal or fork a second assistant-open state -model to do it. That same shared shell rule applies when presentation policy suppresses hosted organization chrome: `frontend-modern/src/App.tsx` and `frontend-modern/src/AppLayout.tsx` may hide org switchers or demo-only org -labels, but they must not couple assistant visibility, session reset, or -drawer-open behavior to that organization presentation state. +labels, but they must not couple AI availability or Patrol entry visibility +to that organization presentation state. That same shell boundary also owns demo-only support-surface suppression: Pulse no longer exposes Operations as a top-level route. Demo-only support -surfaces now hide inside the shared Settings navigation instead, and assistant -availability plus reset behavior must stay independent of that settings-nav -presentation choice. +surfaces now hide inside the shared Settings navigation instead, and AI +availability must stay independent of that settings-nav presentation choice. Authenticated `/login` recovery belongs to that same route shell boundary: once login succeeds, `frontend-modern/src/App.tsx` must resolve `/login` through the canonical provider-first post-auth landing route instead of leaving -the assistant-capable authenticated shell stranded on a route that only exists +the authenticated shell stranded on a route that only exists for logged-out presentation. App-shell route preloading may include the Patrol route module, but it must -remain module-only. It must not prefetch AI settings, model state, findings, -chat sessions, or assistant context while the drawer is closed. +remain module-only. It must not prefetch AI settings, model state, findings, or +chat sessions. `docs/release-control/v6/internal/subsystems/registry.json` must therefore keep -`frontend-modern/src/stores/aiRuntimeState.ts` and -`frontend-modern/src/components/AI/Chat/` on the explicit AI runtime proof -route, and keep `frontend-modern/src/stores/aiChat.ts` on the shared -AI-runtime/frontend-primitives proof boundary instead of leaving the chat shell -or assistant drawer state unowned. +`frontend-modern/src/stores/aiRuntimeState.ts` on the explicit AI runtime proof +route, and keep `frontend-modern/src/stores/aiAvailability.ts` on the shared +AI-runtime/frontend-primitives proof boundary instead of leaving the AI +availability fact unowned. That same settings/runtime boundary now also governs BYOK first-run setup: `frontend-modern/src/components/Settings/useAISettingsState.ts` may send only provider credentials or base URLs when the operator connects a provider, and @@ -6226,48 +4611,26 @@ empty-state, and range-button presentation contract. Future cost-surface work must extend those owners instead of reintroducing inline AI usage copy or dashboard-local segmented-button styling. The same runtime boundary also owns the shared AI semantic presentation -helpers used across chat, settings, and usage surfaces. -`frontend-modern/src/utils/aiProviderPresentation.ts`, -`frontend-modern/src/utils/aiProviderHealthPresentation.ts`, -`frontend-modern/src/utils/aiControlLevelPresentation.ts`, -`frontend-modern/src/utils/aiChatPresentation.ts` are the canonical owners -for provider naming, provider health labels, control-level semantics, -chat drawer title/subtitle, launcher title/aria copy, session-menu labeling, -discovery hint framing, chat/session empty states, assistant message and -question-card labels. -Discovery hint framing must follow the Pulse Intelligence settings IA: use the -simple `Discovery` label, and explain the concrete service context it unlocks, -without promoting workload discovery as another first-class product surface. -Settings and chat surfaces must consume those helpers instead of keeping local -AI wording or model/provider inference branches. -Assistant chat must not render Pulse-authored explore pre-pass cards or -internal workflow-state cards as assistant output. The user-facing stream is -model text, model thinking where supported, model-selected tool calls, governed -approval requests, and model questions; internal runtime telemetry stays out of -the chat transcript. The browser runtime may keep the latest `workflow_state` -message on the in-flight assistant turn only as drawer status text while waiting -for model content, so provider/session progress is visible without turning -runtime telemetry into transcript content. +helpers used across settings, Patrol, and usage surfaces. +`frontend-modern/src/utils/aiProviderPresentation.ts` and +`frontend-modern/src/utils/aiProviderHealthPresentation.ts` are the canonical +owners for provider naming and provider health labels. Settings and Patrol +surfaces must consume those helpers instead of keeping local AI wording or +model/provider inference branches. +The Assistant chat stream must not carry Pulse-authored explore pre-pass +cards or internal workflow-state cards as assistant output. The user-facing +stream is model text, model thinking where supported, model-selected tool +calls, governed approval requests, and model questions; internal runtime +telemetry stays out of the transcript. Cold-start Assistant chat session creation is also stream-owned. Ordinary first messages may call `/api/ai/chat` without a `session_id`; `chat.Service.ExecuteStream` must create or resolve the durable session before provider execution and emit a -first-class `session` SSE event carrying `SessionData{ID: ...}`. The browser -chat runtime binds its active session from that stream event, with `done` and -`question` session identifiers retained only as compatible terminal/interactive -payloads, rather than issuing a separate `/api/ai/sessions` preflight before -the first user message. Explicit session-management actions may still create a -session through the session endpoint. -The Assistant drawer's `New` action is a local blank-conversation reset, not a -backend session creation. It must clear the active transcript, scoped handoff -context, and browser session ID immediately, leave the model selection intact, -and avoid adding empty conversations to the session list; the next submitted -message materializes the durable session through the stream-owned cold-start -contract above. -The drawer must also stay composer-first: when Assistant opens, starts a blank -conversation, or loads an existing session, the textarea is registered with the -shared `aiChatStore` focus owner and focused without requiring an extra click. -Global shell shortcuts may use that store focus boundary, but drawer-local code -must not fork a second input-focus registry. +first-class `session` SSE event carrying `SessionData{ID: ...}`. Clients bind +their active session from that stream event, with `done` and `question` +session identifiers retained only as compatible terminal/interactive payloads, +rather than issuing a separate `/api/ai/sessions` preflight before the first +user message. Explicit session-management actions may still create a session +through the session endpoint. The AI transport files are shared with `api-contracts`, not delegated away to it. `frontend-modern/src/api/ai.ts`, @@ -7181,17 +5544,12 @@ operator paragraph. Future overview or brief surfaces need a governed product owner first, must pass fact-bound structured context from owning Infrastructure, Workloads, Patrol, storage, recovery, and alert summaries, and must not let an unbounded prompt become a route's source of truth. -Future route-to-Assistant handoffs inherit the server-set execution mode -instead of carrying one. When an overview brief opens Assistant, the drawer may -prefill only governed prompt/context data; the submitted chat request carries -no execution-mode field (`/api/ai/chat` runs every request approval-required), -the operator's persistent Assistant control-level setting stays untouched, and -the drawer discloses the approval-required mode through the -`autonomousMode:false` context flag instead of showing the generic Autonomous -warning. -Scoped Assistant handoffs that originate in owned product surfaces may also -send bounded `handoff_context` text, structured `handoff_resources`, and safe -structured `handoff_actions` through `frontend-modern/src/api/aiChat.ts` and +Any future route-to-chat handoff inherits the server-set execution mode +instead of carrying one: the submitted chat request carries no execution-mode +field (`/api/ai/chat` runs every request approval-required), and the +operator's persistent Assistant control-level setting stays untouched. +Scoped Assistant handoffs may also send bounded `handoff_context` text, +structured `handoff_resources`, and safe structured `handoff_actions` through `/api/ai/chat`. That context is model-only session metadata, not saved user-authored message text, and the backend must clamp the exchange to approval-required mode whenever such scoped handoff context, resources, or @@ -7201,8 +5559,8 @@ Patrol finding IDs remain stricter: when canonical authority; the handler may merge only a recognized same-finding Patrol product handoff section as secondary model-only briefing, and it must drop mismatched resource/action references plus raw command payload lines. -Direct alert-investigation runtime handoffs follow the same rule even when -they bypass the chat drawer. `/api/ai/investigate-alert` must set +Direct alert-investigation runtime handoffs follow the same rule. +`/api/ai/investigate-alert` must set `ai.ExecuteRequest.AutonomousMode` to false plus `ai.ExecuteRequest.RequireCommandApproval` to true, and `internal/ai/alert_provider.go` must frame diagnostics as approval-bound @@ -8264,10 +6622,10 @@ authority. When the authenticated shell in `frontend-modern/src/App.tsx` and (a kiosk token carrying `monitoring:read` only), the Patrol utility tab route `/patrol` stays inside the shell's blocked-route redirect whether or not kiosk mode is currently on, and the Escape key must not be usable to leave kiosk into -that chrome. Assistant and Patrol shell state stays owned by -`frontend-modern/src/stores/aiChat.ts` and the canonical Patrol path as before; -this containment is a shell-level authority rule and must not be forked into a -second AI-side scope check. +that chrome. AI availability and Patrol shell state stay owned by +`frontend-modern/src/stores/aiAvailability.ts` and the canonical Patrol path as +before; this containment is a shell-level authority rule and must not be forked +into a second AI-side scope check. Application branding in `frontend-modern/src/AppLayout.tsx` is presentation only. A licensed custom name or logo may replace the default Pulse lockup and @@ -8614,16 +6972,6 @@ real out-of-scope resource, an ambiguous name, a type mismatch, or a name that does not resolve inside the lifecycle scope remains rejected. No fuzzy name or ID matching is permitted at this boundary. -The Assistant overlay clears the mobile navigation bar from the published bar -height rather than a literal of its own. In overlay layout both the panel in -`frontend-modern/src/components/AI/Chat/index.tsx` and its closing backdrop end -at `--pulse-mobile-nav-height`, so the dimmed, click-blocking backdrop reaches -the top edge of the bar with no gap. Reserving more than the bar's real height -leaves page content visible and interactive below the backdrop while the -Assistant is open, which is a modal-containment defect rather than a cosmetic -one. The bar owns that value; see the frontend-primitives contract. Proofs live -in `frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx`. - ### Mobile destination links do not expand AI authority `AppLayout` supplies the same remembered primary route to desktop and mobile @@ -8926,11 +7274,9 @@ is distinct from the earlier Gemini lab matrix and from wider rollout evidence. ### Compact shell Assistant coexistence -The mobile Assistant launcher remains in shared header flow. That header and -its controls may wrap at compact widths so an entitled Organization selector -does not displace session actions or the connection indicator beyond the page. -Keep desktop launcher placement and existing visibility decisions unchanged; -wrapping must not introduce entitlement reads or alter Assistant authority. +The mobile authenticated header may wrap at compact widths so an entitled +Organization selector does not displace session actions or the connection +indicator beyond the page; wrapping must not introduce entitlement reads. AppLayout component and architecture regressions preserve these boundaries; real-backend phone geometry proof is retained by the Organization mobile audit. @@ -9000,17 +7346,18 @@ regression in `internal/ai/adapters/adapters_additional_test.go` enforce it. ### AI entry points follow the assistant session capability Every AI entry point in the app shell follows the `assistantEnabled` session -capability (AI enabled and a provider configured, or mock mode). That covers -the Assistant launcher and Assistant command-palette commands, and the Patrol +capability (AI enabled and a provider configured, or mock mode), read through +`frontend-modern/src/stores/aiAvailability.ts`. That covers the Patrol top-level tab, mobile bottom-bar slot, `g r` shortcut, shortcuts-help row and -`Go to Patrol` palette command. Patrol cannot run without a configured -provider, so an AI-off install no longer carries navigation that only reaches -an "off" setup page, which restores the issue #905 behaviour for Patrol. The -Settings Patrol and Assistant items and the `/patrol` route stay reachable, so -turning AI back on needs no new path. This is presentation gating only: Patrol -runtime, findings, schedules and the capability's server derivation are -unchanged. `AppLayout.test.tsx`, `CommandPaletteModal.test.tsx`, -`KeyboardShortcutsModal.test.tsx` and `App.architecture.test.ts` pin it. +`Go to Patrol` palette command, plus alert Investigate actions. Patrol cannot +run without a configured provider, so an AI-off install no longer carries +navigation that only reaches an "off" setup page, which restores the issue +#905 behaviour for Patrol. The Pulse Intelligence settings pages and the +`/patrol` route stay reachable, so turning AI back on needs no new path. This +is presentation gating only: Patrol runtime, findings, schedules and the +capability's server derivation are unchanged. `AppLayout.test.tsx`, +`KeyboardShortcutsModal.test.tsx`, `InvestigateAlertButton.test.tsx` and +`App.architecture.test.ts` pin it. ### Prompt-cache usage is accounted, not just logged @@ -9059,6 +7406,22 @@ read-only. Proof: `TestExecutionIntent_ArgumentAwareReadCommands` and `TestExecutionIntent_RouteOperationBoundary` in `internal/ai/tools/tools_query_readonly_test.go`. +The browser Assistant chat window was retired on 2026-10-22. Operators who +want to talk to their infrastructure use Claude Code, Codex, and other MCP +clients, which reach the same governed tools through Pulse MCP, so the drawer, +its launcher and command-palette entries, and every "Open in Assistant" or +"Discuss" hand-off left the web app. Alert Investigate now starts a targeted Patrol run +(`POST /api/ai/patrol/run` with the alert's resource and identifier), is offered +only to sessions whose `settingsCapabilities.patrolRun` says that admin route +accepts them, and its findings land on the Patrol page. The External agents settings page moved to +`/settings/pulse-intelligence/agents`; `/settings/pulse-intelligence/assistant` +and its `?tab=` aliases redirect there with the hash preserved. The chat +service in `internal/ai/chat` stays: Patrol investigations run on it and Pulse +Mobile chat uses it until 31 March 2027, so `/api/ai/chat`, the session +endpoints, and the stream contract keep their rules in this contract. +`frontend-modern/src/stores/aiAvailability.ts` replaced the chat store as the +owner of the shell's AI-enabled fact. + ### Typed investigation conclusions and subscription-aware investigation limits (22 October 2026) Patrol investigations record their zero-action conclusion through diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index eb045f0eea..0c9cd7247b 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -210,24 +210,20 @@ checkpoint controls also assert the breach date; restart controls cover both durable active state and the JSON mirror, unchanged mirror bytes/mtime, and legacy unknown-date recovery through a new breach. -### Held breach dates in attention and Assistant +### Held breach dates in attention `getMetricAlertLastBreachAt` selects a valid timezone-qualified ISO `metricStatus.lastBreachAt` before a valid legacy `Alert.lastSeen`. Unknown, malformed, impossible-calendar and Go-zero dates stay undated; the occurrence start, current poll and recovery start are never substitutes. Attention, overview/incident hover and active History share this selection. -Relative ages use the caller's clock. Assistant model-only context carries -the normalised UTC ISO date as `Last Breach At` when known, separate from -the retained breach value and live reading/clear rule. The existing compact -Chat attachment is unchanged; no removed detail surface is restored. -Missing live status after restart keeps the existing recorded-message path. -The scoped target and explicit operator-approval boundary are unchanged. +Relative ages use the caller's clock. Missing live status after restart +keeps the existing recorded-message path. The Assistant hand-off that also +carried the date as `Last Breach At` model context was retired with the chat +window on 2026-10-22. -Verification: date-selection/attention controls in `helpers.test.ts`, -`alertAssistantHandoffModel.test.ts` and final-byte desktop/phone production -surface fixture `browser-tests/held-breach-time.cjs`. These are synthetic -source/rendering controls, not native readings, inference, notification +Verification: date-selection/attention controls in `helpers.test.ts`. These +are synthetic source controls, not native readings, inference, notification provider delivery or restart recovery acceptance. ### Alert card and open incident lead with the live reading — issue #2068 @@ -271,10 +267,11 @@ a screen-reader or speech-input user could not tell rows apart. start time, since history repeats one alert over time; "Hide timeline for ..." while open, with `aria-expanded`) and Resource with `getAlertHistoryResourceActionLabel` (rows on one resource share it because -they open the same panel). The Assistant icon names its alert through the -`alerts.assistant.iconLabel` and `alerts.assistant.iconLabelAt` catalog keys -from the host's `subjectLabel` and `subjectTime`; a locked button keeps its -locked explanation and the hover title is unchanged. Each name begins with +they open the same panel). The icon-only Investigate button, which replaced +the Assistant icon on 2026-10-22, names its alert through the +`alerts.investigate.iconLabel` and `alerts.investigate.iconLabelAt` catalog +keys from the host's `subjectLabel` and `subjectTime`, while its hover title +stays the generic action name. Each name begins with the visible label (`AlertHistoryItemActions.test.tsx`, `AlertHistoryMobileList.test.tsx`, `InvestigateAlertButton.test.tsx`, `alertIncidentPresentation.test.ts`). @@ -975,15 +972,11 @@ default construction path still restores. 58. `internal/alerts/active_lifecycle.go` 59. `internal/alerts/active_cleanup.go` 60. `frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx` -61. `frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts` -62. `frontend-modern/src/components/Alerts/IncidentAssistantHandoffButton.tsx` -63. `frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts` 64. `internal/alerts/storage_override_identity.go` 65. `internal/alerts/unified_eval.go` 66. `frontend-modern/src/components/Alerts/ThresholdsTable.tsx` 67. `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsData.ts` 68. `frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsPlatformData.ts` -69. `frontend-modern/src/utils/alertTargetTypes.ts` 70. `frontend-modern/src/types/alerts.ts` 71. `frontend-modern/src/stores/websocket.ts` 72. `frontend-modern/src/utils/alerts.ts` @@ -1255,37 +1248,27 @@ timeline says the alert moved instead of "Alert resolved: ". `frontend-modern/src/stores/alertsActivation.ts` so browser display colors consume the same configured alert thresholds and override identity chain as the alert runtime instead of hard-coded per-surface thresholds. -7. Add or change alert investigation handoffs through - `frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx` and - `frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts`. - On resource-backed active alert cards, Patrol is the primary doer: the - visible primary action must run a manual scoped Patrol trigger such as - "Have Patrol investigate" through the `ai-runtime` manual Patrol route - contract. Pulse Assistant remains a secondary explicit explanation path: - Assistant handoffs must preserve alert context, force request-scoped - approval mode, send bounded model-only handoff context plus structured - resource references through the shared Assistant chat transport, and render a - compact Alerts-owned briefing in the Assistant drawer without transferring - raw command payloads. The labelled Explain action dispatches the shared - explanation request through `aiChatStore.explain`, retaining selected alert - evidence through normal send and retry without replacing the composer draft. - Ordinary context-only opens remain free of inference. The Patrol trigger - and the Assistant explanation request must stay distinct. -8. Add or change Pulse Assistant incident timeline handoffs through - `frontend-modern/src/components/Alerts/IncidentAssistantHandoffButton.tsx` - and `frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts`; - these handoffs must preserve sanitized incident facts and timeline event - summaries, force request-scoped approval mode, send the same sanitized facts - as model-only handoff context plus structured resource references through - the shared Assistant chat transport, and keep raw command/output details in - the incident or approval surface rather than the chat handoff. Incident - handoffs must not add suggested prompt chips or route-owned remediation - instructions; the configured model owns investigation and next-step - reasoning after it receives the context. +7. Add or change alert investigation through + `frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx`. + On resource-backed alerts Patrol is the doer: the action runs a manual + scoped Patrol trigger through the `ai-runtime` manual Patrol route contract + (`POST /api/ai/patrol/run` with the alert's resource, identifier, and type), + and Patrol's findings land on the Patrol page. The button stays hidden when + AI is not configured, when the session cannot start a Patrol run + (`settingsCapabilities.patrolRun`, which mirrors the run route's + RequireAdmin + `ai:execute` gate; auth-free installs pass), and for Pulse + system alerts that have no resource to scope. A click while the request is + in flight starts no second run. Alerts must not grow a second AI path beside it; the Assistant chat + hand-offs that used to sit here were retired on 2026-10-22. +8. Keep alert incident timelines in place through + `frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx`: the + incident panel and timeline cards carry sanitized incident facts, raw + command/output details stay in the incident or approval surface, and the + timeline must not add chat hand-offs, suggested prompt chips, or + route-owned remediation instructions. 9. Add or change alert target resource types through - `internal/alerts/specs/types.go`, `internal/alerts/config/identity.go`, - `internal/alerts/unified_eval.go`, and - `frontend-modern/src/utils/alertTargetTypes.ts`. Supported target types + `internal/alerts/specs/types.go`, `internal/alerts/config/identity.go`, and + `internal/alerts/unified_eval.go`. Supported target types must share the unified evaluator, the canonical threshold/override identity chain, and the standard notification delivery path. 10. Add or change the alert notification destinations catalog through @@ -1570,8 +1553,7 @@ The alert resource-incident panel dropped its "Open in Infrastructure / Workloads / Storage / Recovery" cross-jump chip strip on 2026-05-16 when the surrounding platform-first migration retired broad surface-link chips. The panel now keeps -investigation flow in-place through `IncidentAssistantHandoffButton` and the -shared incident-timeline cards; it must not reintroduce a chip strip that +investigation flow in place through the shared incident-timeline cards; it must not reintroduce a chip strip that links to the retired top-level routes, and the supporting `buildResolvedResourceSurfaceLinks` helper was deleted from `frontend-modern/src/routing/resourceLinks.ts` as part of the same pass. @@ -3099,14 +3081,12 @@ copy consumed by `frontend-modern/src/pages/Alerts.tsx`, `frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx`, `frontend-modern/src/features/alerts/useAlertAcknowledgementState.ts`, `frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx`, -`frontend-modern/src/components/Alerts/IncidentEventFilters.tsx`, -`frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx`, and -`frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts`. +`frontend-modern/src/components/Alerts/IncidentEventFilters.tsx`, and +`frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx`. Machine-facing alert identifiers, alert types, resource IDs, resource names, node names, source-system messages, commands, command output, event payloads, -log text, and the English Assistant model-context labels must remain -untranslated; only the user-visible briefing labels and alert controls may use -the active app locale. +and log text must remain untranslated; only the user-visible alert controls +may use the active app locale. The Recovery section is a thresholds deep link: `/alerts/thresholds/proxmox#backups` expands and scrolls to it like `#pbs` and `#pmg` (`useThresholdsTableState.test.tsx`). @@ -3703,9 +3683,7 @@ names the selected resource through the unified-resource lookup it is given selected history row carried while no unified record matches. It imports nothing from `frontend-modern/src/routing/resourceLinks.ts`, and it offers no resource cross-jump links: investigation continues in place through the incident -timeline cards or hands off to Pulse Assistant through -`IncidentAssistantHandoffButton`, as the retired cross-jump chip note above -describes. Future incident-link work +timeline cards, as the retired cross-jump chip note above describes. Future incident-link work must not reintroduce a chip set, local infrastructure-link assembly, or provider-local route strings inside the alert feature shell; `frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts` pins the panel @@ -5150,3 +5128,15 @@ metrics target being set to find the same alerts. Inside a guest drawer, a physical disk's agent temperature alerts (`agent:/disk_temp:`) are matched through the guest's own agent id. No alert is created, cleared or retyped by this; it changes which open alerts a drawer lists. + +Alert Investigate became a Patrol run on 2026-10-22, when the browser +Assistant chat window was retired. Active alert cards and History rows keep one +Investigate action (`InvestigateAlertButton`), which starts a targeted Patrol +check through `POST /api/ai/patrol/run` with the alert's resource ID, +identifier, and type, then confirms with a toast that points at the Patrol +page. It is offered only to sessions that route accepts, read from the new +`settingsCapabilities.patrolRun` security-status field. The "Ask Pulse Assistant" icon, the incident-timeline Assistant hand-off, +and their hand-off models (`alertAssistantHandoffModel.ts`, +`incidentAssistantHandoffModel.ts`, `IncidentAssistantHandoffButton.tsx`) are +gone, as is the chat-only `alertTargetTypes.ts` resolver. The `alerts.assistant.*` +catalog keys became `alerts.investigate.*` in English, German, and Spanish. diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 6fd8b32499..1567cc8f6f 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -172,8 +172,7 @@ then checks explicit read-only opt-out and later opt-in remain possible. field dating `Alert.value`. Older servers and restart-before-evaluation may omit it. Consumers validate it, then a valid legacy `lastSeen`, without creating a date from start/poll time. Frontend type and consumer regression -controls are in `types/api.ts`, `features/alerts/__tests__/helpers.test.ts` -and `components/Alerts/__tests__/alertAssistantHandoffModel.test.ts`. +controls are in `types/api.ts` and `features/alerts/__tests__/helpers.test.ts`. No endpoint, required field or mobile capability changes. ### PMG scope readback preserves explicit all-off settings @@ -925,11 +924,8 @@ failure retention require their own owner result, not source acceptance. 66b. `pkg/aicontracts/fix_execution.go` 76. `internal/api/ai_intelligence_handlers.go` 77. `frontend-modern/src/api/ai.ts` -78. `frontend-modern/src/api/aiChat.ts` - 69a. `frontend-modern/src/api/aiChatDevStreamFixture.ts` 79. `frontend-modern/src/api/patrol.ts` 79a. `internal/api/patrol_objectives.go` -80. `frontend-modern/src/api/generated/aiChatEvents.ts` 81. `internal/api/agent_exec_token_binding.go` 81a. `internal/api/agentbinding/policy.go` 81b. `internal/api/agenttokens/install.go` @@ -1187,11 +1183,10 @@ candidate or call node setup/save APIs until the operator has approved the current endpoint, credential path, and collection scope. Assistant chat `workflow_state` events are a shared AI/API payload boundary. -The backend source of truth is `internal/ai/chat.WorkflowStateData`, generated -to `frontend-modern/src/api/generated/aiChatEvents.ts` by -`scripts/generate-types.go` and pinned by `internal/api/contract_test.go`. -The done event (`internal/ai/chat.DoneData`) follows the same generated -contract and carries, alongside token usage and `context_limit_tokens`, an +The backend source of truth is `internal/ai/chat.WorkflowStateData`, pinned by +`internal/api/contract_test.go`; the browser's generated TypeScript projection +left with the Assistant chat window on 2026-10-22. The done event +(`internal/ai/chat.DoneData`) follows the same contract and carries, alongside token usage and `context_limit_tokens`, an optional `session_cost_usd`: the backend-estimated cumulative session spend summed from the operator cost ledger. It is omitted (not zero) whenever any of the session's recorded models has unknown pricing, so clients must treat @@ -1241,13 +1236,11 @@ that same manifest. `POST /api/ai/workflow-prompts/render` is an authenticated `internal/api/router_routes_ai_relay.go`; it accepts the manifest prompt name plus argument map, validates them against the manifest-owned workflow prompt catalogue through `internal/agentcapabilities`, and returns only the rendered -Assistant prompt text plus description. Browser -clients must read prompt names, display labels, presentation kind hints, -descriptions, and arguments through the generated `workflowPrompts` manifest -type and call `AIChatAPI.renderWorkflowPrompt` rather than rendering prompt -templates locally. Browser context may carry a preferred workflow prompt name -only as a request to render one manifest-owned starter through that API route; -it is not a transport for prompt text or a reason to bypass argument resolution. +Assistant prompt text plus description. Clients must read prompt names, +display labels, presentation kind hints, descriptions, and arguments through +the generated `workflowPrompts` manifest type and call this route rather than +rendering prompt templates locally. No first-party browser surface renders +starters since the Assistant chat window was retired on 2026-10-22. MCP `prompts/list` projects those same labels as protocol prompt titles and MCP `prompts/get` remains the protocol-edge projection over the same shared workflow-prompt core. Route inventory and AI chat scope tests must pin that @@ -1452,10 +1445,10 @@ tool availability from local tool-name lists. Do not expose live Assistant registry availability through the public, cacheable `/api/agent/capabilities` manifest; that runtime inventory belongs behind the authenticated `GET /api/ai/assistant/surface-tools` Assistant surface endpoint when the UI -needs it. The Pulse Assistant shell may render a runtime tool-posture chip, but -it must call `AIChatAPI.getAssistantSurfaceTools()` and -`getAgentSurfaceToolPosturePresentation` instead of hard-coding registry tool -names or treating MCP capability names as native Assistant tools. The Agent +needs it. A client that renders runtime tool posture must read that endpoint and +format it through `getAgentSurfaceToolPosturePresentation` instead of +hard-coding registry tool names or treating MCP capability names as native +Assistant tools. The Agent integrations panel may render MCP tool posture from the static public manifest, but the request/response capability filtering and `SurfaceToolContract` construction must be backend-owned in `Manifest.SurfaceToolContracts`; the @@ -1499,8 +1492,8 @@ filters non-request/response capabilities, and enforces the resolved surface `tools` affordance before docs, adapters, or UI projections see any tools. Provider retry progress must use typed fields (`attempt`, `max_attempts`, `retry_after_ms`) on that same event instead of frontend-only string parsing or -provider-specific ad hoc events; the Assistant UI may format those fields, but -must not invent retry progress that the stream contract did not carry. Assistant +provider-specific ad hoc events; clients may format those fields, but must not invent retry progress that the +stream contract did not carry. Assistant `provider_start` status text must remain route-neutral (`Waiting for assistant.`) while selected route identity travels through the typed `provider` and `model` fields. `provider_retry` status text must describe a selected-route @@ -1508,45 +1501,9 @@ retry, not a provider fallback or hidden route switch. Assistant chat stream payloads must not expose automatic provider fallback metadata: `workflow_state` may carry the selected `provider` and `model` plus same-route retry fields, but `provider_fallback`, `failed_provider`, `failed_model`, -`next_provider`, and `next_model` are retired from the generated stream event +`next_provider`, and `next_model` are retired from the stream event contract. Cross-route recovery belongs to explicit failed-turn actions, not hidden `/api/ai/chat` stream mutation. -The frontend chat stream client may expose a local stream-open lifecycle hook -for UI state, but that hook is not a backend `workflow_state` payload. It must -fire only after `/api/ai/chat` returns an OK event-stream response and before -the first parsed SSE event is delivered to the reducer, allowing the Assistant -drawer to promote local `request_send` status to local `request_wait` without -inventing provider activity. -Assistant local stream fixtures are part of the same frontend API contract: -`frontend-modern/src/api/aiChatDevStreamFixture.ts` may short-circuit only -explicit `/fixture ...` prompts in development or test mode, must emit the same -typed stream event sequence as live chat, and must never open a provider request. -That helper also owns the exported fixture-name and alias catalog consumed by -Assistant command discovery; frontend command surfaces may search and insert -`/fixture` from those exports, but they must not duplicate the fixture registry -or advertise fixture commands in production. -Fixtures used for visible stream proof must pace status/tool/content events -enough for the browser to paint the intermediate state, including keeping the -`/fixture tool-burst` running tool row visible before the matching `tool_end`; -unit tests may explicitly disable that pace, but the live dev fixture must not -collapse to the terminal row. -The `/fixture tool-chain` stream must pace consecutive -`tool_start`/`tool_progress`/`tool_end` transitions across at least two tools, -so browser proof can stop on a completed compact row plus the next live running -row without a provider request. -Fixtures that emit consecutive context/read/query tool events must keep those -events as ordinary typed `tool_start` / `tool_end` activity and must not encode -obsolete grouped-context wording in fixture answer content. The fixture payload -contract proves the stream reducer and transcript renderer against the same -chronological event order a live provider would produce; UI grouping or footer -summaries are not part of the fixture contract. -The provider-retry fixture must exercise selected-route retry by emitting -`provider_retry` for the selected route and completing with that same model; it -must not simulate an automatic switch to a configured gateway or alternate -provider route. -Queue verification fixtures must cover both the active hold turn and the queued -drain turn so UX proof can exercise queued follow-up ordering and tool rows -without consuming external model quota. Assistant session summarization is a typed compaction API contract, not a free-form helper response. `POST /api/ai/sessions/{id}/summarize` returns a browser-safe `ChatSessionCompactionResult` with stable fields for `success`, @@ -1556,10 +1513,9 @@ browser-safe `ChatSessionCompactionResult` with stable fields for `success`, `success=false` with `status=not_needed`; transport failures, missing sessions, provider errors, or active-running sessions remain normal API errors. The payload must not include the raw transcript, raw provider response, raw tool -input/output, hidden reasoning, secrets, or token values. Frontend consumers -under `frontend-modern/src/api/aiChat.ts` must preserve that typed response and -let the Assistant drawer reload canonical session state instead of rebuilding a -compacted transcript from local browser state. +input/output, hidden reasoning, secrets, or token values. Clients must +preserve that typed response and reload canonical session state instead of +rebuilding a compacted transcript locally. Infrastructure settings copied agent commands are a shared API/payload boundary even when the final command string is assembled in the browser. Selected-row @@ -1895,7 +1851,6 @@ payload shape change when the portal presents compact client rows. `protected_guests` key from an older client is ignored. Guests Pulse must not act on are locked through the resource operator state (`neverAutoRemediate`), which the Assistant honours at planning. -6. `frontend-modern/src/api/aiChat.ts` shared with `ai-runtime`: the Assistant chat frontend client is both the first-party Assistant transport surface and a canonical API payload contract boundary. 7. `frontend-modern/src/api/generated/agentCapabilities.ts` shared with `ai-runtime`: the generated agent capabilities frontend types are both the Pulse Intelligence manifest TypeScript projection and a canonical API payload contract boundary. 8. `frontend-modern/src/api/nodes.ts` shared with `agent-lifecycle`: the shared Proxmox node client is both an agent lifecycle setup/install control surface and a canonical API payload contract boundary. 9. `frontend-modern/src/api/notifications.ts` shared with `notifications`: the notifications frontend client is both a notification delivery control surface and a canonical API payload contract boundary. @@ -1956,6 +1911,7 @@ payload shape change when the portal presents compact client rows. update checks without rebuilding upgrade state locally. The frontend may disable blocked installs, but the backend apply route remains authoritative and must reject a recomputed `blocked` readiness verdict. +14. `frontend-modern/src/api/workflowPrompts.ts` shared with `ai-runtime`: the workflow prompt activity client is both the AI runtime workflow-prompt telemetry surface and a canonical API payload contract boundary. 15. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` shared with `ai-runtime`, `frontend-primitives`: the External agents settings panel is the optional settings-shell projection of Pulse MCP onboarding, the AI runtime connected-agent onboarding surface, and a presentation consumer of the shared agent capabilities frontend client. The panel may own settings placement, copy composition, local fallback rendering, and the user-facing hierarchy that frames Patrol as the primary @@ -1993,13 +1949,13 @@ payload shape change when the portal presents compact client rows. Client config follows the same product hierarchy: it may expose manifest-derived OpenCode and Claude-style wrappers, but the default panel must lead with Patrol control and scoped-token setup, not raw JSON blocks. - Direct `/settings/pulse-intelligence/assistant#external-agent-setup` links + Direct `/settings/pulse-intelligence/agents#external-agent-setup` links must scroll and briefly focus the External agents panel after the - Assistant settings layout settles, so the external-agent setup route lands + settings layout settles, so the external-agent setup route lands on the optional client setup boundary rather than generic token inventory. Legacy `/settings/security/api#external-agent-setup` and `/settings/security/api#pulse-mcp-setup` links must remain accepted and - redirect to the canonical Pulse Intelligence Assistant route. Normal API + redirect to the canonical Pulse Intelligence External agents route. Normal API Access visits keep token management first; token-preset links may still land on API Access because credential minting remains the API token contract, not a separate credential or execution surface. @@ -2322,18 +2278,15 @@ payload shape change when the portal presents compact client rows. before optional `limit` truncation, and matching may use only safe session identity, title, timestamps, and handoff-summary fields. Search must not expose provider-bound model context, stored prompts, raw tool output, or - remediation details, and frontend clients must route searchable picker - requests through the shared `AIChatAPI.listSessions({ search, limit })` - helper rather than inventing a parallel local history endpoint. + remediation details, and clients must search through this endpoint rather + than inventing a parallel history endpoint. `PATCH /api/ai/sessions/{id}` owns the Assistant session-title mutation contract. The request body is limited to a user-visible `title`; the handler must reject empty titles, the chat store must normalize whitespace and bound persisted title length, and the response must be the updated browser-safe `ChatSession` projection. Rename must not expose or mutate stored prompts, messages, provider reasoning, model handoff context, - approvals, action state, or tool evidence. Browser clients must call the - shared `AIChatAPI.renameSession(sessionId, title)` helper so path encoding - and JSON body shape stay canonical. + approvals, action state, or tool evidence. `POST /api/ai/sessions/{id}/undo` and `POST /api/ai/sessions/{id}/redo` own the Assistant turn-repair API contract. Undo removes the latest user-authored turn and all later @@ -2344,7 +2297,7 @@ payload shape change when the portal presents compact client rows. undone turn and returns `restored_messages` plus the remaining `can_redo` state. `GET /api/ai/sessions` and other `ChatSession` projections may expose only the boolean `can_redo` hint alongside safe title/timestamp, - count, and handoff-summary fields so the drawer can re-enable redo after a + count, and handoff-summary fields so a client can re-enable redo after a reload without reading the redo stack itself. The projection also carries a boolean `system` flag for Pulse-owned background sessions (Patrol detection `patrol-main`, `patrol-eval`, and `investigation-*` runs), @@ -2358,11 +2311,7 @@ payload shape change when the portal presents compact client rows. prompt being re-run; retry/regenerate flows send it so a stale retry can never remove a different turn, and a guard mismatch returns `success:false` with a browser-safe message rather than an error status. - An absent or empty body preserves the unguarded undo shape. Browser - clients must use the shared - `AIChatAPI.undoLastTurn(sessionId, { expectedPrompt })` and - `AIChatAPI.redoLastTurn(sessionId)` helpers so path encoding, guard - trimming, and response shape stay canonical. + An absent or empty body preserves the unguarded undo shape. `POST /api/ai/sessions/{id}/steer` owns the mid-turn steering API contract (`chat.SessionSteerRequest` -> `chat.SessionSteerResult`). The request carries `prompt` plus an optional `client_message_id`; the @@ -2373,12 +2322,10 @@ payload shape change when the portal presents compact client rows. follow-up on the ordinary queue-drain path. Steering cannot carry model route, control level, or autonomous-mode changes. Confirmation of delivery arrives only on the session's existing chat stream as a - generated `steer_applied` event (`chat.SteerAppliedData`, in - `aiChatEvents.ts` and the `AIChatStreamEvent` union) echoing + `steer_applied` event (`chat.SteerAppliedData`) echoing `client_message_id`, the server message id, the prompt, and the turn index; clients must treat the endpoint's `accepted:true` as inbox - receipt, not delivery. Browser clients must use the shared - `AIChatAPI.steerSession(sessionId, { prompt, clientMessageId })` helper. + receipt, not delivery. Every Assistant session mutation sub-resource under `/api/ai/sessions/{id}` (`/abort`, `/summarize`, `/fork`, `/undo`, `/redo`, `/steer`) accepts only `POST`. Any other method that reaches the @@ -2421,21 +2368,15 @@ payload shape change when the portal presents compact client rows. `success` state while keeping internal `tool_result` records out of the browser transcript. Assistant message history must also preserve the effective `model` route that produced the answer, including local runtime - routes such as `pulse:local-inventory`. Frontend API types must accept - restored `tool_calls[].input` from backend history as a structured object - as well as legacy/display strings, then normalize it before rendering. - Chat stream events are generated from `internal/ai/chat` payload structs - into `frontend-modern/src/api/generated/aiChatEvents.ts`; agent capability - manifest types are generated from `internal/agentcapabilities` payload - structs into `frontend-modern/src/api/generated/agentCapabilities.ts`. - Those generated projections must stay derived through - `scripts/generate-types.go`, and the chat event union must not include the - retired `explore_status` pre-pass event. Runtime - workflow telemetry may remain a transport event, but browser Assistant + routes such as `pulse:local-inventory`. Agent capability manifest types are + generated from `internal/agentcapabilities` payload structs into + `frontend-modern/src/api/generated/agentCapabilities.ts` and must stay + derived through `scripts/generate-types.go`. The chat stream must not + carry the retired `explore_status` pre-pass event. Runtime workflow + telemetry may remain a transport event, but client-bound Assistant streams must use `chat.StreamEvent.ClientSafe()` so raw provider `thinking` payloads and serialized tool-call prose cannot cross the API - boundary. Browser Assistant output is owned by the AI runtime presentation - contract rather than by API-client event typing. + boundary. Tool execution progress is part of this SSE contract: `tool_start`, `tool_progress`, `tool_cancel`, and `tool_end` must carry typed payloads generated from `internal/ai/chat` structs so the frontend can mutate one @@ -2491,52 +2432,6 @@ payload shape change when the portal presents compact client rows. fixture must pace its browser-facing status/tool/content events enough to prove the visible stream contract, while tests may disable that pace to keep backend proof fast. - The frontend API client may also own an explicit local dev/test Assistant - stream fixture at `frontend-modern/src/api/aiChatDevStreamFixture.ts` for - fast UX iteration against the real stream reducer. That fixture may - short-circuit `frontend-modern/src/api/aiChat.ts` only in Vite dev or test - mode, only for reserved `/fixture ...` prompts, and only by calling the - normal `AIChatAPI.chat` event callback with the generated - `AIChatStreamEvent` union (`session`, `workflow_state`, `thinking`, - `tool_start`, `tool_progress`, `tool_cancel`, `tool_end`, `content`, - `done`). It must not open a backend session, mutate persisted chat history, - add browser-only stream event shapes, or become a production fallback for - provider or VPN failures. At least one fixture-backed Assistant tool - sequence must exercise the OpenCode-parity raw tool-input path by starting - with incomplete provider-style `raw_input`, then mutating the same tool row - with completed arguments through the normal `tool_progress` event. - At least one fixture-backed Assistant tool sequence must exercise the - skipped-tool lifecycle by resolving a visible `tool_start` row through - `tool_cancel` instead of hiding the activity. At least one fixture-backed - Assistant tool sequence must exercise live pending/running tool evidence by - emitting placeholder structured input plus raw provider-style input on - `tool_start`, mutating that same row through `tool_progress`, and pacing the - running state long enough for browser proof to inspect and copy both the - completed input and current progress before `tool_end`. At least one - fixture-backed Assistant tool sequence must exercise successful long - plain-text tool output by emitting enough output lines to prove the browser - drawer renders a bounded collapsed preview, avoids an opaque output badge, - and preserves the full output in expandable details. At least one - fixture-backed Assistant tool sequence must exercise compacted provider artifact - suppression by emitting compacted pre-tool content before a governed tool row - and final normal answer content. At least one fixture-backed Assistant tool - sequence must exercise a buffered `tool_start` -> `tool_end` burst with no - provider delay between the two events, followed by a paced content/terminal - step, so browser proof can verify that fast command activity remains - perceptible without a real provider request. At least one fixture-backed - Assistant tool sequence must exercise consecutive multi-tool activity with a - paced completed first row followed by a live second row, so browser proof can - verify replacement/compaction motion without provider timing. At least one - fixture-backed Assistant sequence must exercise `provider_retry` with `attempt`, - `max_attempts`, and `retry_after_ms` metadata plus a paced retry window, so - browser proof can verify visible retry countdown behavior without a real - provider request, VPN dependency, or API spend. At least one fixture-backed - Assistant sequence must exercise `stream_idle` after selected-provider - startup, so browser proof can verify visible idle liveness without forcing a - real provider to pause on demand. At least one fixture-backed Assistant - sequence must exercise the local prompt-send state by emitting `session`, - then pacing the first backend `workflow_state` long enough for browser proof - to verify immediate visible activity without opening a provider request. 65. `internal/api/ai_handlers.go` shared with `ai-runtime`: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary. The AI settings payload on `/api/settings/ai` carries no cloud-context-privacy field: cloud context behavior is a fixed posture (real context to cloud, with @@ -2582,8 +2477,8 @@ payload shape change when the portal presents compact client rows. Assistant and settings readiness; Patrol-specific wording is reserved for Patrol preflight, Patrol findings, and Patrol run records. Raw details stay available only to server logs or redacted governed internal Patrol - evidence. The frontend API client, settings shell, and Assistant drawer - must treat this payload as the canonical provider health contract rather + evidence. The frontend API client and settings shell must treat this + payload as the canonical provider health contract rather than parsing free-form provider error strings. 66. `internal/api/ai_intelligence_handlers.go` shared with `ai-runtime`: AI intelligence handlers are both an AI runtime control surface and a canonical API payload contract boundary. 67. `internal/api/alerting/notification_queue.go` shared with `notifications`: the notification queue and DLQ handler is both a notification delivery consequence surface and a canonical API payload boundary for operational transition links. @@ -3748,9 +3643,7 @@ a new API state machine, queue contract, or verification-accounting field. selected-route `provider_start` message is operator progress copy and must be active/in-progress wording such as ` is starting the response.` while the typed `provider` and `model` - fields carry the exact route identity. The generated - `frontend-modern/src/api/generated/aiChatEvents.ts` type must stay derived - from `internal/ai/chat/types.go` through `scripts/generate-types.go`; + fields carry the exact route identity. `frontend-modern/src/api/generated/agentCapabilities.ts` must stay derived from `internal/agentcapabilities`, including manifest surface contract fields; and frontend API tests must pin @@ -4797,9 +4690,8 @@ the authoritative analysis outcome. 26. Keep shared AI runtime reads centralized on that same governed contract: `frontend-modern/src/stores/aiRuntimeState.ts` is the canonical frontend read owner for `/api/settings/ai` and `/api/ai/models`. AI-owned consumers - such as `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`, - `frontend-modern/src/components/AI/Chat/index.tsx`, and - `frontend-modern/src/components/AI/AICostDashboard.tsx` must reuse that + such as `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` + and `frontend-modern/src/components/AI/AICostDashboard.tsx` must reuse that shared store for read-side runtime truth, while `frontend-modern/src/components/Settings/useAISettingsState.ts` remains the write-side settings owner. Non-AI settings surfaces such as @@ -7826,8 +7718,8 @@ provider-local routes. Phase 1 must not add public `/api/vmware/resources`, transport just because the internal poller now projects VMware-backed resources into the shared canonical inventory. That same shared API contract now also owns Assistant mention transport for -those canonical resources. `frontend-modern/src/api/aiChat.ts`, -`internal/api/ai_handler.go`, and `internal/api/ai_handlers.go` must preserve +those canonical resources. `internal/api/ai_handler.go` +and `internal/api/ai_handlers.go` must preserve structured mention payloads for canonical `agent`, `vm`, `storage`, and `network` resources as shared unified-resource IDs plus shared mention types, so VMware-backed reads stay on `/api/ai/*` and `/api/resources*` instead of @@ -9432,10 +9324,8 @@ Pulse-authored attention reason, operator decision, or recommended next step. An action note from a durable investigation record is context, not remediation guidance. That governed action artifact metadata must come from the same structured action reference sent to chat execution, including any recovered live -Patrol approval, so visible and model-only handoffs stay consistent. Patrol's -frontend Assistant drawer briefing must use that same factual frame for -visible handoffs from findings, while the downstream chat -service hydrates live resource state, timeline, and action audit context around +Patrol approval, so visible and model-only handoffs stay consistent. The downstream chat service +hydrates live resource state, timeline, and action audit context around that same handoff. Backend chat handling must treat matching frontend Patrol handoff context as secondary to durable finding context, not a replacement for server-refreshed approval, resource, action, or requester authority. Frontend Patrol @@ -9453,9 +9343,7 @@ paste raw approval or action command text into a chat prompt. Patrol remediation-plan or action-artifact Assistant handoffs must pass only safe status, risk, description, and command-count posture as non-authoritative context for the configured LLM to critique; raw plan command and rollback command -payloads remain owned by the governed remediation/action APIs and panels. The -frontend Assistant drawer must not turn those artifacts into visible Patrol step -lists, suggested-prompt chips, or a "plan attached" answer. Evidence, +payloads remain owned by the governed remediation/action APIs and panels. Evidence, confidence, approval risk, prerequisites, recurrence, rollback, and verification belong in model-only context or governed approval surfaces unless the operator explicitly asks the LLM for them. @@ -12587,6 +12475,36 @@ Routes, caller scope/token filtering, cache policy and probe count are unchanged HTTP statuses, private-body controls, same-credential fixture recovery and wire attribution. They are not installed permission or guest-recovery acceptance. +The browser Assistant chat window was retired on 2026-10-22. Its frontend +client (`frontend-modern/src/api/aiChat.ts`), the dev stream fixture, and the +generated TypeScript chat event projection left with it; `scripts/generate-types.go` +no longer emits chat stream types. `/api/ai/chat`, the session endpoints, and +the chat stream payloads keep their contracts because Patrol investigations +and Pulse Mobile chat still use them, pinned server-side by +`internal/api/contract_test.go`. The alert Investigate action now calls +`POST /api/ai/patrol/run` with `resource_ids`, `alert_identifier`, and +`alert_type`, and the External agents settings route is +`/settings/pulse-intelligence/agents`. + +`/api/security/status` `settingsCapabilities` gained `patrolRun` on 2026-10-22. +`securityStatusCanRunPatrol` answers that route's RequireAdmin + `ai:execute` +gate from the security status snapshot, like the other admin capabilities, so +it validates no token and has no side effect. Where `ensureAdminSession` +differs from `canAccessAdminSurface` it follows the route: an instance +administrator on an org-scoped session is admitted, an organization-bound +token is refused whatever its scopes, and a session cookie beside a token must +belong to an instance administrator. The snapshot's remaining gaps +(recovery-session binding, proxy-over-token precedence, Bearer-only tokens, +the dev bypass beside an explicit token) are shared with every settings +capability. The browser offers alert Investigate only where the field is true. Auth-free installs report no capability +set and the frontend treats them as allowed, as it already does for update +status. `TestContract_SecurityStatusPatrolRunTracksRunRouteGate` keeps the +field and the route in agreement, including a non-admin cookie beside a token; +`TestSecurityStatusPatrolRunRefusesOrgBoundToken` and +`TestSecurityStatusPatrolRunFollowsInstanceAdminOnOrgScopedSession` compare it +with `ensureAdminSession` directly, and the capability enforcement tests list it +with the other admin-only fields. + ### Typed investigation conclusion tool (22 October 2026) `patrol_conclude_investigation` joins `patrol_propose_action` and diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 94656e76c6..4def63b12f 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -2563,16 +2563,6 @@ pipeline (downloading through restarting, including the `restoring` stage emitted by update rollback) and it carries no hosted entitlement, org, or paid-gating semantics; hosted and paid surfaces must not key tenant or billing behavior off update progress stages. -That same authenticated shell split must also respect shared blocking dialogs: -hosted chrome may not leave the Pulse Assistant launcher or an already-open -assistant drawer interactive behind a modal that currently owns the viewport. -That same hosted chrome rule also covers responsive launcher placement. While -the authenticated shell is using mobile navigation, `frontend-modern/src/AppLayout.tsx` -must keep the closed Pulse Assistant launcher in the authenticated header flow, -where it cannot cover page content or compete with the mobile nav. It must not -reintroduce the desktop right-edge launcher until the desktop shell breakpoint, -otherwise hosted and self-hosted operators lose usable dashboard and -billing-page edge space at tablet widths. That same authenticated shell contract now also has to distinguish backend availability from websocket-stream liveness. When hosted runtime health stays available during a stream reconnect or renegotiation window, @@ -3745,13 +3735,13 @@ boundary without granting Community private RBAC or changing CI tier membership. ### AI navigation gating stays outside commercial scope -The app shell hides Patrol navigation and Assistant palette commands while the +The app shell hides Patrol navigation and alert Investigate actions while the `assistantEnabled` session capability is false. The gate reads only that AI capability: it does not consult entitlements, organization scope, hosted bootstrap or upgrade state, and it adds no commercial prompt. Paid Patrol modes stay governed on the Patrol and Actions surfaces, which remain -reachable by route. `App.architecture.test.ts` pins that the gate reads the -same capability as the Assistant launcher. +reachable by route. `App.architecture.test.ts` pins that the shell reads that capability through +`aiAvailabilityStore`. ### Relay retired inside the app @@ -3801,3 +3791,11 @@ metric/recovery retention remain unchanged. This source change does not deploy a runtime or change customer subscriptions. Private renderer tests cover formats, missing/observed-zero distinction, formula-safe CSV and PDF generation. Runtime hook tests and final-source Pro browser delivery qualify registration/composition. + +The browser Assistant chat window was retired on 2026-10-22, so the hosted +shell rules about its launcher placement and modal behaviour went with it. +The chat was sold as a BYOK feature of the free tier ("Pulse Assistant (BYOK +Chat)"), not as a Pro capability, so no paid entitlement changes. Paid Patrol +capabilities stay as they were: Patrol modes, investigations, governed fixes, +and history live on the Patrol and Actions surfaces, and alert Investigate now +starts a targeted Patrol run instead of opening a chat. diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index e25c06f16c..f498f928a9 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -4120,6 +4120,27 @@ release-selection, promotion or rollback boundary is introduced. Full exact-grap checks, the strict full-graph audit and fresh representative browser rendering remain necessary before acceptance; a prepared lock or acquisition is not proof. +### Reviewed DOMPurify and Vitest advisory floors (22 Oct 2026) + +`frontend-modern/package.json` moves `dompurify` to `^3.4.17` and `vitest` and +`@vitest/coverage-v8` to `^4.1.12`, and `frontend-modern/package-lock.json` +locks those versions. `GHSA-cg2g-xh64-v4q7` (critical `SAFE_FOR_TEMPLATES` +bypass through attached shadow roots), `GHSA-mrv2-3pqr-j5jr` (an `IN_PLACE` +hook that removes a parent or sibling left detached handlers armed) and +`GHSA-c6gr-qvv4-5v3f` (SMIL `attributeName=href` guard skipped under +`application/xhtml+xml`) affect DOMPurify up to 3.4.16, and +`GHSA-qxg6-89mg-rqv8` and `GHSA-xp26-3454-mjgp` affect `@vitest/mocker` below +4.1.12. Published on 22 October 2026, they failed the required frontend +dependency audit on every pull request. DOMPurify ships in the frontend bundle +and the change is a patch release inside the reviewed `^3` range; Vitest is +development-only. The lock also takes the `chai`, `obug`, `std-env` and +`tinyrainbow` versions the new Vitest ranges resolve and one deduplicated +`picomatch` 4.0.7, and leaves the installer, artifact, signing, promotion and +rollback boundary unchanged. +`frontend-modern/src/security/__tests__/dependencySecurity.test.ts` raises the +Vitest floor to 4.1.12 and the reviewed `dompurify` floor to 3.4.17, so a later +downgrade is rejected. + ### Reviewed @types/node 26.6.2 refresh The 2026-09-23 `npm-minor-patch` group (Dependabot #2189) advances the @@ -8089,6 +8110,13 @@ They prove phase coverage, build/discovery/check failure propagation, passing tests, explicit skips, examples and fuzz seeds; actual failure, exit zero, timeout and unmatched test names must fail. +`highlight.js` left the `frontend-modern` dependencies on 2026-10-22. Its only +importer was the retired Assistant chat code-block highlighter, so the +production build already shipped no `vendor-highlight` chunk; the reviewed +dependency floor in `dependencySecurity.test.ts` drops with it. The lockfile +loses only that package entry. Installer, artifact, signing, promotion and +rollback boundaries are unchanged. + ## Release-body updater ownership (30 September 2026) Each executable `/bin/update --version` example in a published body must carry diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 1a5a9969c6..29601a17e8 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -1583,7 +1583,6 @@ removal/failure retention and containing publication remain separate evidence. 5. `frontend-modern/src/components/Settings/settingsPanelRegistry.ts` 6. `frontend-modern/src/components/Settings/APIAccessPanel.tsx` 6a. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` -7. `frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx` 8. `frontend-modern/src/components/Settings/AIModelSelectionSection.tsx` 9. `frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx` 10. `frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx` @@ -1695,7 +1694,6 @@ removal/failure retention and containing publication remain separate evidence. 111a. `frontend-modern/src/utils/storageSummaryCache.ts` 111. `frontend-modern/src/components/shared/TableCardHeader.tsx` 112. `frontend-modern/src/components/shared/UpgradeLink.tsx` -113. `frontend-modern/src/components/shared/useUpgradeNavigation.ts` 114. `frontend-modern/src/utils/upgradeNavigation.ts` 115. `frontend-modern/src/components/DemoBanner.tsx` 116a. `frontend-modern/src/components/CommercialMigrationBanner.tsx` @@ -1706,7 +1704,7 @@ removal/failure retention and containing publication remain separate evidence. 119. `frontend-modern/src/stores/licenseCommercial.ts` 120. `frontend-modern/src/useAppRuntimeState.ts` 121. `frontend-modern/src/routing/routePreload.ts` -122. `frontend-modern/src/stores/aiChat.ts` +122. `frontend-modern/src/stores/aiAvailability.ts` 123. `frontend-modern/scripts/header-audit.mjs` 124. `frontend-modern/src/components/Settings/DataHandlingPanel.tsx` 125. `frontend-modern/src/components/Settings/dataHandlingPanelModel.ts` @@ -2226,11 +2224,11 @@ AGENT_SURFACE_ID_PULSE_MCP)` and `getAgentSurfaceToolPosturePresentation`, work, with Patrol as the built-in operator that checks infrastructure, follows Patrol mode before acting, asks when approval is required, verifies outcomes, and records history, state that external agents use that same boundary, and make the canonical - `/settings/pulse-intelligence/assistant#external-agent-setup` route land on + `/settings/pulse-intelligence/agents#external-agent-setup` route land on and briefly focus this panel with setup open rather than leaving the user at the generic API token inventory. Legacy `/settings/security/api#external-agent-setup` and `/settings/security/api#pulse-mcp-setup` links must remain accepted and may - redirect to the canonical Pulse Intelligence Assistant route. Normal API + redirect to the canonical Pulse Intelligence External agents route. Normal API Access visits remain token-management first, and external-agent setup must not reorder the API token inventory because it no longer lives on that page. The Assistant settings default must keep setup mechanics behind a `Show connector setup` @@ -2290,70 +2288,7 @@ AGENT_SURFACE_ID_PULSE_MCP)` and `getAgentSurfaceToolPosturePresentation`, 11. `frontend-modern/src/features/platformPage/PlatformWindowedRows.tsx` shared with `performance-and-scalability`: the shared bounded table-row renderer is both a canonical platform-page primitive and a fleet-scale mounted-DOM performance boundary. 12. `frontend-modern/src/features/platformPage/usePlatformWindowedItems.ts` shared with `performance-and-scalability`: the platform windowing controller is both a canonical frontend scroll primitive and a directional-runway performance hot path. 13. `frontend-modern/src/routing/routePreload.ts` shared with `performance-and-scalability`, `unified-resources`: the app-shell route preload registry is a canonical frontend shell boundary, an authenticated hot-path performance boundary, and the entry point for the unified-resource Actions workspace. -14. `frontend-modern/src/stores/aiChat.ts` shared with `ai-runtime`: the assistant drawer and session store is both an AI runtime control surface and a canonical app-shell presentation boundary. - Assistant session pickers and reloads must restore only safe - `handoff_summary` presentation state from the session list. Loading a plain - session or starting a new conversation must clear stale scoped handoff - briefing state so Patrol and alert context does not visually leak between - conversations. Browser-originated model handoff payloads are one-shot - request seeds: after the first successful chat send, this store must clear - `handoffContext`, `handoffResources`, `handoffActions`, and safe - `handoffMetadata`; it must also clear any preferred workflow prompt request - once the manifest-rendered starter has seeded the composer and the first - scoped send succeeds. The store must preserve the safe visible briefing and - scoped approval-required posture, so later turns rely on backend session - hydration instead of resending stale browser context. Patrol handoffs must not include - Persisted Assistant redo availability is safe session chrome, not transcript - content. The drawer may consume `ChatSession.can_redo` from the backend - session list to re-enable Redo after reload or session refresh, but it must - not read or duplicate the redo stack in frontend state. Undo restores the - editable prompt draft and safe structured send metadata into the composer; - Redo clears that recovered draft only after the backend restores the turn. - Browser-owned session-management comments, command request handling, and - question-answer plumbing in this store and the adjacent chat hook must name - the native drawer surface as Pulse Assistant rather than reviving the - retired generic `Pulse AI` label. Shared model/provider settings guidance - still belongs to Pulse Intelligence > Provider & Models. - Patrol handoffs must not include - safe next-step labels, action kinds, or whitelisted app-route hrefs in - `handoffMetadata`; the drawer must treat - `handoff_summary.requires_approval` as a current pending-decision flag, not a - historical action marker, so completed or rejected handoff actions render as - action context rather than pending approval. A restored Patrol run summary - must remain visibly sourced to Pulse Patrol, restore a `patrol-run` target - plus run ID/type/status/runtime-failure presentation only, and must not - rehydrate model-only runtime failure detail into browser context. New Patrol - run requests follow the same drawer boundary: source-owned context and - briefing copy may show classified, redacted failure summaries for operator - review, but `handoffContext`, `handoffResources`, and `handoffActions` for - run-history context must stay absent from the browser request so the backend - can rebuild model-bound context from the stored Patrol run. Restored - Patrol assessment, Patrol finding, and Patrol control save-failure sessions - follow the same safe-summary rule: the drawer may restore source label, - title, target type, status badge, bounded resource facts, and approval/action - status from `handoff_summary`, but - it must not infer a finding target from bounded action references or - reconstruct hidden model context, provider details, retry payloads, commands, - preflight output, or action results in the browser. If the safe summary - was created by a legacy build that stored Patrol next-step metadata, - recommendation detail, action labels, safe action kind, or whitelisted - app-route href, the session picker plus restored drawer must ignore those - fields rather than carrying them forward as hidden context or visible - recommendation copy. - Session-load and new-conversation transitions must be success-bound: if the - underlying session operation fails, the shared drawer store must not clear or - replace the current scoped handoff context. - Restored Patrol assessment sessions use that same - `patrol-assessment`/`pulse-patrol-assessment` target identity rather than a - retired dashboard target, so restored-session chrome remains source-named; - Patrol no longer opens live assessment handoffs. - The shared `frontend-modern/src/components/shared/AIModelPicker.tsx` - primitive must keep model route presentation delegated to the AI runtime - label helpers. Pulse-owned local Assistant routes such as - `pulse:local-inventory` and `pulse:mock-assistant` are implementation - routes and must render as named choices without secondary raw route IDs, - while external provider route IDs may remain visible where they disambiguate - catalog entries. +14. `frontend-modern/src/stores/aiAvailability.ts` shared with `ai-runtime`: the AI availability store is both the AI runtime session capability and the app-shell gate for Patrol entry points. 15. `frontend-modern/src/utils/platformSupportManifest.generated.ts` shared with `unified-resources`: the generated platform support projection is both a canonical unified-resource platform union boundary and a shared frontend source/platform vocabulary boundary. It must expose the manifest `surface_kind` field so runtime lenses such as `docker` are not collapsed back into owning platform semantics. @@ -2659,14 +2594,8 @@ not a replacement status card, CTA band, or page-local nested card. Standalone machine row action triggers follow the same rule: the Machines table owns remove-agent semantics and menu placement, while `ActionIconButton` owns the compact muted trigger chrome. - AI Chat follows that same boundary for drawer header controls, session row - actions, transcript fallback close/download actions, activity-dock queued - follow-up controls, composer send, footer help/route actions, and compact - dismiss controls: the Assistant owns chat/session behavior and copy, while - `ActionIconButton` owns h-5/h-6/h-7/h-8/h-9 sizing, outline, primary, accent, - warning, info, danger, disabled, title, and focus chrome. - AI Chat message and tool copy controls follow the copy-action boundary: - MessageItem and ToolExecutionBlock own the copied text and timer semantics, + Patrol tool copy controls follow the copy-action boundary: + `ToolExecutionBlock` owns the copied text and timer semantics, while `CopyValueButton` owns copied-state iconography, disabled handling, focus, and embedded-row propagation behavior. Global app-shell prompts are part of the same action boundary. @@ -3510,36 +3439,26 @@ Agent`), with the plain-language source phrase available through accessible review destination for a current paid feature, it must scope the operator into the usage-owned policy ledger rather than plan-selection intent or CTA copy that frames the flow as monitored-system-cap expansion. -14. Keep assistant availability bootstrap on the shared app-shell boundary. +14. Keep AI availability bootstrap on the shared app-shell boundary. `frontend-modern/src/useAppRuntimeState.ts`, - `frontend-modern/src/App.tsx`, - `frontend-modern/src/stores/aiChat.ts`, and - `frontend-modern/src/components/AI/Chat/index.tsx` must consume the + `frontend-modern/src/App.tsx`, and + `frontend-modern/src/stores/aiAvailability.ts` must consume the backend-owned `/api/security/status.sessionCapabilities.assistantEnabled` fact instead of probing `/api/settings/ai` or `/api/ai/*` during ordinary - route bootstrap. Closed assistant chrome and non-AI settings panels may - not initialize assistant runtime state until an owned assistant or Patrol - surface is actually open. `frontend-modern/src/stores/aiChat.ts` is the - shared drawer shell owner for assistant open/close state, focus handoff, - and tenant-local context/session persistence; the app shell must not fork - that state across `App.tsx`, `AppLayout.tsx`, or page-level helpers. - The same shared shell boundary must keep Pulse Assistant coherent while a - blocking shared dialog owns the viewport: closed launcher affordances must - hide until the dialog clears, and the shell must close any already-open - assistant drawer instead of leaving background assistant controls visibly - active behind the modal. Non-critical app-shell prompts, including - promotional or feedback prompts, must not use the shared blocking dialog - stack because they must not suppress Pulse Assistant access or look like - required operational acknowledgement. + route bootstrap. Non-AI settings panels may not initialize AI runtime state + until an owned Patrol surface is actually open, and the app shell must not + fork the AI-enabled fact across `App.tsx`, `AppLayout.tsx`, or page-level + helpers. Non-critical app-shell prompts, including promotional or feedback + prompts, must not use the shared blocking dialog stack because they must + not look like required operational acknowledgement. AI-owned frontend surfaces that need shared settings or model-catalog truth must route those reads through `frontend-modern/src/stores/aiRuntimeState.ts` rather than each feature bootstrapping `/api/settings/ai` or `/api/ai/models` independently. Non-AI settings panels such as `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts` - must stay on the app-shell assistant-availability fact instead of - re-reading raw AI settings just to decide whether assistant affordances - should render. + must stay on the app-shell AI availability fact instead of re-reading raw + AI settings just to decide whether AI affordances should render. 15. Keep Patrol shell composition and product-first provider vocabulary on the shared feature-presentation boundary. `frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx`, @@ -3639,23 +3558,15 @@ Agent`), with the plain-language source phrase available through accessible save-failure state returns, it must stay viewport-bounded, expose an accessible dialog label, bind any model selector to the shared runtime settings and model catalog, show readiness context (provider, model, - summary) next to a direct provider-settings action rather than behind - Assistant alone, and describe a saved but not ready response as a saved - configuration issue rather than a failed save. An Assistant handoff from it - must send a source-named, model-only briefing and close the popover so the - shared Assistant drawer is not visually hidden behind feature chrome. When - governed fixes are locked, the Patrol header presents `Watch only` as the + summary) next to a direct provider-settings action, and describe a saved but not ready response as a saved + configuration issue rather than a failed save. When governed fixes are + locked, the Patrol header presents `Watch only` as the effective mode with no enabled paid choice, and the Patrol state owner refuses any other level when a save starts. Its autonomy payload never carries `full_mode_unlocked`: `PUT /api/ai/patrol/autonomy` itself clears stale full-mode unlock and Autopilot activation state on every save below Autopilot, so disabled paid controls cannot leak stale permission - into a save. When a - restored Patrol assessment session is attached, the shared Assistant drawer - empty state must - stay aligned with that source-named briefing and must not render generic - cluster/system starter prompts below the Patrol-owned context. The Patrol - feature shell must also consume the Patrol-owned findings source for its + into a save. The Patrol feature shell must also consume the Patrol-owned findings source for its findings tab, run-scoped findings panels, and tab badges so shared feature composition does not rebuild Patrol state by filtering the cross-product unified findings feed. @@ -4185,114 +4096,16 @@ production table, router and styles; it does not qualify full-app scrolling. IDs into setup payloads. The shared settings shell should let the backend resolve the effective BYOK model and then render that returned state rather than guessing a model in the modal. - Scoped Assistant handoffs keep their approval-required disclosure in - drawer context: `autonomousMode:false` on `AIChatContext` is a drawer-only - flag the chat transport never sends, because `/api/ai/chat` sets - approval-required mode server-side for every request. Dashboard and other - route-owned entry points may open the - Assistant drawer with source context and that flag, but they - must not infer a user task from an ordinary context-only open. Explicit - labelled explanation actions use `aiChatStore.explain` and the shared - explanation dispatcher, which captures the request context, waits for open - initialization, preserves drafts, and acknowledges each request once. - Older request completion must not clear newer handoff context. Neither - path may mutate persistent AI control-level settings or trigger background - Assistant settings/model bootstrap before the drawer is open. Patrol finding handoffs that add structured - investigation-record framing must derive that context through - `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` - so shared drawer primitives stay shell-owned rather than becoming a - Patrol-specific diagnosis formatter; shared drawer primitives must not - branch on intent themselves. Patrol-page - surfaces must not add standalone trust strips to shared header or - workspace chrome; high-signal trust facts may feed the Patrol-owned - assessment readout, but shared drawer/chrome primitives stay free of - the `FindingsTrustSummary` shape so adding new trust signals goes + Patrol-page surfaces must not add standalone trust strips to shared header + or workspace chrome; high-signal trust facts may feed the Patrol-owned + assessment readout, but shared chrome primitives stay free of the + `FindingsTrustSummary` shape so adding new trust signals goes through the contract first rather than per-shell branching. Patrol refresh controls stay on that same feature-owned shell boundary: `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` must make the refresh affordance generation-aware and timeout-bounded, so a slow supporting intelligence read cannot permanently disable the stale-data Retry control while Patrol findings and status remain visible. - That feature-owned - presentation helper is the single emitter for investigation-record - `impact` and `rollback` fields: when an investigation record exists but those fields - are empty, the helper emits explicit `Impact not assessed` and - `Rollback not specified` lines into the model-only Patrol finding - prompt context so the operator-visible gap is surfaced to Assistant - rather than hidden, and shared chat primitives stay free of that - placeholder logic. Patrol run-history handoffs must also use that feature helper rather - than a row-local Assistant prompt, so the shared drawer receives only a - generic visible briefing, safe run identity metadata, and - `autonomousMode:false`; the backend rebuilds the bounded model-only run - context, scoped resource references, and runtime failure detail from the - run ID, while the Patrol feature remains the source of run copy and - retry/configuration guidance. Finding handoff action references - may add live pending approval posture only as safe structured metadata: - approval ID, pending status, risk, target, requested/expiry timestamps, - action plan identity, requester identity, approval policy, plan expiry, and - dry-run posture. Those references may be passed through shared chat - transport as `handoff_actions` for model-only refresh, but the shared drawer - stays a generic shell rather than a Patrol summary prompt builder. The - Patrol helper may turn those same safe references into visible action - labels and safety notes for finding-level handoffs, but it must not produce - Patrol-authored suggested prompt chips, recommendation titles, - recommendation reasons, or route-owned next-step actions. Patrol finding - prompts, action labels, and safety notes must describe findings, pending - approvals, and governed action references as evidence for the configured - model, not as a frontend-authored decision tree. - Finding-level drawer opens may also pass one bounded - model-only finding context, one target resource reference, and one - `handoff_actions` reference for a live approval or proposed fix. It must not - expose raw command or execution payloads. - The - drawer may render a generic - context-briefing band from `frontend-modern/src/stores/aiChat.ts`, but - feature-owned helpers must provide compact source labels, primary subject, - status, and governed approval/action artifact metadata while keeping - detailed evidence, safety notes, and model-only finding context outside - drawer chrome. Prompt suggestions, attention-reason copy, and - operator-decision framing must stay out of Patrol drawer chrome. - Patrol finding and action-artifact handoffs must not render suggested prompt - chips in the drawer and must not become another primitive path for raw - approval, command, or rollback command payload text. Missing-detail - queued-fix recovery actions must still provide the feature-owned Patrol - briefing and approval-required disclosure rather than opening the - shared drawer as context-free generic Assistant chat. If a feature-owned - expired-approval recovery action still has structured action artifact metadata, - the shared drawer may receive only safe summary fields and command counts; - raw command text remains outside shared Assistant primitives. - When those feature-owned helpers attach backend model-only context, the - drawer store may carry only bounded handoff text and structured resource - references for the shared chat transport; approval, lifecycle, and command - authority remain with the owning runtime surfaces. - Patrol finding handoffs should still provide that briefing from current - finding facts when a durable Patrol investigation record is not attached - yet, rather than opening the shared drawer as empty generic chat. - When the feature helper adds live approval state to the generic drawer - briefing, it may pass only safe approval metadata into - `AIChatContextBriefing`, including generated approval summaries and command - counts when available; raw approval commands remain owned by the governed - approval/remediation panels. If the generic finding-level helper hydrates - latest investigation detail to recover action artifact context, it may pass only - safe summary fields and command counts into the drawer briefing. Shared - approval-required posture must derive its subject from that briefing or - structured finding context, so Patrol handoffs render as Patrol handoffs or - Patrol findings, and alert handoffs render as alert investigations, rather - than generic dashboard briefs. Patrol approval-row Assistant prompts must - route through the same - feature-owned finding handoff helper rather than hand-written prompt-only - drawer opens: safe approval metadata, action artifact summaries, resource - references, and bounded `handoff_actions` may enter the prompt and context, - but raw command text stays out and the handoff must set the drawer's - `autonomousMode:false` disclosure instead of changing the user's persistent - Assistant control level. Patrol remediation-plan drawer handoffs must use the same - primitive boundary: plan title/status/risk, step labels, and command counts - may enter Assistant context; raw command and rollback command payloads must - stay in the governed remediation/action panel. All Patrol finding - discussion handoffs, including context-only findings without a live approval - or proposed fix, must set `autonomousMode:false` as that drawer-only flag - so the drawer shows the approval-required posture the server enforces, - without mutating the persistent Assistant control setting. 14. Keep shared filter primitives coherent with source-owned option hydration. Active platform/runtime pages and Settings infrastructure surfaces must keep canonical options visible in shared filter controls even when current @@ -4546,9 +4359,8 @@ production table, router and styles; it does not qualify full-app scrolling. Proxmox scope resolver as a compatibility fallback. 37. Keep shared commercial upgrade navigation typed and destination-aware. Shared paywall shells and upgrade actions must route internal billing or - cloud destinations through `frontend-modern/src/utils/upgradeNavigation.ts`, - `frontend-modern/src/components/shared/UpgradeLink.tsx`, and - `frontend-modern/src/components/shared/useUpgradeNavigation.ts` instead of + cloud destinations through `frontend-modern/src/utils/upgradeNavigation.ts` + and `frontend-modern/src/components/shared/UpgradeLink.tsx` instead of guessing from labels, hardcoding `target="_blank"`, or calling `window.open(...)` from each feature surface. Inline upgrade links may use `UpgradeLink`; button-styled upgrade CTAs must use `UpgradeButtonLink` so @@ -4828,13 +4640,14 @@ elsewhere. No new member primitive was introduced — vSphere composition reuses the same expandable member-row rendering, status presentation, and merge semantics Proxmox clusters already use. -Assistant availability in the app shell is derived from the +AI availability in the app shell is derived from the `sessionCapabilities.assistantEnabled` security-status capability, and -`aiChatStore.refreshEnabledFromServer()` is the canonical way to re-derive it -mid-session. AI settings save paths (setup modal, enable toggle, Provider & -Models save) must call it after a successful save so assistant entry points -appear or disappear without a full reload; no surface may flip -`aiChatStore.setEnabled` from settings state directly. +`aiAvailabilityStore.refreshEnabledFromServer()` is the canonical way to +re-derive it mid-session. AI settings save paths (setup modal, enable toggle, +Provider & Models save, provider removal) and organization switches must call +it so Patrol entry points and alert Investigate appear or disappear without a +full reload; no surface may flip `aiAvailabilityStore.setEnabled` from settings +state directly. Patrol `fix_rejected` presentation is owned by the Patrol/AI finding surfaces that render the governed-action loop, while the surrounding badge, button, @@ -5042,18 +4855,6 @@ that mention the Infrastructure settings destination now consume `frontend-modern/src/utils/infrastructureSettingsPresentation.ts` for the canonical `Settings → Infrastructure` label and source-strategy copy. Shared primitives must not fork that string or revive removed nested route labels. -The shared Assistant drawer owns compact source-named approval posture for -governed handoffs. Patrol handoffs render as Patrol, and alert plus alert -incident timeline handoffs render as alert investigations rather than dashboard -briefs. Those same drawer handoffs may carry model-only chat context and -resource references to the backend, but the drawer remains a presentation and -transport owner rather than the source of approval or execution truth. Patrol -briefings must stay simple: source, status, one primary subject, and an optional -safe route link. They must not render remediation step lists, evidence chips, -command summaries, or suggested-prompt chips as drawer chrome. If a -feature-owned briefing includes a safe route-owned `actionHref`, the drawer may -render the briefing action label as a normal app link; that link is navigation -guidance only and must not become approval or execution authority. `SettingsTab` no longer includes `infrastructure-connections` or `infrastructure-install`. The single `infrastructure-systems` entry in @@ -5207,8 +5008,7 @@ storage-only live chart primitive for the same telemetry. That same shared-primitive floor now also owns upgrade-navigation semantics. `frontend-modern/src/utils/upgradeNavigation.ts` is the canonical typed internal-vs-external destination helper, while -`frontend-modern/src/components/shared/UpgradeLink.tsx` and -`frontend-modern/src/components/shared/useUpgradeNavigation.ts` own how shared +`frontend-modern/src/components/shared/UpgradeLink.tsx` owns how shared paywall surfaces navigate those destinations. Feature shells may request a commercial destination, but they must not re-decide whether that destination opens in-app or in a new tab once the shared primitive exists. @@ -5755,8 +5555,8 @@ interaction across guest, node, Docker/unified-resource, Ceph cluster, Proxmox Mail Gateway, and shared inline detail panels: pressing anywhere in the subject row collapses the detail, Enter/Space work through the native button, and lifecycle actions do not bubble into collapse. 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. +in object drawer headers: raw context export must not compete with the +operational reading path. `frontend-modern/src/components/Workloads/MetricMiniSparkline.tsx` and `frontend-modern/src/features/proxmox/BackupActivityChart.tsx` render their hover tooltips through `frontend-modern/src/components/shared/TooltipPortal.tsx` @@ -6541,20 +6341,8 @@ builds and filters command rows, holds selected menu state via `createFooterMenuState`, resets selection as the query changes, and routes keyboard movement and selection through `handleKey`. Pulse adapts that contract by keeping command construction in the shared palette model and command -execution in the shared palette state / Assistant store instead of moving +execution in the shared palette state instead of moving editor-specific command routing into the render shell. -Assistant command-palette actions are shell requests, not duplicated chat -logic. New session, session picker, model picker, Undo, and Redo commands must -flow through the shared `frontend-modern/src/stores/aiChat.ts` command request -contract so the drawer owns disabled/loading state, prompt restoration, and -notifications while the palette remains only a searchable command surface. -The command-palette Assistant open command is also contextual shell routing: -`frontend-modern/src/components/shared/useCommandPaletteState.ts` must derive -current-view context through `frontend-modern/src/utils/assistantPageContext.ts` -and pass that context into `aiChatStore.open(...)`, while -`frontend-modern/src/components/shared/commandPaletteModel.ts` owns the -corresponding `Ask about ` label. It must not fall back to an empty -generic Assistant open action. The shared search field now follows that same owner split. `frontend-modern/src/components/shared/SearchField.tsx` stays the render shell, `frontend-modern/src/components/shared/useSearchFieldState.ts` owns focused- @@ -7937,7 +7725,6 @@ representative top-level panels such as `frontend-modern/src/components/Settings/AISettings.tsx`, `frontend-modern/src/components/Settings/AIModelSelectionSection.tsx`, `frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx`, -`frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx`, `frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx`, `frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx`, `frontend-modern/src/components/Settings/AISettingsDialogs.tsx`, and @@ -7945,13 +7732,12 @@ representative top-level panels such as the canonical AI settings runtime boundary. `AISettings.tsx` is the shell, `frontend-modern/src/components/Settings/useAISettingsState.ts` owns the runtime lifecycle and persistence flow, model/provider setup now routes -through `AIModelSelectionSection.tsx`, discovery, budget, timeout, and -permission controls route through `AIRuntimeControlsSection.tsx`, chat -maintenance routes through `AIChatMaintenanceSection.tsx`, and readiness plus +through `AIModelSelectionSection.tsx`, budget and timeout controls route +through `AIRuntimeControlsSection.tsx`, and readiness plus save/test actions route through `AISettingsStatusAndActions.tsx`. `AISettingsStatusAndActions.tsx` may expose provider connection status and test actions only for the Provider & Models page; section pages reuse the save -bar without making Patrol or Assistant look like provider setup +bar without making Patrol or External agents look like provider setup screens. Future AI settings work must extend those section owners instead of re-inlining large runtime subsections into the shell. Provider-specific settings fields inside @@ -8240,34 +8026,18 @@ activation/recovery routes may still render their owned content, but sidebar discovery, trial CTAs, plan-review links, plan upsells, and feature upgrade links must not appear unless an explicit handoff, hosted-mode policy, or active entitlement says they should. -That same shared app-shell boundary now also owns assistant bootstrap silence -on non-AI routes. `frontend-modern/src/useAppRuntimeState.ts`, -`frontend-modern/src/App.tsx`, -`frontend-modern/src/stores/aiChat.ts`, -`frontend-modern/src/components/AI/Chat/index.tsx` must treat +That same shared app-shell boundary now also owns AI bootstrap silence on +non-AI routes. `frontend-modern/src/useAppRuntimeState.ts`, +`frontend-modern/src/App.tsx`, and +`frontend-modern/src/stores/aiAvailability.ts` must treat `/api/security/status.sessionCapabilities.assistantEnabled` as the only -general-route assistant availability fact, while closed assistant chrome and -non-AI settings panels stay off `/api/settings/ai` and `/api/ai/*` until an -owned assistant or Patrol surface is actually open. `frontend-modern/src/stores/aiChat.ts` -must therefore stay presentation-only with respect to assistant bootstrap: -org-switch cleanup, keyboard focus, drawer state, and local context/session -persistence belong there, while backend settings/model reads stay on +general-route AI availability fact, while non-AI settings panels stay off +`/api/settings/ai` and `/api/ai/*` until an owned Patrol surface is actually +open. Backend settings/model reads stay on `frontend-modern/src/stores/aiRuntimeState.ts`. The governed browser proof in `tests/integration/tests/11-first-session.spec.ts` must continue to assert -that plain settings routes render without assistant bootstrap traffic or -console noise. -When an owned Patrol or alert surface attaches a source-named Assistant -handoff, that same drawer shell must keep the empty conversation state aligned -with the attached briefing as neutral `Context attached` copy instead of -rendering generic cluster/system starter prompts or feature-authored suggested -prompt chips below the source-owned context. -The global Assistant launcher and the command-palette Assistant open command -follow the same contextual rule. They must derive current-view context through -`frontend-modern/src/utils/assistantPageContext.ts`, label the action as asking -about the current monitoring, Patrol, alerts, or settings view, and open the -drawer with that `pulse-view` context attached. They must not call -`aiChatStore.toggle()` or `aiChatStore.open()` without context from the -authenticated shell, because that reintroduces a generic Assistant front door. +that plain settings routes render without AI bootstrap traffic or console +noise. Shared table, disclosure, and form primitives must also stay explicitly typed at the browser edge. Summary rows may memoize repeated pending-update reads, shared buttons must preserve discriminated disclosure props, toggle and a11y @@ -10076,3 +9846,13 @@ one shape. A host table that scopes keeps its details chevron on phones and takes the `platform-table-mobile-w-38` identity column (Uptime drops to 10%), the same allowance the Proxmox node column has for the toggle and dot before the name. + +The browser Assistant chat window was retired on 2026-10-22, taking its +drawer, launcher, command-palette entries, chat action-icon and copy shells, +and the Assistant hand-off buttons on alerts, Patrol, and resource drawers +with it. `frontend-modern/src/stores/aiAvailability.ts` now holds the shell's +AI-enabled fact that the chat store used to carry, and the Patrol tool +transcript block moved to `frontend-modern/src/components/patrol/` with its +`CopyValueButton` composition intact. The External agents settings page lives +at `/settings/pulse-intelligence/agents`; the old `/assistant` path redirects +there. diff --git a/docs/release-control/v6/internal/subsystems/organization-settings.md b/docs/release-control/v6/internal/subsystems/organization-settings.md index 39fe9efd26..632c931959 100644 --- a/docs/release-control/v6/internal/subsystems/organization-settings.md +++ b/docs/release-control/v6/internal/subsystems/organization-settings.md @@ -438,3 +438,12 @@ store initialization, migration, and read failures stay visible as as an organization with no roles or users. Local identities and opaque stable SSO principals are both valid assignment subjects, including principals with an intentionally empty role set. + +The Pulse Intelligence settings panel that hosts Pulse MCP setup is the +External agents panel since 2026-10-22: `settingsPanelRegistryContext.tsx` +registers `AgentIntegrationsPanel` under the `system-ai-agents` tab at +`/settings/pulse-intelligence/agents`, replacing the retired Assistant +settings page. The old `system-ai-assistant` tab id and +`/settings/pulse-intelligence/assistant` path redirect there, so saved links +keep working. Organization-scoped settings ownership, RBAC gating, and the +settings shell's organization presentation rules are unchanged. diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index c57eaea0db..2c4eab0e47 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -178,7 +178,6 @@ sources, and retains the note as operator context. 27. `frontend-modern/src/utils/patrolRuntimePresentation.ts` 28. `frontend-modern/src/utils/patrolRuntimeActions.ts` 29. `frontend-modern/src/utils/textPresentation.ts` -30. `tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts` 31. `tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts` 32. `frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx` 33. `frontend-modern/src/stores/patrolAttention.ts` @@ -832,7 +831,6 @@ attention`, `approval needed`, `outcome verified`, `no active work`) instead 5. Add or change Patrol header schedule and runtime presentation through `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx`, `frontend-modern/src/utils/aiPatrolSchedulePresentation.ts`, and `frontend-modern/src/utils/patrolRuntimePresentation.ts`. Patrol must not surface retired hosted-model credit badges or trial-like activation prompts in the normal self-hosted GA app, even when legacy transport fields are still present. 6. Keep Patrol and chat identifier-label presentation aligned through the shared `frontend-modern/src/utils/textPresentation.ts` -7. Keep Patrol and chat stream-matching / mention dedupe aligned through the shared `frontend-modern/src/utils/chatIdentifiers.ts` 8. Keep Patrol transport and payload changes aligned through the governed AI runtime and API contract transport surfaces ## Forbidden Paths @@ -1003,158 +1001,27 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or not as a broad automation promise. 8. Keep the Patrol store aligned with the shared structured investigation record when transport carries one. `frontend-modern/src/stores/aiIntelligence.ts` - may retain `investigationRecord` as data for Assistant handoff and Patrol - presentation, but visible Patrol copy and Assistant handoff context - must flow through the governed Patrol investigation-context helpers. Those - helpers also own the Assistant drawer briefing content for Patrol records, - including the rule that action artifact commands are summarized by count only - and never rendered as raw command text in the handoff surface. When Patrol - hands a pending fix or approval into Assistant, the shared handoff may retain - only structured action references such as approval ID, status, request/expiry - timestamps, action plan identity, fix ID, risk, target, and resource - identity; approval and execution authority stays with the governed - approval/remediation surfaces. Assistant may refresh the referenced - approval's current status for review, and the backend handoff builder must - recover a live pending Patrol investigation-fix approval by finding ID when - the durable record lacks the current approval reference, but Patrol - presentation must still keep command payloads inside governed - approval/remediation context rather than rendering them as handoff copy. - Assistant's governed action artifact metadata must derive from the same - structured handoff action after that recovery, so the briefing cannot - contradict the action reference passed to chat execution while still leaving - next-step reasoning to the configured LLM. - Assistant may also hydrate the referenced action plan or action audit so the - handoff explains current action lifecycle state, requester, capability, - approval policy, plan expiry, preflight/dry-run posture, and terminal - success/failure without treating approval as execution authority or exposing - raw command/execution payloads. Assistant may also - enrich that same handoff with refreshed unified finding and - investigation-record state, canonical resource-policy guidance, current - canonical resource-state and capability context, canonical - resource-relationship context, and recent canonical resource-timeline changes - for explanation; Assistant resolves those state, topology, and timeline - lookups through the current canonical unified-resource model before falling - back to handoff IDs where compatibility requires it. - When a Patrol finding declares root-cause or correlated finding IDs, the - Assistant handoff may also resolve those related findings through the current - unified finding store and summarize them as model-only context. Those - related finding summaries must include current recency and latest lifecycle - facts when present, and may seed their own structured resources for the same - canonical policy, state, topology, and timeline hydration, but Patrol - presentation must treat them as explanation for the current finding rather - than approval, lifecycle, disclosure, or execution authority. - Assistant handoffs from Patrol findings must also include a concise factual - finding briefing derived from the unified finding and structured - investigation record before the detailed finding context, so Assistant - receives current risk, recency, evidence snapshot, verification summary, - conclusion, latest lifecycle event, and governed approval/action artifact - metadata instead of behaving like a generic chat over a pasted incident dump. - Patrol is the scheduled - probe, context assembler, and execution-governance owner; the configured LLM - is the diagnostic and remediation-reasoning owner. Patrol handoffs may - provide system context, resource posture, action posture, and governed tools, - but ordinary context attachment must not synthesize, pre-fill, or submit - chat prompts. The explicitly labelled Explain action starts the shared - evidence-first Assistant request with the selected attention record, bounded - evidence, resource references, and the canonical finding handoff when exactly - one finding is linked. Ambiguous links must not select an arbitrary finding. - Neither path may force active tool use, name a required tool path, show - suggested prompt chips, or present - a Patrol-authored remediation answer for the LLM to execute. Patrol - runs must still call the configured model when deterministic triage is quiet, - and unmatched deterministic signals may be returned as context for another - model pass but must not be converted into Pulse-authored findings after the - model declines to report them. Legacy unified AI-finding integration must - likewise store model-reported findings as evidence and must not auto-generate - remediation plans from finding metadata. - Visible Patrol - Assistant handoffs must not make the operator think Pulse has already - produced the correct fix. The visible Assistant drawer briefing opened from a Patrol - finding must be compact and source-named: current status/risk, one primary - subject, and any approval-required boundary, with richer evidence, action - artifacts, and command counts staying in model-only or governed action - context rather than drawer chrome; prompt suggestions must not be generated - for Patrol handoffs. When a - structured investigation record is not available yet, the same Patrol-owned - helper must still brief the operator from current finding facts such as - active status, severity, recurrence, and loop state instead of opening a - generic empty Assistant drawer. When a live pending Patrol approval exists - for that finding, the visible Assistant briefing may include only safe - approval metadata such as approval ID, pending status, risk, requested time, - expiry, target label, generated approval summary, and command count; it must - not copy the approval command payload into Assistant drawer prose. The - model-only runtime briefing must apply that same - recovered approval reference when adding factual governed action artifact - metadata. The initial prompt for a Patrol finding may include governed action - context when safe metadata is attached, but it must frame that context as - input for the LLM to evaluate rather than as a Patrol-authored remediation - answer. Finding - handoffs must be assembled through the Patrol-owned handoff model so the - prompt, visible briefing, model-only finding context, resource reference, - bounded action reference, and drawer approval-required disclosure stay in - sync. The model-only context may include - current finding status, recurrence, investigation record facts, evidence, - verification, approval state, dry-run posture, existing action artifact - summary, target resource references, and governed action references without - raw command payloads. Patrol action summaries in - `frontend-modern/src/components/patrol/ApprovalSection.tsx` that open - Assistant must follow that same Patrol-owned handoff model rather than a - prompt-only local shortcut: pass approval ID/status/risk/target plus safe - summary/count metadata as review context, attach the target resource - reference, include bounded `handoff_actions` for live approvals or structured - action artifacts when present, mark the drawer's approval-required - disclosure, attach the Patrol-owned visible drawer briefing for the pending approval or - queued-fix recovery state, and never paste the approval command or - action command text into the chat prompt. Existing remediation-plan or - action-plan artifacts follow the same boundary: plan status, risk, and - command counts are allowed as non-authoritative governed action posture for - the LLM to critique, but visible handoffs must not render Patrol step lists - or suggested prompt chips, and command or rollback command text stays in the - governed remediation or approval surface. Generic finding - discussion handoffs for any non-empty Patrol `finding_id`, including - context-only findings and findings that reference a live approval, action - artifact, fix outcome, or remediation plan, run in the approval-required - mode `/api/ai/chat` sets server-side for every request; the browser sends no - execution-mode field, so default autonomous Assistant settings cannot bypass - the Patrol action-governance boundary. The assembled handoff must still pass - through the Assistant runtime's resource-policy sanitizer before prompt - injection, so Patrol-owned prose - cannot leak governed resource names, IDs, aliases, nodes, paths, or - addresses outside the canonical policy boundary. - If a `fix_queued` finding no longer has a live approval payload or detailed - action artifact payload available, the recovery action must still open Assistant - with the same Patrol-owned visible briefing and approval-required posture - from current finding facts; it must not fall back to generic investigation - chat or invite execution from missing command details. - Patrol run-history Assistant handoffs are a separate run-context surface: - the visible drawer must keep the `Patrol run attached` headline, may show the - scoped run subject, safe runtime-failure/action label, and classified - redacted failure summary, and must continue to omit raw provider payloads, - command text, and remediation instructions. - If the live approval is gone but the structured action artifact payload is still - available, the recovery Assistant briefing must carry only safe action artifact - metadata such as description, target, risk, rationale, destructive posture, - and command count. Raw command text remains in the governed remediation or - approval panel, while Assistant gets enough context to explain approval - recovery and risk without becoming an execution surface. - Generic finding-level Assistant handoffs must use that same safe metadata - boundary when the list response lacks a full investigation record: they may - hydrate the latest investigation session to recover action artifact summary, - risk, target, rationale, destructive posture, and command count, but they must - still keep command text out of both the user-authored prompt and visible - Assistant briefing. - If the referenced finding is no longer current, Assistant must drop the - stored handoff instead of continuing from stale Patrol context. Assistant - handoff context must also carry the unified - finding's current lifecycle and recency facts, including active/resolved/ - snoozed/dismissed/suppressed state, detection/last-seen/resolution - timestamps, recurrence/regression, and recent lifecycle events, rather than - treating the investigation conclusion as the whole current record. Detailed - lifecycle events in Assistant handoff context must stay bounded, model-only, - and explicitly separated from approval/execution authority. Patrol must keep - the visible finding and drawer briefing tied to the shared investigation - payload rather than forking a Patrol-local lifecycle, policy, topology, or - timeline summary. The inline Patrol investigation surface must also treat a + may retain `investigationRecord` as data for Patrol presentation, and visible + Patrol copy must flow through the governed Patrol investigation-context + helper (`buildPatrolInvestigationRecordPresentation` in + `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`), + which summarizes action artifact commands by count only and never renders + raw command text. Approval and execution authority stays with the governed + approval/remediation surfaces. The finding, approval, remediation-plan, + run-history, and assessment hand-offs into the browser Assistant drawer + that this obligation used to govern were retired with the chat window on + 2026-10-22, and Patrol surfaces must not grow a chat hand-off back. The + backend chat runtime keeps its finding-context rules for clients that still + send a Patrol `finding_id` (see `ai-runtime`). + Patrol is the scheduled probe, context assembler, and execution-governance + owner; the configured LLM is the diagnostic and remediation-reasoning owner. + Patrol runs must still call the configured model when deterministic triage + is quiet, and unmatched deterministic signals may be returned as context for + another model pass but must not be converted into Pulse-authored findings + after the model declines to report them. Legacy unified AI-finding + integration must likewise store model-reported findings as evidence and must + not auto-generate remediation plans from finding metadata. + The inline Patrol investigation surface must also treat a structured `investigationRecord` as investigation data, even when the legacy investigation-detail endpoint has no separate session payload, so it must not render empty-state copy above a durable Patrol record. The Patrol-owned @@ -1164,11 +1031,10 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or renderer surfaces an explicit `Impact not assessed` placeholder whenever an investigation record exists with no impact text, and a parallel `Rollback not specified` placeholder whenever rollback is empty, so the - operator-visible gap is conspicuous rather than hidden. The Patrol - finding briefing card built by + operator-visible gap is conspicuous rather than hidden. The record + presentation built by `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` - may include populated impact text in its bounded detail lines but must - omit the placeholder copy from that compact card so it stays tight, + may carry populated impact text but must omit the placeholder copy, leaving placeholder rendering to the full investigation surface. `frontend-modern/src/components/AI/FindingsPanel.tsx` is the canonical renderer for that operator-facing impact text on the unified findings @@ -1177,7 +1043,7 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or On non-Patrol findings views, the same panel may surface `investigation_record.confidence` as a collapsed-row badge beside the investigation outcome. The default Patrol page keeps that process evidence in - the expanded issue, run history, or Assistant context so current issue rows + the expanded issue or run history so current issue rows stay readable. Finding rows must keep their disclosure control separate from inline manual controls: the row summary may be a keyboard-accessible expand/collapse @@ -1187,8 +1053,7 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or `FindingsPanel.tsx` also renders the `previousResolvedFixSummary` operational-memory field directly on the expanded finding card with emerald-accented styling, so - operators see "what worked last time" inline without having to - open Assistant. The summary lives on the finding shell + operators see "what worked last time" inline. The summary lives on the finding shell (`UnifiedFinding`/`Finding`), not on the per-record investigation presentation; the Patrol-owned investigation-context model must not absorb it into impact, verification, or rollback (those represent @@ -1212,31 +1077,22 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or compact status text. New trust categories must extend the `FindingsTrustSummary` contract first rather than inventing per-page keys. - Findings carry contextual Assistant entry points keyed on intent: the - default "Discuss with Assistant" button opens an open-ended chat - handoff, and a parallel "Explain" button opens the same handoff with - a `PatrolAssistantFindingIntent='explain'` seed that asks the LLM to - walk through what we know, why it matters, how confident the - analysis is, what remains uncertain, and what the model would do next. Both - buttons must route through `buildPatrolAssistantFindingHandoff` so - the structured context (investigation record, operational memory, - pending approval, proposed fix, next-step action) is attached - uniformly; only the leading sentence differs by intent. The - badge palette is provided by `getInvestigationConfidenceBadgeClasses` - in `frontend-modern/src/utils/aiFindingPresentation.ts`: high is - reassuringly emphasized, medium is neutral, low is a soft amber. - Findings without an investigation record (or without a recorded - confidence) must show no confidence badge rather than defaulting to - one, mirroring the impact rule that absent metadata is not fabricated. - Expanded Patrol finding cards must keep action density product-grade: one - primary Assistant intent button is visible inline (`Investigate`, `Verify -fix`, or `Explain` based on current finding state), while secondary - Assistant intents and operator management controls live behind compact - in-flow `Assistant` and `Manage` menus. Those menus must render inside the - expanded finding detail rather than as clipped floating panels, and the + Expanded Patrol finding cards keep action density product-grade: one + primary action is visible inline (the finding's governed primary action + when it has one, otherwise `Investigate` for an un-investigated active + finding, which starts Patrol's investigation through + `reinvestigateFinding`), while operator management controls live behind a + compact in-flow `Resolve or dismiss` menu. That menu must render inside the + expanded finding detail rather than as a clipped floating panel, and the per-finding "Create rule from this" action must remain disabled unless the finding has both a concrete resource and category for a scoped suppression - rule. + rule. The confidence badge palette is provided by + `getInvestigationConfidenceBadgeClasses` in + `frontend-modern/src/utils/aiFindingPresentation.ts`: high is reassuringly + emphasized, medium is neutral, low is a soft amber. Findings without an + investigation record (or without a recorded confidence) must show no + confidence badge rather than defaulting to one, mirroring the impact rule + that absent metadata is not fabricated. An actionable active finding must always expose a forward path into the governed action loop at its own expanded row. Above Watch only, that is the inline `Investigate` intent. In Watch only, the same expanded-row slot @@ -1272,35 +1128,11 @@ fix`, or `Explain` based on current finding state), while secondary than dropping it; runtime-failure findings produced by `internal/ai/patrol_runtime_failure.go` are the first source to depend on that path reaching the Patrol page surface. - Patrol no longer opens a whole-surface assessment handoff: the summary card - that offered it and its builder in - `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` are - gone. Assistant sessions saved from that handoff still restore through the - shared drawer as `handoff_metadata.kind=patrol_assessment` with the - `patrol-assessment` target and `targetId=pulse-patrol-assessment`, never as - generic scoped context or as a single-finding session because one bounded - action reference names a finding. Saved assessment and finding sessions must - not expose or restore Patrol-authored next-step titles, recommendation - detail, action labels, or app-route hrefs through `handoff_summary`; legacy - stored recommendation fields are ignored rather than converted into hidden - context. - Patrol run-history entries may also open Assistant for a selected run, but - that handoff must flow through the same Patrol-owned investigation-context - model rather than a row-local prompt. The browser-visible prompt and drawer - briefing may include only classified, redacted runtime-failure summaries and - safe run identity facts. The browser request must send safe - `handoff_metadata` for the saved-session identity envelope: kind - `patrol_run`, run ID, safe run type/status, and a runtime-failure boolean, - never provider-bound runtime failure detail, analysis text, tool traces, raw - provider payloads, or scoped resource context. Backend Assistant runtime owns - the model-only run context: it resolves the run ID from Patrol history, - rebuilds bounded run facts, scoped resource references, sanitized analysis, - and classified failure detail server-side, and rehydrates the same context - from stored metadata on follow-up turns. The exchange runs in the - server-set approval-required mode; the handoff must present a source-named - visible drawer briefing and - frame Assistant as explanation and next-step review rather than execution or - automatic retry authority. + Assistant sessions saved from the retired Patrol assessment, finding, and + run hand-offs may still exist server-side. Their `handoff_summary` must not + expose or restore Patrol-authored next-step titles, recommendation detail, + action labels, or app-route hrefs; legacy stored recommendation fields are + ignored rather than converted into hidden context. 9. Keep the normal Patrol status summary plain and operator-first rather than a hero-style or decorative card surface. The default page renders no status summary card or assessment strip today: the header control row @@ -1751,8 +1583,7 @@ Proxmox monitoring lens before Patrol or Assistant chrome, while the stable Patrol tab may still expose the open-work count. Calm-day copy such as `No current issues` and checked-resource coverage belongs inside the Patrol route after the operator chooses Patrol; the monitor lens must not become a hidden -Patrol all-clear page, and Assistant launchers must stay scoped to the current -surface. +Patrol all-clear page. The readiness contract now applies before Patrol work is admitted, not only after a page render: recoverable Patrol provider/model settings saves must persist and echo structured readiness cause metadata, manual run requests must @@ -1942,10 +1773,8 @@ model`), and that action must use the canonical Pulse Intelligence route `/settings/pulse-intelligence/patrol` rather than the legacy `/settings/system-ai` compatibility alias. Runtime/setup findings in the default current-issues flow must stay primary-action-only: they should not add -a visible `Open in Assistant` secondary button or a generic `Manage` menu -beside the direct setup action. -Assistant can still receive runtime context from the broader Patrol/Assistant -handoff paths, but a setup impairment should not read as a menu of workflows. +a secondary chat button or a generic `Manage` menu beside the direct setup +action; a setup impairment should not read as a menu of workflows. When setup is the only active Patrol work, the Patrol page should collapse that state into one `Fix Patrol setup` workspace task with the direct `Open Provider & Models` action; the readiness banner must not duplicate that @@ -2948,3 +2777,13 @@ handoff. The attached Assistant context suppresses unrelated empty-chat starters and recent sessions. Starting a new conversation clears that context and restores the ordinary welcome surface. Regression and current-browser qualification are recorded in the customer-journey qualification document. + +Patrol stopped handing work to the browser Assistant on 2026-10-22, when the +chat window was retired. Finding cards, approval rows, run history, and the +attention workbench lost their "Discuss", "Explain with Assistant", and +"Ask Assistant" actions; the inline `Investigate` action (a Patrol +re-investigation) and the governed approval controls stay. Alert Investigate +now starts a targeted Patrol run, so alert follow-up lands on the Patrol page. +`patrolInvestigationContextModel.ts` shrank to the investigation-record +presentation that `InvestigationSection` uses, and the Patrol tool transcript +block moved to `frontend-modern/src/components/patrol/ToolExecutionBlock.tsx`. 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 d1e21cc5bd..db424bbe60 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -1920,30 +1920,16 @@ path. `frontend-modern/src/pages/Dashboard.tsx`, surfaces for KPI cards, problem-resource rows, or top-infrastructure identities. New summary cards must live on their owning product route and prove their data path there. -32. Keep assistant and other globally mounted consumers off deleted dashboard -summary state. Globally mounted helpers such as -`frontend-modern/src/components/AI/Chat/index.tsx` must read the live websocket snapshot or existing unified-resource cache -rather than forcing root navigation to pay for a replacement dashboard -overview transport. When the assistant shell changes presentation, -`frontend-modern/src/utils/aiChatPresentation.ts` must remain the canonical -owner for launcher, drawer, session-menu, and empty-state copy so hot-path -consumers do not grow one-off inline strings or extra state branches alongside -the mounted shell. Blocking shared dialogs must also suppress closed assistant -affordances through the shared dialog runtime instead of leaving the mounted -shell clickable behind another overlay. - Approval presentation inside that mounted assistant shell must stay - state-local to the existing drawer/session state and backend approval - endpoints. Deny/skip failure handling may preserve the pending approval - card, but it must not add polling, resource hydration, or mounted-shell work - to recover UI state. - That same mounted-shell hot path must protect usable width on constrained - viewports. When the shared assistant drawer opens inside - `frontend-modern/src/components/AI/Chat/index.tsx`, it may not shrink the - infrastructure or dashboard operating surface below a workable narrow-width - floor just because the assistant stays mounted in the app shell; below the - canonical dock threshold, the assistant must switch to an overlay drawer so - table filters, grouped rows, and other hot-path controls keep their - existing layout budget instead of paying a second collapse cost. +32. Keep globally mounted consumers off deleted dashboard summary state. + Globally mounted shell helpers must read the live websocket snapshot or + the existing unified-resource cache rather than forcing root navigation to + pay for a replacement dashboard overview transport, and must not grow + one-off inline strings or extra state branches on that hot path. The + browser Assistant drawer this obligation was written for was retired on + 2026-10-22, so no chat shell stays mounted in the app shell any more; a + future globally mounted AI surface inherits this same budget, including + the rule that it may not shrink a primary operating surface below a + workable width. 34. Keep the retired dashboard page-header path absent from the compact hot path. New page headers must stay pure presentation on their owning route and must not introduce a second data load, widen suspense ownership, or @@ -4426,3 +4412,14 @@ windowing budget, runway and mounted-row counts are unchanged. The Docker host drawer loads its history charts lazily inside `Suspense` like the node and guest drawers, keeping them out of the table chunk, and its action history is fetched only when Manage opens. + +The app shell lost its largest globally mounted consumer on 2026-10-22, when +the browser Assistant chat window was retired: the drawer, its launcher, and +its lazily loaded `Chat` and `vendor-highlight` chunks are gone, and the +production build ships roughly 100 kB less gzipped JavaScript than the main it +branched from. `useAppRuntimeState.ts` now seeds AI availability through +`frontend-modern/src/stores/aiAvailability.ts`, and the Workloads keyboard +handler in `useWorkloadsControlsState.ts` no longer checks for a focused chat +composer. `.bundlesize.json` drops the retired chunks; `useUnifiedResources` +grew to about 10 kB gzipped because Rollup moved the shared filter toolbar +modules into it once the `Chat` chunk stopped sharing them. diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 46a159ec6d..0ef63092d2 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -49,14 +49,6 @@ "api-contracts" ] }, - { - "path": "frontend-modern/src/api/aiChat.ts", - "rationale": "the Assistant chat frontend client is both the first-party Assistant transport surface and a canonical API payload contract boundary", - "subsystems": [ - "ai-runtime", - "api-contracts" - ] - }, { "path": "frontend-modern/src/api/generated/agentCapabilities.ts", "rationale": "the generated agent capabilities frontend types are both the Pulse Intelligence manifest TypeScript projection and a canonical API payload contract boundary", @@ -129,6 +121,14 @@ "deployment-installability" ] }, + { + "path": "frontend-modern/src/api/workflowPrompts.ts", + "rationale": "the workflow prompt activity client is both the AI runtime workflow-prompt telemetry surface and a canonical API payload contract boundary", + "subsystems": [ + "ai-runtime", + "api-contracts" + ] + }, { "path": "frontend-modern/src/components/CommercialMigrationBanner.tsx", "rationale": "the global commercial migration notice is both a cloud-paid entitlement recovery surface and a shared app-shell notice primitive consumer", @@ -420,8 +420,8 @@ ] }, { - "path": "frontend-modern/src/stores/aiChat.ts", - "rationale": "the assistant drawer and session store is both an AI runtime control surface and a canonical app-shell presentation boundary", + "path": "frontend-modern/src/stores/aiAvailability.ts", + "rationale": "the AI availability store is both the AI runtime session capability and the app-shell gate for Patrol entry points", "subsystems": [ "ai-runtime", "frontend-primitives" @@ -2005,7 +2005,6 @@ "owned_prefixes": [ "cmd/patrol-qualify/", "cmd/pulse-mcp/", - "frontend-modern/src/components/AI/Chat/", "internal/agentcapabilities/", "internal/ai/", "internal/mutationregistry/", @@ -2018,15 +2017,13 @@ "docs/AI_PATROL_QUALIFICATION.md", "frontend-modern/src/api/agentCapabilities.ts", "frontend-modern/src/api/ai.ts", - "frontend-modern/src/api/aiChat.ts", "frontend-modern/src/api/generated/agentCapabilities.ts", "frontend-modern/src/api/patrol.ts", + "frontend-modern/src/api/workflowPrompts.ts", "frontend-modern/src/components/AI/AICostDashboard.tsx", "frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx", - "frontend-modern/src/stores/aiChat.ts", + "frontend-modern/src/stores/aiAvailability.ts", "frontend-modern/src/stores/aiRuntimeState.ts", - "frontend-modern/src/utils/aiChatPresentation.ts", - "frontend-modern/src/utils/aiControlLevelPresentation.ts", "frontend-modern/src/utils/aiCostPresentation.ts", "frontend-modern/src/utils/aiProviderHealthPresentation.ts", "frontend-modern/src/utils/aiProviderPresentation.ts", @@ -2165,8 +2162,8 @@ "match_prefixes": [], "match_files": [ "frontend-modern/src/api/ai.ts", - "frontend-modern/src/api/aiChat.ts", "frontend-modern/src/api/patrol.ts", + "frontend-modern/src/api/workflowPrompts.ts", "internal/api/ai_handler.go", "internal/api/ai_handlers.go", "internal/api/ai_intelligence_handlers.go" @@ -2207,22 +2204,17 @@ }, { "id": "ai-chat-runtime-read-model", - "label": "AI chat and shared runtime read model proof", - "match_prefixes": [ - "frontend-modern/src/components/AI/Chat/" - ], + "label": "AI availability and shared runtime read model proof", + "match_prefixes": [], "match_files": [ - "frontend-modern/src/stores/aiChat.ts", + "frontend-modern/src/stores/aiAvailability.ts", "frontend-modern/src/stores/aiRuntimeState.ts" ], "allow_same_subsystem_tests": false, - "test_prefixes": [ - "frontend-modern/src/components/AI/Chat/__tests__/" - ], + "test_prefixes": [], "exact_files": [ "frontend-modern/src/components/AI/__tests__/AICostDashboard.test.tsx", - "frontend-modern/src/stores/__tests__/aiChat.test.ts", - "tests/integration/tests/42-vmware-ai-chat-read-recovery.spec.ts" + "frontend-modern/src/stores/__tests__/aiAvailability.test.ts" ] }, { @@ -2230,16 +2222,12 @@ "label": "AI presentation helper proof", "match_prefixes": [], "match_files": [ - "frontend-modern/src/utils/aiChatPresentation.ts", - "frontend-modern/src/utils/aiControlLevelPresentation.ts", "frontend-modern/src/utils/aiProviderHealthPresentation.ts", "frontend-modern/src/utils/aiProviderPresentation.ts" ], "allow_same_subsystem_tests": false, "test_prefixes": [], "exact_files": [ - "frontend-modern/src/utils/__tests__/aiChatPresentation.test.ts", - "frontend-modern/src/utils/__tests__/aiControlLevelPresentation.test.ts", "frontend-modern/src/utils/__tests__/aiProviderHealthPresentation.test.ts", "frontend-modern/src/utils/__tests__/aiProviderPresentation.test.ts", "frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts" @@ -2418,7 +2406,6 @@ "frontend-modern/src/utils/alertSchedulePresentation.ts", "frontend-modern/src/utils/alertSeverityPresentation.ts", "frontend-modern/src/utils/alertTabsPresentation.ts", - "frontend-modern/src/utils/alertTargetTypes.ts", "frontend-modern/src/utils/alertThresholdDefaults.ts", "frontend-modern/src/utils/alertThresholdsPresentation.ts", "frontend-modern/src/utils/alertThresholdsSectionPresentation.ts", @@ -2559,23 +2546,9 @@ "frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts", "frontend-modern/src/pages/__tests__/Alerts.readOnly.test.tsx", "frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts", - "frontend-modern/src/utils/__tests__/alertTargetTypes.test.ts", "scripts/check-incident-request-ownership.mjs" ] }, - { - "id": "alert-target-type-resolution", - "label": "alert target type resolver proof", - "match_prefixes": [], - "match_files": [ - "frontend-modern/src/utils/alertTargetTypes.ts" - ], - "allow_same_subsystem_tests": false, - "test_prefixes": [], - "exact_files": [ - "frontend-modern/src/utils/__tests__/alertTargetTypes.test.ts" - ] - }, { "id": "alert-configuration-presentation-helpers", "label": "alert configuration presentation helper proof", @@ -3027,17 +3000,15 @@ ] }, { - "id": "ai-chat-stream-event-types", - "label": "AI chat SSE generated type proof", + "id": "generated-api-types", + "label": "generated API type proof", "match_prefixes": [], "match_files": [ - "frontend-modern/src/api/generated/aiChatEvents.ts", "scripts/generate-types.go" ], "allow_same_subsystem_tests": false, "test_prefixes": [], "exact_files": [ - "frontend-modern/src/api/__tests__/aiChatEvents.test.ts", "internal/api/contract_test.go" ] }, @@ -5408,7 +5379,6 @@ "frontend-modern/src/components/Infrastructure/useTableWindowing.ts", "frontend-modern/src/components/Login.tsx", "frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx", - "frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx", "frontend-modern/src/components/Settings/AIModelSelectionSection.tsx", "frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx", "frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx", @@ -5501,7 +5471,7 @@ "frontend-modern/src/index.css", "frontend-modern/src/pages/Docs.tsx", "frontend-modern/src/routing/routePreload.ts", - "frontend-modern/src/stores/aiChat.ts", + "frontend-modern/src/stores/aiAvailability.ts", "frontend-modern/src/stores/updates.ts", "frontend-modern/src/utils/aiSettingsPresentation.ts", "frontend-modern/src/utils/diagnosticsPresentation.ts", @@ -5684,7 +5654,6 @@ "match_prefixes": [], "match_files": [ "frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx", - "frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx", "frontend-modern/src/components/Settings/AIModelSelectionSection.tsx", "frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx", "frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx", @@ -5862,13 +5831,13 @@ "label": "assistant app-shell bootstrap proof", "match_prefixes": [], "match_files": [ - "frontend-modern/src/stores/aiChat.ts" + "frontend-modern/src/stores/aiAvailability.ts" ], "allow_same_subsystem_tests": false, "test_prefixes": [], "exact_files": [ "frontend-modern/src/__tests__/App.architecture.test.ts", - "frontend-modern/src/stores/__tests__/aiChat.test.ts", + "frontend-modern/src/stores/__tests__/aiAvailability.test.ts", "tests/integration/tests/11-first-session.spec.ts" ] }, @@ -7225,7 +7194,6 @@ "frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts", "frontend-modern/src/utils/__tests__/patrolPagePresentation.test.ts", "tests/integration/tests/18-patrol-runtime-state.spec.ts", - "tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts", "tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts", "tests/integration/tests/91-operational-trust-attention-workbench.spec.ts" ] diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index b866824ecc..27fbd9938c 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -3644,3 +3644,17 @@ weakened. The diagnostic wire/identity and ordinary same-credential recovery controls in `diagnostics_access_test.go`, `diagnostics_additional_test.go`, `contract_test.go` and `client_request_test.go` cover these properties; they do not establish provider or installed acceptance. + +The i18n catalogs dropped the retired Assistant alert hand-off strings on +2026-10-22. The `alerts.assistant.*` keys were replaced by user-visible +`alerts.investigate.*` action labels in English, German, and Spanish, and their +entries left the allowed-identical-translation lists in +`frontend-modern/src/i18n/policy.ts`. No alert identifiers, resource names, commands, or machine-facing +text moved into translated copy, and the untranslated-content policy is +otherwise unchanged. + +The security status `settingsCapabilities.patrolRun` field (2026-10-22) is a +read-only projection of the existing RequireAdmin + `ai:execute` gate on +`POST /api/ai/patrol/run`. It grants nothing: the route still enforces its own +gate, and the field only stops the browser offering alert Investigate to +sessions that route would refuse. diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 1c75958192..1b49b14167 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -2755,7 +2755,7 @@ change. Fake persistent-logger controls in 7. Letting storage `source` aliases or case drift survive in canonical route state; shared storage URLs must rewrite pasted values like `PVE`, `pbs`, or `ALL` to the owned source option values (for example `proxmox-pve`) or the canonical unset state so copied links match the same source filter values the storage toolbar presents 8. Letting explicit storage `all` sentinels survive in canonical route state; shared storage URLs must collapse case- or whitespace-variant `all` values for the managed `node` filter back to the canonical unset state so copied links do not preserve a fake active node filter 9. Reintroducing an aggregate cross-platform Recovery page, a recovery route-state owner, or a shared recovery query serializer in `frontend-modern/src/routing/resourceLinks.ts` without a governed product decision. The aggregate page (`frontend-modern/src/components/Recovery/` with its state owner `frontend-modern/src/features/recovery/useRecoverySurfaceState.ts`) was deleted on 2026-05-26, and `RECOVERY_QUERY_PARAMS`, `parseRecoveryLinkSearch`, and `buildRecoveryRouteSearch` were deleted once nothing read them. Recovery rows render in the platform-owned Proxmox Backups and TrueNAS Protection tabs; the Proxmox Backups tab owns its route state through the `/proxmox/backups/:view` segment and `PROXMOX_BACKUPS_QUERY_PARAMS`, and the TrueNAS Protection tab keeps its search and status filter in component state and its sort in local storage, outside the URL. -10. Letting adjacent route state in shared `frontend-modern/src/routing/resourceLinks.ts` reuse storage or Proxmox Backups query names or alter their parse/build behavior. Workload platform scoping, unified-resource storage deep links, `buildStandalonePath()` platform paths, the Kubernetes page's route-local `KUBERNETES_QUERY_PARAMS.cluster` scope, the Patrol control starter (`patrolControlStarter=patrol_control`, with legacy `operationsLoopStarter=patrol_control`, `operationsLoopStarter=patrol_autonomy`, and `operationsLoopStarter=pulse_pro_activation` as compatibility aliases only), and the Pulse Intelligence external-agent setup anchors (`/settings/pulse-intelligence/assistant#external-agent-setup`, with `/settings/security/api#external-agent-setup` and `/settings/security/api#pulse-mcp-setup` as legacy compatibility hashes) share that module, and none of them is recovery state, storage focus, platform scope, or proof of backup or recovery posture. +10. Letting adjacent route state in shared `frontend-modern/src/routing/resourceLinks.ts` reuse storage or Proxmox Backups query names or alter their parse/build behavior. Workload platform scoping, unified-resource storage deep links, `buildStandalonePath()` platform paths, the Kubernetes page's route-local `KUBERNETES_QUERY_PARAMS.cluster` scope, the Patrol control starter (`patrolControlStarter=patrol_control`, with legacy `operationsLoopStarter=patrol_control`, `operationsLoopStarter=patrol_autonomy`, and `operationsLoopStarter=pulse_pro_activation` as compatibility aliases only), and the Pulse Intelligence external-agent setup anchors (`/settings/pulse-intelligence/agents#external-agent-setup`, with `/settings/security/api#external-agent-setup` and `/settings/security/api#pulse-mcp-setup` as legacy compatibility hashes) share that module, and none of them is recovery state, storage focus, platform scope, or proof of backup or recovery posture. 11. Letting recovery row details on the Proxmox Backups or TrueNAS Protection tabs lead with transport-shaped payloads; operator-facing details must summarize outcome, artifact, target, and readable metadata labels first, and raw JSON or provider-specific keys must not lead the primary detail. 12. Letting storage or recovery surfaces invoke retired self-hosted trial acquisition; `POST /api/license/trial/start` and `/auth/trial-activate` must stay closed on the ordinary self-hosted router, and storage/recovery-adjacent billing or support handoffs must not treat trial activation as recovery identity, restore proof, or backup transport state. 13. Treating an `AvailabilityData` facet on a storage resource as backup, protection, or recovery evidence; an agentless probe attached to a NAS, PBS, or datastore resource is monitoring reachability, not backup success, snapshot health, or restore readiness. Storage and recovery must read protection status from the canonical backup and storage resources, not from the availability probe facet. @@ -7159,6 +7159,15 @@ nodes) now renders through the shared `PlatformHostScopedTableTitle` that the Docker and vSphere overviews also use; its Storage, Replication, Backups, Ceph and Mail Gateway tabs, and every storage and recovery rule, are unchanged. +The Pulse Intelligence external-agent setup anchor named in Forbidden Path 10 +moved to `/settings/pulse-intelligence/agents#external-agent-setup` on +2026-10-22, when the browser Assistant chat window was retired. It is still +adjacent settings route state in `frontend-modern/src/routing/resourceLinks.ts` +and carries no recovery or storage meaning. The same change adds the read-only +`settingsCapabilities.patrolRun` security-status field in `internal/api`, a +projection of the Patrol run route's admin gate that opens no storage or +recovery path. + The `internal/api` changes for typed Patrol investigation conclusions open no storage, backup or recovery path. They hand the run's typed conclusion to the private orchestrator, make investigation concurrency and timeout follow the diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 58b871ff04..73f1543d14 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -2700,9 +2700,7 @@ the route-backed Patrol control starter helpers, and the `buildStandalonePath`, Per-platform surfaces and tab specs must derive every internal link from those builders so the canonical resource URL vocabulary stays single-sourced; ad hoc string concatenation of -platform routes inside feature directories is not permitted. The canonical -Pulse Intelligence external-agent hash -`/settings/pulse-intelligence/assistant#external-agent-setup` and legacy +platform routes inside feature directories is not permitted. The canonical Pulse Intelligence external-agent hash `/settings/pulse-intelligence/agents#external-agent-setup` and legacy `/settings/security/api#external-agent-setup` / `/settings/security/api#pulse-mcp-setup` compatibility hashes may live in the shared route helper, but they are adjacent settings route state, not @@ -8695,3 +8693,12 @@ ID within its vCenter connection so two vCenters' `host-12` stay apart; a host with no VMs gets a scope that names it and matches none. A host table with no workloads under it (Docker Swarm hosts) keeps opening the drawer on a row click. No resource identity, status or alert rule changes. + +Resource surfaces stopped handing context to the browser Assistant on +2026-10-22, when the chat window was retired: the resource and guest drawer +Assistant context models are gone, the operator lock copy in +`ResourceOperatorStateSection.tsx` no longer names the Assistant, and +`frontend-modern/src/pages/Actions.tsx` reads AI availability from +`frontend-modern/src/stores/aiAvailability.ts`. The external-agent setup hash +in `frontend-modern/src/routing/resourceLinks.ts` moved to the Agents route. +Resource identity, policy, and drawer focus state are unchanged. diff --git a/frontend-modern/.bundlesize.json b/frontend-modern/.bundlesize.json index e83a9e4697..050455c8e1 100644 --- a/frontend-modern/.bundlesize.json +++ b/frontend-modern/.bundlesize.json @@ -22,9 +22,6 @@ "Proxmox": { "baselineGzip": 80339 }, - "Chat": { - "baselineGzip": 67677 - }, "AIIntelligence": { "baselineGzip": 73662 }, @@ -49,9 +46,6 @@ "TrueNAS": { "baselineGzip": 17053 }, - "vendor-highlight": { - "baselineGzip": 13743 - }, "vendor-icons": { "baselineGzip": 11486 }, @@ -79,14 +73,11 @@ "UpdatesSettingsPanel": { "baselineGzip": 9687 }, - "aiControlLevelPresentation": { - "baselineGzip": 9630 - }, "AuditLogPanel": { "baselineGzip": 8806 }, "useUnifiedResources": { - "baselineGzip": 7595 + "baselineGzip": 10267 }, "ActionReviewDialog": { "baselineGzip": 10414 diff --git a/frontend-modern/browser-tests/assistant-control-compatibility.cjs b/frontend-modern/browser-tests/assistant-control-compatibility.cjs deleted file mode 100644 index c6ee89babc..0000000000 --- a/frontend-modern/browser-tests/assistant-control-compatibility.cjs +++ /dev/null @@ -1,448 +0,0 @@ -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const path = require('node:path'); -const crypto = require('node:crypto'); -const { chromium, webkit } = require('playwright'); -const root = '/workspace/frontend-modern'; -const output = '/workspace/tmp/assistant-controls-proof/browser'; -const hash = (f) => crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex'); -(async () => { - fs.mkdirSync(output, { recursive: true }); - const binding = JSON.parse( - fs.readFileSync('/workspace/tmp/assistant-controls-proof/browser-binding.json'), - ); - for (const [file, digest] of Object.entries(binding.content_sha256)) - assert.equal(hash(path.join('/workspace', file)), digest); - const version = require('playwright/package.json').version; - assert.equal( - version, - JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[ - 'node_modules/@playwright/test' - ].version, - ); - process.chdir(root); - const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js')); - const server = await createServer({ - root, - configFile: path.join(root, 'vite.config.ts'), - cacheDir: path.join(output, 'vite-cache'), - server: { host: '127.0.0.1', port: 5308, strictPort: true, watch: null }, - }); - const origin = 'http://127.0.0.1:5308'; - const results = [], - screenshots = [], - browserVersions = []; - let browser, - phase = 'server', - browserClosed = false, - serverClosed = false; - const result = () => ({ - result: 'passed', - base_sha: binding.base_sha, - content_sha256: binding.content_sha256, - playwrightVersion: version, - browserVersions, - results, - screenshots, - browserClosed, - serverClosed, - scope: - 'Actual Settings state/save, Chat menus/scoped context and Docs with synthetic local API transport. No model/action/native persistence or deployed/released acceptance.', - }); - try { - await server.listen(); - for (const [name, engine, width, height, dark] of [ - ['desktop-light', chromium, 1365, 900, false], - ['phone-dark', webkit, 390, 844, true], - ['narrow-light', webkit, 320, 740, false], - ]) { - browserClosed = false; - browser = await engine.launch( - engine === chromium - ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] } - : { headless: true }, - ); - browserVersions.push({ - name, - engine: engine === chromium ? 'chromium' : 'webkit', - version: browser.version(), - }); - const page = await browser.newPage({ - viewport: { width, height }, - isMobile: width < 500, - hasTouch: width < 500, - locale: 'en-GB', - }); - page.setDefaultTimeout(8000); - page.setDefaultNavigationTimeout(60000); - const errors = [], - offOrigin = [], - writes = [], - checks = [], - reads = []; - let saved = 'autonomous', - failSave = false; - await page.addInitScript((dark) => { - localStorage.clear(); - const apply = () => document.documentElement.classList.toggle('dark', dark); - if (document.documentElement) apply(); - else document.addEventListener('DOMContentLoaded', apply, { once: true }); - }, dark); - page.on('pageerror', (e) => errors.push(e.message)); - await page.routeWebSocket(/.*/, (s) => s.close()); - const settings = () => ({ - enabled: true, - configured: false, - model: '', - custom_context: '', - control_level: saved, - protected_guests: ['vm-101'], - patrol_autonomy_level: 'full', - patrol_action_emergency_stop: true, - configured_providers: [], - providers: [], - request_timeout_seconds: 300, - }); - await page.route('**/*', async (route) => { - const req = route.request(), - url = new URL(req.url()); - if (url.origin !== origin) { - offOrigin.push(url.origin); - return route.abort(); - } - if (!url.pathname.startsWith('/api/')) return route.continue(); - if (req.method() !== 'GET') { - const entry = { method: req.method(), path: url.pathname, payload: req.postDataJSON() }; - writes.push(entry); - if (url.pathname !== '/api/settings/ai/update' || req.method() !== 'PUT') - return route.fulfill({ - status: 403, - json: { error: 'Fixture refuses inference, actions and unrelated writes' }, - }); - if (failSave) - return route.fulfill({ status: 503, json: { error: 'Synthetic save unavailable' } }); - if (Object.hasOwn(entry.payload, 'control_level')) saved = entry.payload.control_level; - return route.fulfill({ - json: { - ...settings(), - control_level: ['autonomous', 'suggest'].includes(saved) ? 'controlled' : saved, - }, - }); - } - reads.push(url.pathname); - if (url.pathname === '/api/settings/ai') return route.fulfill({ json: settings() }); - if (url.pathname === '/api/security/status') - return route.fulfill({ - json: { - hasAuthentication: true, - requiresAuth: false, - sessionCapabilities: { assistantEnabled: true }, - }, - }); - if (url.pathname.includes('license') || url.pathname.includes('runtime-capabilities')) - return route.fulfill({ json: { capabilities: [], entitlements: [], limits: [] } }); - if (url.pathname === '/api/ai/models') return route.fulfill({ json: { models: [] } }); - if (url.pathname === '/api/ai/status') - return route.fulfill({ json: { enabled: true, configured: false, running: true } }); - if (url.pathname.endsWith('/sessions')) return route.fulfill({ json: { sessions: [] } }); - if (url.pathname === '/api/agent/capabilities') - return route.fulfill({ - json: { - version: 1, - capabilities: [], - surfaces: [], - workflowPrompts: [], - mcpAdapter: {}, - operatorSurfaces: [], - }, - }); - return route.fulfill({ json: [] }); - }); - const check = async (label, action) => { - phase = `${name}: ${label}`; - await action(); - checks.push({ label, passed: true }); - }; - const capture = async (state) => { - const file = `${name}-${state}.png`; - await page.screenshot({ path: path.join(output, file) }); - screenshots.push({ - file, - sha256: hash(path.join(output, file)), - state, - viewport: { width, height }, - }); - }; - const load = async (surface) => - page.goto( - `${origin}/browser-tests/assistant-control-compatibility.html?surface=${surface}`, - ); - try { - for (const level of ['autonomous', 'suggest', 'controlled', 'read_only', 'unknown']) { - saved = level; - await load('settings'); - const select = page.getByLabel('Chat action mode', { exact: true }); - const expected = ['autonomous', 'suggest', 'controlled'].includes(level) - ? 'controlled' - : 'read_only'; - await check(`Settings ${level} projects without granting execution`, async () => { - await select.waitFor(); - assert.equal(await select.inputValue(), expected); - assert.equal(await select.locator('option').count(), 2); - const text = await page.locator('main').innerText(); - assert.ok( - !text.includes('chat-only') && - !text.includes('Infrastructure changes stay with Patrol'), - ); - if (expected === 'controlled') { - assert.ok(text.includes('Chat does not execute the plan')); - assert.equal(await page.getByLabel(/Protected guests/i).count(), 0); - assert.ok(!text.includes('Protected guests')); - const hint = page.getByTestId('ai-never-auto-remediate-hint'); - assert.ok((await hint.innerText()).includes('Never auto-remediate')); - assert.ok( - !(await page - .locator('input') - .evaluateAll((els) => els.some((el) => el.value === 'vm-101'))), - ); - } else - assert.ok( - text.includes( - 'Assistant can query and explain only. It cannot plan infrastructure actions.', - ), - ); - assert.ok( - await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), - ); - }); - if (level === 'autonomous' || level === 'read_only') { - await select.scrollIntoViewIfNeeded(); - await capture(level === 'read_only' ? 'read-only-settings' : 'legacy-settings'); - } - const before = writes.length; - await page.getByRole('button', { name: 'Save Assistant settings', exact: true }).click(); - await check(`Unrelated save preserves ${level} and scoped policy`, async () => { - await page.waitForFunction( - () => !document.querySelector('button[disabled]')?.textContent?.includes('Saving'), - ); - for (let i = 0; writes.length === before && i < 20; i++) await page.waitForTimeout(50); - assert.equal(writes.length, before + 1); - for (const key of [ - 'control_level', - 'protected_guests', - 'patrol_autonomy_level', - 'patrol_action_emergency_stop', - ]) - assert.ok(!Object.hasOwn(writes.at(-1).payload, key)); - assert.equal(saved, level); - }); - } - saved = 'autonomous'; - await load('chat'); - const trigger = page.getByRole('button', { - name: 'Assistant chat action mode: Ask first', - exact: true, - }); - await check('Legacy Chat has two modes and keyboard-safe review-only copy', async () => { - await trigger.waitFor(); - assert.equal( - await page.getByRole('status', { name: 'Assistant chat actions warning' }).count(), - 0, - ); - await trigger.focus(); - await page.keyboard.press('ArrowDown'); - const modes = page.getByRole('menuitemradio'); - assert.equal(await modes.count(), 2); - const ask = page.getByRole('menuitemradio', { name: /Plans actions for your review/ }); - assert.equal(await ask.getAttribute('aria-checked'), 'true'); - await page.keyboard.press('Home'); - assert.ok(await modes.first().evaluate((el) => el === document.activeElement)); - await page.keyboard.press('ArrowUp'); - assert.ok(await ask.evaluate((el) => el === document.activeElement)); - const bounds = await page.getByRole('menu').boundingBox(); - const composer = await page.locator('[data-assistant-control-toolbar]').boundingBox(); - assert.ok( - bounds.x >= composer.x - 1 && - bounds.x + bounds.width <= composer.x + composer.width + 1, - ); - assert.ok(bounds.x >= 0 && bounds.x + bounds.width <= width + 1); - await capture('chat-menu'); - await page.keyboard.press('Escape'); - await page.waitForFunction( - () => - document.activeElement?.getAttribute('aria-label') === - 'Assistant chat action mode: Ask first', - ); - assert.equal(await page.getByRole('menu').count(), 0); - }); - await check('Viewport resize closes the menu and reopened bounds fit', async () => { - await trigger.click(); - await page.setViewportSize({ width: width + 1, height }); - await page.getByRole('menu').waitFor({ state: 'hidden' }); - await page.setViewportSize({ width, height }); - await trigger.click(); - const bounds = await page.getByRole('menu').boundingBox(); - assert.ok(bounds.x >= 0 && bounds.x + bounds.width <= width + 1); - await page.keyboard.press('Escape'); - }); - await check('Scoped alert attachment keeps identity and approval boundary', async () => { - await page.evaluate(() => window.__assistantProof.scoped()); - await page.getByText('Approval required before any action.', { exact: true }).waitFor(); - assert.equal( - await page.evaluate(() => window.__assistantProof.context().autonomousMode), - false, - ); - assert.equal( - await page.evaluate(() => window.__assistantProof.context().handoffResources[0].id), - 'vm:lab:101', - ); - assert.ok( - await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), - ); - await capture('scoped-chat'); - }); - await check('Chat mode save failure retains current mode', async () => { - failSave = true; - await trigger.click(); - await page.getByRole('menuitemradio', { name: /Read-only/ }).click(); - await trigger.waitFor(); - assert.equal(saved, 'autonomous'); - failSave = false; - }); - await check('Explicit Chat opt-out changes only the mode', async () => { - await trigger.click(); - await page.getByRole('menuitemradio', { name: /Read-only/ }).click(); - await page - .getByRole('button', { name: 'Assistant chat action mode: Read-only', exact: true }) - .waitFor(); - assert.deepEqual(writes.at(-1).payload, { control_level: 'read_only' }); - assert.equal(saved, 'read_only'); - }); - saved = 'controlled'; - await load('settings'); - await page.getByLabel('Chat action mode', { exact: true }).waitFor(); - for (const [link, heading, state] of [ - ['Read Assistant modes', 'Control Levels', 'help-modes'], - ['Read control guide', 'Assistant Control Levels', 'help-guide'], - ]) { - if (state === 'help-guide') { - await load('settings'); - await page.getByLabel('Chat action mode', { exact: true }).waitFor(); - } - await check( - `Actual Docs ${heading} describes planning and independent verification`, - async () => { - await page.getByRole('link', { name: link, exact: true }).click(); - const h = page.getByRole('heading', { name: heading, exact: true }); - await h.waitFor(); - await h.scrollIntoViewIfNeeded(); - const text = await page.locator('article').innerText(); - assert.ok(text.includes('Assistant chat does not execute')); - assert.ok( - text.includes('stored') && - text.includes('entitlement') && - text.includes('independent verification') && - text.includes('Never auto-remediate') && - text.includes('has been removed'), - ); - assert.ok( - !text.includes('executes actions without prompting') && - !text.includes('executes commands without prompting'), - ); - assert.ok( - await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), - ); - await capture(state); - const tableScroll = await page.locator('article').evaluate((article) => { - const table = [...article.querySelectorAll('table')].find( - (t) => t.innerText.includes('Availability') && t.innerText.includes('Ask first'), - ); - if (!table) return { found: false }; - const wrapper = table.parentElement; - const overflows = wrapper.scrollWidth > wrapper.clientWidth + 1; - const overflowX = getComputedStyle(wrapper).overflowX; - if (overflows) wrapper.scrollLeft = wrapper.scrollWidth; - const lastCell = table.querySelector('thead tr').lastElementChild; - return { - found: true, - overflows, - overflowX, - scrollLeft: wrapper.scrollLeft, - lastCellRight: lastCell.getBoundingClientRect().right, - wrapperRight: wrapper.getBoundingClientRect().right, - }; - }); - assert.equal(tableScroll.found, true); - if (tableScroll.overflows) { - assert.equal(tableScroll.overflowX, 'auto'); - assert.ok(tableScroll.scrollLeft > 0); - assert.ok(tableScroll.lastCellRight <= tableScroll.wrapperRight + 1); - await capture(`${state}-table-end`); - } - }, - ); - } - assert.deepEqual(errors, []); - assert.deepEqual(offOrigin, []); - assert.ok(writes.every((r) => r.method === 'PUT' && r.path === '/api/settings/ai/update')); - results.push({ - name, - viewport: { width, height }, - dark, - checks, - writes, - errors, - offOrigin, - }); - } catch (error) { - fs.writeFileSync( - path.join(output, 'debug.json'), - JSON.stringify( - { - phase, - errors, - reads, - body: await page.locator('body').innerText(), - buttons: await page.getByRole('button').allTextContents(), - }, - null, - 2, - ), - ); - await page.screenshot({ path: path.join(output, 'failed-view.png') }); - throw error; - } finally { - await page.close(); - await browser.close(); - browser = null; - browserClosed = true; - } - } - } catch (error) { - fs.writeFileSync( - path.join(output, 'failure.json'), - JSON.stringify({ phase, error: error.message, results, screenshots }, null, 2), - ); - throw error; - } finally { - if (browser) { - await browser.close(); - browserClosed = true; - } - await server.close(); - serverClosed = true; - } - fs.writeFileSync(path.join(output, 'result.json'), JSON.stringify(result(), null, 2) + '\n'); - console.log( - JSON.stringify({ - result: 'passed', - groups: results.reduce((n, r) => n + r.checks.length, 0), - screenshots: screenshots.length, - browserClosed, - serverClosed, - }), - ); -})().catch((e) => { - console.error(e); - process.exitCode = 1; -}); diff --git a/frontend-modern/browser-tests/assistant-control-compatibility.html b/frontend-modern/browser-tests/assistant-control-compatibility.html deleted file mode 100644 index d4b85c5f7f..0000000000 --- a/frontend-modern/browser-tests/assistant-control-compatibility.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Existing Assistant controls verification - - -
    - - - diff --git a/frontend-modern/browser-tests/assistant-control-compatibility.tsx b/frontend-modern/browser-tests/assistant-control-compatibility.tsx deleted file mode 100644 index d975c298e0..0000000000 --- a/frontend-modern/browser-tests/assistant-control-compatibility.tsx +++ /dev/null @@ -1,59 +0,0 @@ -// Actual existing Settings/Chat/Docs surfaces, not a shipped route. Synthetic -// API responses and local settings saves only; no model/action/backend calls. -import { render } from 'solid-js/web'; -import { A, Route, Router } from '@solidjs/router'; -import { AIAssistantSettings } from '../src/components/Settings/AISettings'; -import { AIChat } from '../src/components/AI/Chat'; -import Docs from '../src/pages/Docs'; -import { aiChatStore } from '../src/stores/aiChat'; -import '../src/index.css'; -const params = new URLSearchParams(location.search); -window.history.replaceState( - {}, - '', - params.get('surface') === 'chat' ? '/proof-chat' : '/settings/ai/assistant', -); -aiChatStore.setEnabled(true); -(window as any).__assistantProof = { - scoped: () => - aiChatStore.setContext({ - autonomousMode: false, - briefing: { - sourceLabel: 'Pulse Alerts', - title: 'Synthetic alert investigation attached', - subject: 'lab-vm', - }, - handoffResources: [{ id: 'vm:lab:101', name: 'lab-vm', type: 'vm', node: 'lab' }], - context: { alertIdentifier: 'synthetic-alert-1' }, - }), - context: () => aiChatStore.context, -}; -function SettingsFixture() { - return ( -
    - - -
    - ); -} -function ChatFixture() { - aiChatStore.open(); - return ( -
    - aiChatStore.close()} /> -
    - ); -} -render( - () => ( - - - - - - ), - document.getElementById('root')!, -); diff --git a/frontend-modern/browser-tests/held-breach-time.cjs b/frontend-modern/browser-tests/held-breach-time.cjs deleted file mode 100644 index a4e6178f87..0000000000 --- a/frontend-modern/browser-tests/held-breach-time.cjs +++ /dev/null @@ -1,339 +0,0 @@ -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const path = require('node:path'); -const crypto = require('node:crypto'); -const { chromium, webkit } = require('playwright'); -const root = '/workspace/frontend-modern'; -const output = '/workspace/tmp/held-breach-proof/browser'; -const runtime = [ - 'frontend-modern/src/types/api.ts', - 'frontend-modern/src/features/alerts/metricAlertPresentation.ts', - 'frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts', -]; -const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); -(async () => { - fs.mkdirSync(output, { recursive: true }); - const binding = JSON.parse(fs.readFileSync('/workspace/tmp/held-breach-proof/binding.json')); - const hashes = Object.fromEntries(runtime.map((f) => [f, hash(path.join('/workspace', f))])); - assert.deepEqual(hashes, binding.content_sha256); - const version = require('playwright/package.json').version; - assert.equal( - version, - JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[ - 'node_modules/@playwright/test' - ].version, - ); - process.chdir(root); - const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js')); - const server = await createServer({ - root, - configFile: path.join(root, 'vite.config.ts'), - cacheDir: path.join(output, 'vite-cache'), - server: { host: '127.0.0.1', port: 5298, strictPort: true, watch: null }, - }); - const origin = 'http://127.0.0.1:5298'; - const results = [], - screenshots = []; - let browser, - phase = 'server'; - try { - await server.listen(); - for (const [name, engine, width, height, dark] of [ - ['chromium-desktop-light', chromium, 1365, 900, false], - ['chromium-desktop-dark', chromium, 1365, 900, true], - ['webkit-phone-dark', webkit, 390, 844, true], - ['webkit-narrow-light', webkit, 320, 740, false], - ]) { - phase = name; - browser = await engine.launch( - engine === chromium - ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] } - : { headless: true }, - ); - const page = await browser.newPage({ - viewport: { width, height }, - isMobile: width < 500, - hasTouch: width < 500, - locale: 'en-GB', - timezoneId: 'UTC', - }); - page.setDefaultTimeout(8000); - page.setDefaultNavigationTimeout(60000); - await page.clock.setFixedTime(new Date('2026-10-06T10:52:00Z')); - const errors = [], - requests = [], - offOrigin = [], - writes = [], - checks = []; - page.on('pageerror', (error) => errors.push(error.message)); - await page.addInitScript((dark) => { - localStorage.clear(); - const apply = () => document.documentElement.classList.toggle('dark', dark); - if (document.documentElement) apply(); - else document.addEventListener('DOMContentLoaded', apply, { once: true }); - }, dark); - await page.routeWebSocket(/.*/, (socket) => socket.close()); - await page.route('**/*', async (route) => { - const req = route.request(), - url = new URL(req.url()); - if (url.origin !== origin) { - offOrigin.push(url.origin); - return route.abort(); - } - if (!url.pathname.startsWith('/api/')) return route.continue(); - requests.push({ path: url.pathname, method: req.method() }); - if (req.method() !== 'GET') { - writes.push({ path: url.pathname, method: req.method() }); - return route.fulfill({ - status: 403, - json: { error: 'Fixture refuses every write/inference/action' }, - }); - } - if (url.pathname === '/api/alerts/history') return route.fulfill({ json: [] }); - if (url.pathname === '/api/alerts/incidents') return route.fulfill({ json: null }); - if (url.pathname === '/api/security/status') - return route.fulfill({ - json: { - hasAuthentication: true, - requiresAuth: false, - sessionCapabilities: { assistantEnabled: true }, - }, - }); - if (url.pathname.includes('license')) - return route.fulfill({ - json: { capabilities: [], entitlements: [], limits: [], max_history_days: 90 }, - }); - if (url.pathname === '/api/settings/ai') - return route.fulfill({ - json: { enabled: true, control_level: 'controlled', providers: [], model: '' }, - }); - if (url.pathname === '/api/ai/models') return route.fulfill({ json: { models: [] } }); - if (url.pathname === '/api/ai/status') - return route.fulfill({ json: { enabled: true, configured: false } }); - if (url.pathname.endsWith('/sessions')) return route.fulfill({ json: { sessions: [] } }); - return route.fulfill({ json: [] }); - }); - const check = async (label, action) => { - phase = `${name}: ${label}`; - await action(); - checks.push({ label, passed: true }); - }; - const capture = async (state, locator = page) => { - const file = `${name}-${state}.png`, - target = path.join(output, file); - await locator.screenshot({ path: target, ...(locator === page ? { fullPage: true } : {}) }); - screenshots.push({ file, state, viewport: { width, height }, sha256: hash(target) }); - }; - try { - await page.goto(`${origin}/browser-tests/held-breach-time.html`); - const attention = page.getByRole('region', { name: 'Existing node attention' }); - const overview = page.getByRole('region', { name: 'Existing alert overview' }); - const history = page.getByRole('region', { name: 'Existing alert history' }); - const breachPrefix = 'Last reading at or above 80°C: 80°C'; - const assertBreach = async (expected) => { - const p = attention - .locator('p') - .filter({ hasText: /^Temperature|^Last reading: Temperature/ }) - .first(); - assert.equal((await p.getAttribute('title')).split('\n').at(-1), expected); - assert.equal( - await overview - .locator('p[title]') - .filter({ hasText: /^Temperature|^Last reading: Temperature/ }) - .first() - .getAttribute('title'), - expected, - ); - if (width < 500) - await history.getByText(expected, { exact: true }).waitFor({ state: 'visible' }); - else assert.ok((await history.locator('[title]').all()).length > 0); - }; - await check( - 'status date beats newer legacy poll in attention, overview and History', - async () => { - await attention - .getByText('Temperature 76°C now, back under the 80°C alert level', { exact: true }) - .waitFor(); - await assertBreach(`${breachPrefix}, 20 mins ago`); - if (width >= 1024) - assert.ok( - (await history.locator('[title]').evaluateAll((es) => es.map((e) => e.title))).some( - (t) => t.includes('20 mins ago'), - ), - ); - }, - ); - await capture('latched'); - await check( - 'real Assistant handoff carries retained ISO evidence, live reading and approval boundary', - async () => { - await page - .getByRole('region', { name: 'Existing Assistant handoff' }) - .getByRole('button') - .click(); - const context = await page.evaluate(() => window.__heldBreach.context()); - assert.match(context.handoffContext, /Last Breach At: 2026-10-06T10:31:59\.123Z/); - assert.match(context.handoffContext, /Current Value: 76\.0°C/); - assert.match(context.handoffContext, /Last Reading At Or Above Threshold: 80\.0°C/); - assert.match(context.handoffContext, /Operator Boundary:/); - assert.equal(context.targetId, 'lab-minipc'); - assert.equal(context.autonomousMode, false); - // Model-only evidence is checked above; production Chat deliberately - // retains its compact attachment rather than exposing detail lines. - await page - .getByRole('button', { name: 'Render attached briefing without inference' }) - .click(); - const briefing = page.getByRole('region', { name: 'Assistant context' }); - await briefing.getByText('Warning temperature on minipc', { exact: true }).waitFor(); - assert.doesNotMatch(await briefing.textContent(), /Last reading at or above/); - await capture('assistant', briefing); - await page.evaluate(() => window.__heldBreach.close()); - }, - ); - await check( - 'recovery changes the current reading, not the retained breach date', - async () => { - await page.evaluate(() => window.__heldBreach.setScenario('recovering')); - await attention - .getByText('Temperature 72°C now, recovering', { exact: true }) - .waitFor(); - await assertBreach(`${breachPrefix}, 20 mins ago`); - await attention - .getByText('Clears after 5 minutes at 75°C or lower, 2 minutes so far.', { - exact: true, - }) - .waitFor(); - }, - ); - await check('invalid status date falls back to valid legacy date', async () => { - await page.evaluate(() => window.__heldBreach.setScenario('legacy')); - await assertBreach(`${breachPrefix}, 20 mins ago`); - }); - await check( - 'Go-zero and unknown remain undated on every surface and in Assistant', - async () => { - await page.evaluate(() => window.__heldBreach.setScenario('unknown')); - await assertBreach(breachPrefix); - await page - .getByRole('region', { name: 'Existing Assistant handoff' }) - .getByRole('button') - .click(); - const context = await page.evaluate(() => window.__heldBreach.context()); - assert.doesNotMatch(context.handoffContext, /Last Breach At:/); - await page.evaluate(() => window.__heldBreach.close()); - }, - ); - await capture('unknown'); - await check( - 'stale evaluation is not now, but still retains the correct breach age', - async () => { - await page.evaluate(() => window.__heldBreach.setScenario('stale')); - await attention - .getByText('Last reading: Temperature 76°C, 12 mins ago', { exact: true }) - .waitFor(); - await assertBreach(`${breachPrefix}, 20 mins ago`); - }, - ); - await check( - 'restart-before-evaluation keeps the legacy message and fabricates no date', - async () => { - await page.evaluate(() => window.__heldBreach.setScenario('restart')); - await attention.getByText('Node temperature at 80.0°C', { exact: true }).waitFor(); - assert.equal(await overview.locator('[title^="Last reading at or above"]').count(), 0); - await page - .getByRole('region', { name: 'Existing Assistant handoff' }) - .getByRole('button') - .click(); - const context = await page.evaluate(() => window.__heldBreach.context()); - assert.doesNotMatch(context.handoffContext, /Last Breach At:/); - assert.ok(context.briefing.detailLines.includes('Message: Node temperature at 80.0°C')); - await page.evaluate(() => window.__heldBreach.close()); - }, - ); - await check('reload retains final-byte date selection', async () => { - await page.goto(`${origin}/browser-tests/held-breach-time.html?case=legacy`); - await assertBreach(`${breachPrefix}, 20 mins ago`); - }); - await check( - 'no horizontal page overflow, JavaScript errors, external contacts or writes', - async () => { - assert.equal( - await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), - false, - ); - assert.deepEqual(errors, []); - assert.deepEqual(offOrigin, []); - assert.deepEqual(writes, []); - }, - ); - results.push({ - name, - engine: engine === chromium ? 'chromium' : 'webkit', - browserVersion: browser.version(), - width, - height, - dark, - checks, - requests, - errors, - writes, - offOrigin, - }); - } finally { - await page.close(); - await browser.close(); - browser = undefined; - } - } - assert.deepEqual( - Object.fromEntries(runtime.map((f) => [f, hash(path.join('/workspace', f))])), - hashes, - ); - fs.writeFileSync( - path.join(output, 'result.json'), - JSON.stringify( - { - result: 'passed', - version, - binding, - hashes, - results, - screenshots, - verifiedAt: new Date().toISOString(), - cleanup: { browserClosed: true, serverClosed: false }, - }, - null, - 2, - ), - ); - } catch (error) { - fs.writeFileSync( - path.join(output, 'failure.json'), - JSON.stringify( - { phase, message: error.message, stack: error.stack, results, screenshots }, - null, - 2, - ), - ); - throw error; - } finally { - if (browser) await browser.close(); - await server.close(); - } - const file = path.join(output, 'result.json'), - result = JSON.parse(fs.readFileSync(file)); - result.cleanup.serverClosed = true; - fs.writeFileSync(file, JSON.stringify(result, null, 2)); - console.log( - JSON.stringify({ - result: result.result, - checkGroups: results.reduce((n, r) => n + r.checks.length, 0), - screenshots: screenshots.length, - cleanup: result.cleanup, - runtime: hashes, - }), - ); -})().catch((error) => { - console.error(error); - process.exitCode = 1; -}); diff --git a/frontend-modern/browser-tests/held-breach-time.html b/frontend-modern/browser-tests/held-breach-time.html deleted file mode 100644 index 6ee295bd36..0000000000 --- a/frontend-modern/browser-tests/held-breach-time.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Held-alert breach date verification - - -
    - - - diff --git a/frontend-modern/browser-tests/held-breach-time.tsx b/frontend-modern/browser-tests/held-breach-time.tsx deleted file mode 100644 index fcfda64142..0000000000 --- a/frontend-modern/browser-tests/held-breach-time.tsx +++ /dev/null @@ -1,163 +0,0 @@ -// Production attention, alert/history, Assistant button/store and chat briefing. -// All observations are synthetic. The fixture consumes the explanation intent -// before mounting Chat, so rendering a briefing never calls a model or action. -import { createSignal, Show } from 'solid-js'; -import { render } from 'solid-js/web'; -import { Route, Router } from '@solidjs/router'; -import { NodeDrawerOverview } from '../src/components/Workloads/NodeDrawerOverview'; -import { OverviewTab } from '../src/features/alerts/OverviewTab'; -import { AlertHistoryTableSection } from '../src/features/alerts/AlertHistoryTableSection'; -import { useAlertHistoryState } from '../src/features/alerts/useAlertHistoryState'; -import { InvestigateAlertButton } from '../src/components/Alerts/InvestigateAlertButton'; -import { AIChat } from '../src/components/AI/Chat'; -import { aiChatStore } from '../src/stores/aiChat'; -import type { Alert, Node } from '../src/types/api'; -import type { Resource } from '../src/types/resource'; -import '../src/index.css'; - -const node = { - id: 'lab-minipc', - name: 'minipc', - instance: 'lab', - status: 'online', - type: 'node', - cpu: 0, - memory: { total: 1024, used: 256, free: 768, usage: 25 }, - disk: { total: 1024, used: 256, free: 768, usage: 25 }, - uptime: 3600, - loadAverage: [], - cpuInfo: { model: 'Fixture CPU', cores: 4, sockets: 1, mhz: '2400' }, - lastSeen: '2026-10-06T10:51:50Z', - connectionHealth: 'healthy', -} as Node; -const resource = { ...node, type: 'node', platform: 'proxmox' } as unknown as Resource; -function makeAlert(scenario: string): Alert { - const alert: Alert = { - id: 'lab-minipc::metric-threshold:temperature', - type: 'temperature', - level: 'warning', - resourceId: node.id, - resourceName: node.name, - node: node.name, - instance: 'lab', - message: 'Node temperature at 80.0°C', - value: 80, - threshold: 80, - startTime: '2026-10-06T07:57:44Z', - acknowledged: false, - // A newer legacy poll must not replace the evaluated breach date. - lastSeen: '2026-10-06T10:51:59Z', - metricStatus: { - phase: 'latched', - value: 76, - unit: '°C', - observedAt: '2026-10-06T10:51:50Z', - lastBreachAt: '2026-10-06T11:31:59.123456789+01:00', - trigger: 80, - recovery: 75, - recoveryDelaySeconds: 300, - }, - }; - if (scenario === 'recovering') - alert.metricStatus = { - ...alert.metricStatus!, - phase: 'recovering', - value: 72, - recoveryElapsedSeconds: 120, - }; - if (scenario === 'legacy') { - alert.metricStatus!.lastBreachAt = 'not-a-date'; - alert.lastSeen = '2026-10-06T10:32:00Z'; - } - if (scenario === 'unknown') { - alert.metricStatus!.lastBreachAt = '0001-01-01T00:00:00Z'; - alert.lastSeen = 'unknown'; - } - if (scenario === 'stale') alert.metricStatus!.observedAt = '2026-10-06T10:40:00Z'; - if (scenario === 'restart') { - alert.metricStatus = undefined; - alert.lastSeen = undefined; - } - return alert; -} -function Fixture() { - const initial = makeAlert(new URLSearchParams(location.search).get('case') || 'latched'); - const [alerts, setAlerts] = createSignal>({ [initial.id]: initial }); - const [showAcknowledged, setShowAcknowledged] = createSignal(true); - const [showChat, setShowChat] = createSignal(false); - const history = useAlertHistoryState({ - activeAlerts: alerts, - getResource: () => resource, - allResources: () => [resource], - }); - aiChatStore.setEnabled(true); - const readBriefing = () => { - const request = aiChatStore.explanationRequestSignal(); - if (request) aiChatStore.ackExplanationRequest(request.id); - setShowChat(true); - }; - const closeChat = () => { - setShowChat(false); - aiChatStore.close(); - }; - (window as any).__heldBreach = { - setScenario: (scenario: string) => { - closeChat(); - const next = makeAlert(scenario); - setAlerts({ [next.id]: next }); - }, - context: () => aiChatStore.context, - close: closeChat, - readBriefing, - }; - return ( -
    -

    Held-alert breach date evidence

    -

    - Synthetic readings only. No model, diagnostic or action is executed. -

    -
    - -
    -
    - - setAlerts((old) => ({ ...old, [id]: { ...old[id], ...updates } })) - } - showAcknowledged={showAcknowledged} - setShowAcknowledged={setShowAcknowledged} - showQuickTip={() => false} - dismissQuickTip={() => {}} - alertsDisabled={() => false} - /> -
    -
    -

    History

    - -
    -
    - -
    - - - - -
    - ); -} -render( - () => ( - - - - ), - document.getElementById('root')!, -); diff --git a/frontend-modern/browser-tests/system-alert-scope.cjs b/frontend-modern/browser-tests/system-alert-scope.cjs deleted file mode 100644 index a91ae3d078..0000000000 --- a/frontend-modern/browser-tests/system-alert-scope.cjs +++ /dev/null @@ -1,378 +0,0 @@ -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const path = require('node:path'); -const crypto = require('node:crypto'); -const { chromium, webkit } = require('playwright'); -const root = '/workspace/frontend-modern'; -const output = '/workspace/tmp/system-alert-scope-proof'; -const runtime = [ - 'frontend-modern/src/utils/alertScope.ts', - 'frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts', - 'frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx', - 'frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx', - 'frontend-modern/src/features/alerts/alertHistoryModel.ts', - 'frontend-modern/src/features/alerts/AlertHistoryItemActions.tsx', -]; -const systemTypes = [ - 'backup-evaluation', - 'notification-delivery', - 'deadman-delivery', - 'deadman-monitoring-stalled', - 'deadman-interruption', - 'deadman-state', -]; -const legacy = { - id: 'retained-legacy-system-id', - type: 'notification-delivery', - level: 'warning', - resourceId: 'vm-wrong', - resourceName: 'Pulse', - node: 'wrong-node', - metadata: { systemAlert: true, resourceType: 'vm' }, - message: 'Synthetic retained notification delivery outage.', - startTime: '2026-10-04T14:00:00Z', - lastSeen: '2026-10-04T14:30:00Z', - acknowledged: true, - value: 0, - threshold: 0, -}; -const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); -(async () => { - fs.mkdirSync(output, { recursive: true }); - const version = require('playwright/package.json').version; - assert.equal( - version, - JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[ - 'node_modules/@playwright/test' - ].version, - ); - const hashes = Object.fromEntries( - runtime.map((file) => [file, hash(path.join('/workspace', file))]), - ); - process.chdir(root); - const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js')); - const server = await createServer({ - root, - configFile: path.join(root, 'vite.config.ts'), - cacheDir: path.join(output, 'vite-cache'), - server: { host: '127.0.0.1', port: 5297, strictPort: true, watch: null }, - }); - const origin = 'http://127.0.0.1:5297'; - const results = [], - screenshots = []; - let browser, - phase = 'server'; - try { - await server.listen(); - for (const [name, engine, width, dark] of [ - ['chromium-desktop-light', chromium, 1365, false], - ['chromium-desktop-dark', chromium, 1365, true], - ['webkit-phone-dark', webkit, 390, true], - ['webkit-narrow-light', webkit, 320, false], - ]) { - phase = name; - browser = await engine.launch( - engine === chromium - ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] } - : { headless: true }, - ); - const page = await browser.newPage({ - viewport: { width, height: width < 500 ? 844 : 900 }, - isMobile: width < 500, - hasTouch: width < 500, - locale: 'en-GB', - timezoneId: 'UTC', - }); - page.setDefaultTimeout(10000); - page.setDefaultNavigationTimeout(60000); - const errors = [], - requests = [], - offOrigin = [], - writes = [], - checks = []; - page.on('pageerror', (error) => errors.push(error.message)); - await page.addInitScript((dark) => { - localStorage.clear(); - const apply = () => document.documentElement.classList.toggle('dark', dark); - if (document.documentElement) apply(); - else document.addEventListener('DOMContentLoaded', apply, { once: true }); - }, dark); - await page.routeWebSocket(/.*/, (socket) => socket.close()); - await page.route('**/*', async (route) => { - const req = route.request(), - url = new URL(req.url()); - if (url.origin !== origin) { - offOrigin.push(url.origin); - return route.abort(); - } - if (!url.pathname.startsWith('/api/')) return route.continue(); - requests.push({ path: url.pathname, query: url.search, method: req.method() }); - if (req.method() !== 'GET') { - writes.push({ path: url.pathname, method: req.method(), body: req.postDataJSON() }); - assert.ok( - ['/api/alerts/acknowledge', '/api/alerts/unacknowledge'].includes(url.pathname), - 'unexpected write', - ); - assert.equal(req.method(), 'POST'); - assert.deepEqual(req.postDataJSON(), { - alertIdentifier: 'pulse-system-backup-evaluation', - }); - return route.fulfill({ json: { success: true } }); - } - if (url.pathname === '/api/alerts/history') return route.fulfill({ json: [legacy] }); - if (url.pathname === '/api/alerts/incidents') return route.fulfill({ json: null }); - if (url.pathname === '/api/notifications/health') - return route.fulfill({ - json: { - overall_healthy: true, - queue: { - healthy: true, - status: 'healthy', - pending: 0, - sending: 0, - sent: 0, - failed: 0, - dlq: 0, - attention_required: 0, - reason_codes: [], - completed_retention_days: 7, - dead_letter_retention_days: 30, - counts_are_retention_bounded: true, - retry_attempts_affect_health: false, - terminal_failures_affect_health: true, - failure_classes_7d: { - authentication: 0, - rate_limited: 0, - connectivity: 0, - tls: 0, - configuration: 0, - rejected: 0, - server_error: 0, - unknown: 0, - }, - failure_class_window_days: 7, - failure_classes_available: true, - }, - }, - }); - if (url.pathname.includes('license')) - return route.fulfill({ - json: { capabilities: [], entitlements: [], limits: [], max_history_days: 90 }, - }); - if (url.pathname === '/api/security/status') - return route.fulfill({ - json: { - hasAuthentication: true, - requiresAuth: false, - sessionCapabilities: { assistantEnabled: true }, - }, - }); - return route.fulfill({ json: [] }); - }); - const check = async (label, action) => { - phase = `${name}: ${label}`; - await action(); - checks.push({ label, passed: true }); - }; - const capture = async (locator, state) => { - const file = `${name}-${state}.png`; - await locator.screenshot({ path: path.join(output, file) }); - screenshots.push({ file, state, width, dark, sha256: hash(path.join(output, file)) }); - }; - const assertSystemContext = async (id) => { - const context = await page.evaluate(() => window.__systemAlertScope.context()); - assert.equal(context.targetId, undefined); - assert.equal(context.targetType, undefined); - assert.deepEqual(context.handoffResources, []); - assert.equal(context.autonomousMode, false); - assert.equal(context.context.guestName, undefined); - assert.equal(context.context.node, undefined); - assert.match(context.handoffContext, /Scope: Pulse itself \(not a monitored resource\)/); - assert.ok(context.handoffContext.includes(`Alert Identifier: ${id}`)); - assert.ok(context.handoffContext.includes('Operator Boundary:')); - assert.doesNotMatch( - context.handoffContext, - /Current Value:|Threshold:|wrong-node|vm-wrong/, - ); - assert.doesNotMatch(context.briefing.detailLines.join('\n'), /Current value|threshold/); - await page.evaluate(() => window.__systemAlertScope.closeExplanation()); - }; - try { - await page.goto(`${origin}/browser-tests/system-alert-scope.html`); - const overview = page.getByRole('region', { name: 'Existing alert overview' }); - const history = page.getByRole('region', { name: 'Existing alert history' }); - const backupCard = page.locator('#alert-pulse-system-backup-evaluation'); - await check( - 'canonical service warning has no false resource link or policy action', - async () => { - await backupCard.waitFor({ state: 'visible' }); - assert.equal( - await overview - .getByText('Notification delivery status is unavailable', { exact: true }) - .count(), - 0, - ); - assert.equal(await backupCard.getByRole('link').count(), 0); - assert.equal( - await backupCard.getByRole('button', { name: 'Have Patrol investigate' }).count(), - 0, - ); - assert.match(await backupCard.textContent(), /Backup-age alerts were not evaluated/); - await page.waitForFunction(() => window.__systemAlertScope.historyRows().length === 3); - const rows = await page.evaluate(() => window.__systemAlertScope.historyRows()); - for (const row of rows.filter((r) => r.systemAlert)) { - assert.equal(row.resourceType, 'Pulse'); - assert.equal(row.resourceId, ''); - } - assert.equal(rows.find((r) => r.id === 'resource-cpu').resourceType, 'vm'); - }, - ); - await capture(overview, 'backup-warning'); - await check('acknowledgement remains active, not recovery', async () => { - await backupCard.getByRole('button', { name: 'Acknowledge', exact: true }).click(); - await backupCard.getByText('Acknowledged', { exact: true }).waitFor(); - assert.match(await backupCard.textContent(), /Backup-age alerts were not evaluated/); - await backupCard.getByRole('button', { name: 'Unacknowledge', exact: true }).click(); - await backupCard.getByRole('button', { name: 'Acknowledge', exact: true }).waitFor(); - }); - await check('occurrence timeline keeps exact service identity', async () => { - await backupCard.getByRole('button', { name: 'Timeline', exact: true }).click(); - await page.waitForFunction(() => - document - .querySelector('#alert-pulse-system-backup-evaluation') - ?.textContent.includes('No incident'), - ); - const incident = requests.find((r) => r.path === '/api/alerts/incidents'); - assert.ok(incident); - const query = new URLSearchParams(incident.query); - assert.equal(query.get('alertIdentifier'), 'pulse-system-backup-evaluation'); - assert.equal(query.get('started_at'), '2026-10-04T15:00:00Z'); - await backupCard.getByRole('button', { name: 'Hide Timeline', exact: true }).click(); - }); - for (const type of systemTypes) { - await check(`${type} explanation is service-scoped and model-only`, async () => { - await page.evaluate((type) => window.__systemAlertScope.updateSystem(type), type); - const card = page.locator(`#alert-pulse-system-${type}`); - await card.waitFor({ state: 'visible' }); - assert.equal(await card.getByRole('link').count(), 0); - const button = card.getByRole('button', { name: 'Ask Pulse Assistant', exact: true }); - if (width > 500) { - await button.focus(); - await page.keyboard.press('Enter'); - } else await button.tap(); - await assertSystemContext(`pulse-system-${type}`); - }); - } - await check( - 'legacy system metadata wins over conflicting resource/metric hints', - async () => { - await page.evaluate(() => - window.__systemAlertScope.updateSystem('future-condition', true), - ); - const card = page.locator('#alert-legacy-system-id'); - await card.waitFor({ state: 'visible' }); - assert.equal(await card.getByRole('link').count(), 0); - assert.doesNotMatch(await card.textContent(), /limit:|wrong-node/); - await card.getByRole('button', { name: 'Ask Pulse Assistant', exact: true }).click(); - await assertSystemContext('legacy-system-id'); - }, - ); - await check( - 'desktop and phone History reconstruct the retained service marker', - async () => { - const row = - width < 500 - ? history - .locator('[data-alert-history-row-key]') - .filter({ hasText: legacy.message }) - : history.getByRole('row').filter({ hasText: legacy.message }); - assert.equal(await row.locator('[data-alert-history-action="resource"]').count(), 0); - await row.getByRole('button', { name: 'Ask Pulse Assistant about this alert' }).click(); - await assertSystemContext(legacy.id); - }, - ); - await capture(history, 'retained-history'); - await check( - 'monitored VM called Pulse retains its link and targeted affordances', - async () => { - const card = page.locator('#alert-resource-cpu'); - assert.equal( - await card.getByRole('link', { name: 'Pulse', exact: true }).getAttribute('href'), - '/proxmox/overview', - ); - assert.match(await card.textContent(), /limit: 80%/); - await card - .getByRole('button', { name: 'Have Patrol investigate' }) - .waitFor({ state: 'visible' }); - }, - ); - await check( - 'authoritative recovery withdraws only the current service warning', - async () => { - await page.evaluate(() => window.__systemAlertScope.recover()); - assert.equal(await overview.locator('[id^="alert-legacy"]').count(), 0); - assert.equal(await overview.locator('#alert-resource-cpu').count(), 1); - assert.ok( - (await page.evaluate(() => window.__systemAlertScope.historyRows())).some( - (r) => r.id === legacy.id, - ), - ); - }, - ); - assert.deepEqual(errors, []); - assert.deepEqual(offOrigin, []); - assert.equal(writes.length, 2); - assert.ok( - !requests.some((r) => /patrol\/run|execute|diagnostic|ai\/chat|guest-agent/.test(r.path)), - ); - results.push({ - name, - width, - dark, - browser_version: browser.version(), - checks, - requests, - writes, - errors, - offOrigin, - }); - } finally { - await browser.close(); - browser = null; - } - } - fs.writeFileSync( - path.join(output, 'result.json'), - JSON.stringify( - { passed: true, playwright_version: version, content_sha256: hashes, results, screenshots }, - null, - 2, - ) + '\n', - ); - console.log( - JSON.stringify({ - passed: true, - groups: results.length, - checks: results.reduce((n, r) => n + r.checks.length, 0), - screenshots: screenshots.length, - playwright_version: version, - }), - ); - } catch (error) { - fs.writeFileSync( - path.join(output, 'failed.json'), - JSON.stringify( - { passed: false, phase, error: error.message, results, screenshots }, - null, - 2, - ) + '\n', - ); - throw error; - } finally { - if (browser) await browser.close(); - await server.close(); - } -})().catch((error) => { - console.error(error); - process.exitCode = 1; -}); diff --git a/frontend-modern/browser-tests/system-alert-scope.html b/frontend-modern/browser-tests/system-alert-scope.html deleted file mode 100644 index 5538407dca..0000000000 --- a/frontend-modern/browser-tests/system-alert-scope.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - System-alert scope verification - - -
    - - - diff --git a/frontend-modern/browser-tests/system-alert-scope.tsx b/frontend-modern/browser-tests/system-alert-scope.tsx deleted file mode 100644 index ca02744e78..0000000000 --- a/frontend-modern/browser-tests/system-alert-scope.tsx +++ /dev/null @@ -1,107 +0,0 @@ -// Production alert components/state/router and real Assistant handoff store. -// Synthetic API evidence; no native service, model, diagnostic or command. -import { createSignal, Show } from 'solid-js'; -import { render } from 'solid-js/web'; -import { Router, Route } from '@solidjs/router'; -import { OverviewTab } from '../src/features/alerts/OverviewTab'; -import { AlertHistoryTableSection } from '../src/features/alerts/AlertHistoryTableSection'; -import { useAlertHistoryState } from '../src/features/alerts/useAlertHistoryState'; -import { makeSystemAlert } from '../src/features/alerts/__fixtures__/systemAlerts'; -import { aiChatStore } from '../src/stores/aiChat'; -import type { Alert } from '../src/types/api'; -import type { Resource } from '../src/types/resource'; -import '../src/index.css'; - -aiChatStore.setEnabled(true); -const pulseVM = { id: 'vm-pulse', name: 'Pulse', type: 'vm' } as Resource; -const control = makeSystemAlert('cpu', { - id: 'resource-cpu', - resourceId: pulseVM.id, - metadata: { resourceType: 'vm' }, - value: 92, - threshold: 80, - message: 'Synthetic CPU reading on the monitored VM called Pulse.', -}); - -function Fixture() { - const initial = makeSystemAlert(); - const [alerts, setAlerts] = createSignal>({ - [initial.id]: initial, - [control.id]: control, - }); - const [showAcknowledged, setShowAcknowledged] = createSignal(true); - const history = useAlertHistoryState({ - activeAlerts: alerts, - getResource: () => pulseVM, - allResources: () => [pulseVM], - }); - (window as any).__systemAlertScope = { - updateSystem: (type: string, legacy = false) => { - const next = makeSystemAlert( - type, - legacy - ? { - id: 'legacy-system-id', - resourceId: 'vm-wrong', - node: 'wrong-node', - metadata: { systemAlert: true, resourceType: 'vm' }, - threshold: 80, - } - : {}, - ); - setAlerts({ [next.id]: next, [control.id]: control }); - }, - recover: () => setAlerts({ [control.id]: control }), - alerts, - historyRows: history.alertData, - context: () => aiChatStore.context, - closeExplanation: () => aiChatStore.close(), - }; - return ( -
    -

    Pulse system-alert evidence

    -

    - Synthetic observations. No workload, diagnostic or model is contacted. -

    -
    - - setAlerts((previous) => ({ ...previous, [id]: { ...previous[id], ...updates } })) - } - showAcknowledged={showAcknowledged} - setShowAcknowledged={setShowAcknowledged} - showQuickTip={() => false} - dismissQuickTip={() => {}} - alertsDisabled={() => false} - /> -
    -
    -

    History

    - -
    - -
    -

    - Test capture of the real explanation store; no Assistant request is executed. -

    -
    -            {JSON.stringify(aiChatStore.context, null, 2)}
    -          
    -
    -
    -
    - ); -} -render( - () => ( - - - - ), - document.getElementById('root')!, -); diff --git a/frontend-modern/browser-verification/4f80c415915b4559.json b/frontend-modern/browser-verification/4f80c415915b4559.json new file mode 100644 index 0000000000..3ddab6142a --- /dev/null +++ b/frontend-modern/browser-verification/4f80c415915b4559.json @@ -0,0 +1,283 @@ +{ + "version": 1, + "base_sha": "86ff402d118f3fc1010acfe04a4d8fcc9cbe5d9e", + "verified_at": "2026-10-22T23:11:14Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/App.tsx", + "frontend-modern/src/AppLayout.tsx", + "frontend-modern/src/api/ai.ts", + "frontend-modern/src/api/aiChat.ts", + "frontend-modern/src/api/aiChatDevStreamFixture.ts", + "frontend-modern/src/api/generated/aiChatEvents.ts", + "frontend-modern/src/api/patrol.ts", + "frontend-modern/src/api/workflowPrompts.ts", + "frontend-modern/src/components/AI/Chat/ApprovalCard.tsx", + "frontend-modern/src/components/AI/Chat/AssistantCommandHelpDialog.tsx", + "frontend-modern/src/components/AI/Chat/ChatMessages.tsx", + "frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx", + "frontend-modern/src/components/AI/Chat/MessageItem.tsx", + "frontend-modern/src/components/AI/Chat/ModelSelector.tsx", + "frontend-modern/src/components/AI/Chat/QuestionCard.tsx", + "frontend-modern/src/components/AI/Chat/SlashCommandAutocomplete.tsx", + "frontend-modern/src/components/AI/Chat/ThinkingBlock.tsx", + "frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx", + "frontend-modern/src/components/AI/Chat/activeTurnStatus.ts", + "frontend-modern/src/components/AI/Chat/aiCodeHighlight.ts", + "frontend-modern/src/components/AI/Chat/assistantAnswerText.ts", + "frontend-modern/src/components/AI/Chat/assistantModelRoutes.ts", + "frontend-modern/src/components/AI/Chat/assistantNotifications.ts", + "frontend-modern/src/components/AI/Chat/assistantOutputHygiene.ts", + "frontend-modern/src/components/AI/Chat/assistantPanelWidth.ts", + "frontend-modern/src/components/AI/Chat/assistantSlashCommands.ts", + "frontend-modern/src/components/AI/Chat/assistantTurnSummary.ts", + "frontend-modern/src/components/AI/Chat/composerPaste.ts", + "frontend-modern/src/components/AI/Chat/hooks/useChat.ts", + "frontend-modern/src/components/AI/Chat/hooks/useExplanationRequest.ts", + "frontend-modern/src/components/AI/Chat/index.tsx", + "frontend-modern/src/components/AI/Chat/markdownMorph.ts", + "frontend-modern/src/components/AI/Chat/reasoningSummary.ts", + "frontend-modern/src/components/AI/Chat/recentSessionsModel.ts", + "frontend-modern/src/components/AI/Chat/streamActivityTiming.ts", + "frontend-modern/src/components/AI/Chat/streamEventGrouping.ts", + "frontend-modern/src/components/AI/Chat/transcriptExport.ts", + "frontend-modern/src/components/AI/Chat/types.ts", + "frontend-modern/src/components/AI/Chat/workflowStarters.ts", + "frontend-modern/src/components/AI/Chat/workflowStatusDisplay.ts", + "frontend-modern/src/components/AI/FindingsPanel.tsx", + "frontend-modern/src/components/AI/aiChatUtils.ts", + "frontend-modern/src/components/Alerts/IncidentAssistantHandoffButton.tsx", + "frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx", + "frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx", + "frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts", + "frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts", + "frontend-modern/src/components/Infrastructure/MaintenanceVerificationSection.tsx", + "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx", + "frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx", + "frontend-modern/src/components/Settings/AIModelSelectionSection.tsx", + "frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx", + "frontend-modern/src/components/Settings/AISettings.tsx", + "frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx", + "frontend-modern/src/components/Settings/reportingPanelModel.ts", + "frontend-modern/src/components/Settings/settingsHeaderMeta.ts", + "frontend-modern/src/components/Settings/settingsNavCatalog.ts", + "frontend-modern/src/components/Settings/settingsNavigationModel.ts", + "frontend-modern/src/components/Settings/settingsPanelRegistry.ts", + "frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx", + "frontend-modern/src/components/Settings/useAISettingsState.ts", + "frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts", + "frontend-modern/src/components/Settings/useReportingPanelState.ts", + "frontend-modern/src/components/Settings/useSettingsNavigation.ts", + "frontend-modern/src/components/Workloads/guestAssistantContextModel.ts", + "frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts", + "frontend-modern/src/components/patrol/ApprovalSection.tsx", + "frontend-modern/src/components/patrol/InvestigationMessages.tsx", + "frontend-modern/src/components/patrol/RunHistoryEntry.tsx", + "frontend-modern/src/components/patrol/ToolExecutionBlock.tsx", + "frontend-modern/src/components/patrol/toolPresentation.ts", + "frontend-modern/src/components/shared/AIModelPicker.tsx", + "frontend-modern/src/components/shared/MobileNavBar.tsx", + "frontend-modern/src/components/shared/commandPaletteModel.ts", + "frontend-modern/src/components/shared/useCommandPaletteState.ts", + "frontend-modern/src/components/shared/useUpgradeNavigation.ts", + "frontend-modern/src/constants/apiScopes.ts", + "frontend-modern/src/content/help/ai.ts", + "frontend-modern/src/features/alerts/AlertHistoryItemActions.tsx", + "frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx", + "frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx", + "frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx", + "frontend-modern/src/features/alerts/MobileAlertHistoryInvestigationDialog.tsx", + "frontend-modern/src/features/alerts/alertHistoryModel.ts", + "frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx", + "frontend-modern/src/features/patrol/PatrolSetupPanel.tsx", + "frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts", + "frontend-modern/src/features/patrol/patrolSetupModel.ts", + "frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts", + "frontend-modern/src/i18n/messages.de.ts", + "frontend-modern/src/i18n/messages.es.ts", + "frontend-modern/src/i18n/messages.ts", + "frontend-modern/src/i18n/policy.ts", + "frontend-modern/src/index.css", + "frontend-modern/src/pages/Actions.tsx", + "frontend-modern/src/routing/resourceLinks.ts", + "frontend-modern/src/stores/aiAvailability.ts", + "frontend-modern/src/stores/aiChat.ts", + "frontend-modern/src/stores/aiIntelligence.ts", + "frontend-modern/src/stores/sessionPresentationPolicy.ts", + "frontend-modern/src/stores/sessionSettingsCapabilities.ts", + "frontend-modern/src/types/ai.ts", + "frontend-modern/src/types/config.ts", + "frontend-modern/src/useAppRuntimeState.ts", + "frontend-modern/src/utils/aiChatPresentation.ts", + "frontend-modern/src/utils/aiControlLevelPresentation.ts", + "frontend-modern/src/utils/aiCostPresentation.ts", + "frontend-modern/src/utils/aiFindingPresentation.ts", + "frontend-modern/src/utils/aiSettingsPresentation.ts", + "frontend-modern/src/utils/alertFormatters.ts", + "frontend-modern/src/utils/alertTargetTypes.ts", + "frontend-modern/src/utils/alerts.ts", + "frontend-modern/src/utils/assistantPageContext.ts", + "frontend-modern/src/utils/chatIdentifiers.ts", + "frontend-modern/src/utils/resourceAssistantContextModel.ts" + ], + "content_sha256": { + "frontend-modern/src/App.tsx": "e72ee427e76a10f4c19888b089dab120d0011985bf27e1f95a4d91ed76cabd0d", + "frontend-modern/src/AppLayout.tsx": "21c3aa8db3010c46557f4057fc2925407b776cce0f837b40ce603d59140dfc67", + "frontend-modern/src/api/ai.ts": "9e39a8d9973a226126cda3c01e31ee752be1bf7c4c4f480b97d47618ac91be9b", + "frontend-modern/src/api/aiChat.ts": "deleted", + "frontend-modern/src/api/aiChatDevStreamFixture.ts": "deleted", + "frontend-modern/src/api/generated/aiChatEvents.ts": "deleted", + "frontend-modern/src/api/patrol.ts": "67d03817e7982d779d156a01f51dc1a180aa1759b80cb415c6be6b2e1b76f559", + "frontend-modern/src/api/workflowPrompts.ts": "27976c493e1bbd43ff0f15f8eae943b1c8e0ba0c13aeb3bc6f65760e088d7501", + "frontend-modern/src/components/AI/Chat/ApprovalCard.tsx": "deleted", + "frontend-modern/src/components/AI/Chat/AssistantCommandHelpDialog.tsx": "deleted", + "frontend-modern/src/components/AI/Chat/ChatMessages.tsx": "deleted", + "frontend-modern/src/components/AI/Chat/MentionAutocomplete.tsx": "deleted", + "frontend-modern/src/components/AI/Chat/MessageItem.tsx": "deleted", + "frontend-modern/src/components/AI/Chat/ModelSelector.tsx": "deleted", + "frontend-modern/src/components/AI/Chat/QuestionCard.tsx": "deleted", + "frontend-modern/src/components/AI/Chat/SlashCommandAutocomplete.tsx": "deleted", + "frontend-modern/src/components/AI/Chat/ThinkingBlock.tsx": "deleted", + "frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx": "deleted", + "frontend-modern/src/components/AI/Chat/activeTurnStatus.ts": "deleted", + "frontend-modern/src/components/AI/Chat/aiCodeHighlight.ts": "deleted", + "frontend-modern/src/components/AI/Chat/assistantAnswerText.ts": "deleted", + "frontend-modern/src/components/AI/Chat/assistantModelRoutes.ts": "deleted", + "frontend-modern/src/components/AI/Chat/assistantNotifications.ts": "deleted", + "frontend-modern/src/components/AI/Chat/assistantOutputHygiene.ts": "deleted", + "frontend-modern/src/components/AI/Chat/assistantPanelWidth.ts": "deleted", + "frontend-modern/src/components/AI/Chat/assistantSlashCommands.ts": "deleted", + "frontend-modern/src/components/AI/Chat/assistantTurnSummary.ts": "deleted", + "frontend-modern/src/components/AI/Chat/composerPaste.ts": "deleted", + "frontend-modern/src/components/AI/Chat/hooks/useChat.ts": "deleted", + "frontend-modern/src/components/AI/Chat/hooks/useExplanationRequest.ts": "deleted", + "frontend-modern/src/components/AI/Chat/index.tsx": "deleted", + "frontend-modern/src/components/AI/Chat/markdownMorph.ts": "deleted", + "frontend-modern/src/components/AI/Chat/reasoningSummary.ts": "deleted", + "frontend-modern/src/components/AI/Chat/recentSessionsModel.ts": "deleted", + "frontend-modern/src/components/AI/Chat/streamActivityTiming.ts": "deleted", + "frontend-modern/src/components/AI/Chat/streamEventGrouping.ts": "deleted", + "frontend-modern/src/components/AI/Chat/transcriptExport.ts": "deleted", + "frontend-modern/src/components/AI/Chat/types.ts": "deleted", + "frontend-modern/src/components/AI/Chat/workflowStarters.ts": "deleted", + "frontend-modern/src/components/AI/Chat/workflowStatusDisplay.ts": "deleted", + "frontend-modern/src/components/AI/FindingsPanel.tsx": "fdf9fd2d00b47ec4a46b001013e6ed4005e34b0b37145386d1a6039200fa9e0b", + "frontend-modern/src/components/AI/aiChatUtils.ts": "3072f576a5945c531c2e497b7a46ab30a0b25790048640a34c95e85ec9b9d45b", + "frontend-modern/src/components/Alerts/IncidentAssistantHandoffButton.tsx": "deleted", + "frontend-modern/src/components/Alerts/IncidentTimelinePanel.tsx": "787bd2edefdd12ba3f464b4ff352e6595664b508035fc6b7662934ba20c02fcb", + "frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx": "a82971374c272ae4bb0c4049c66b6e11c2558df99afde0922db7506368e9021d", + "frontend-modern/src/components/Alerts/alertAssistantHandoffModel.ts": "deleted", + "frontend-modern/src/components/Alerts/incidentAssistantHandoffModel.ts": "deleted", + "frontend-modern/src/components/Infrastructure/MaintenanceVerificationSection.tsx": "321aa5ce41305d23d18922be6fc1820945c3cce773c45dbecc83284db2a7c916", + "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx": "689d876f2f9135adedb33499602d4a243de5c76b376a5b1a5c3b8b5ef694a4e2", + "frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx": "deleted", + "frontend-modern/src/components/Settings/AIModelSelectionSection.tsx": "8ddb4ce624605d67aaed1c77b6fe712e2142f581609f0a7161b052578b0b0cee", + "frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx": "0047ddacb376cf79d6b83cc1b91628f122b363130348bafa5984c761657630a7", + "frontend-modern/src/components/Settings/AISettings.tsx": "3348671852cbca68e6e7d8b2f8aad86a6a8ea91f9da85728907345d257169fda", + "frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx": "ad94d888f84a4682d266f129ff7b427f94dea2e1885539b5b2a1f64e88e431b5", + "frontend-modern/src/components/Settings/reportingPanelModel.ts": "875175881a1a0164d14bc153e60a9a8bb8e7fceb3b5a8d2e752719cf77f3e3f6", + "frontend-modern/src/components/Settings/settingsHeaderMeta.ts": "f93aaa9378d7648b6a42a966ea24b19417f90aacff7dadf5170ead9cca831551", + "frontend-modern/src/components/Settings/settingsNavCatalog.ts": "5446e232d7a44e3001d4da6de79bba8c9887af468deab366536a4a7e2a42a50c", + "frontend-modern/src/components/Settings/settingsNavigationModel.ts": "d8d5b803a61f3f4c35708db962ddc1ecf7597c4a005e91ec19648fb3e25ba8e9", + "frontend-modern/src/components/Settings/settingsPanelRegistry.ts": "27e353c45eb1940736cc8f5f2fa102151d581b1b88ab8a3d2584761ab4ea7a49", + "frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx": "fadb22116ee90484e64fe8672d5bed1573d8d64f8adf61b540005a24c24cc84f", + "frontend-modern/src/components/Settings/useAISettingsState.ts": "e97ece391b19d095c945a18389f810dc5421bbf653779f3d61320e8d11c0ea3e", + "frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts": "b88dfe6cfa536d8536b65c83828e6c5f84a36419515ea29d7e6b5f208bda10f6", + "frontend-modern/src/components/Settings/useReportingPanelState.ts": "7ae66a651760b32c85a167f56ce8886be3f71a766f578af89034363a4353ccf0", + "frontend-modern/src/components/Settings/useSettingsNavigation.ts": "b9dcd0e1c705064725b6a3a6e0b04e4500253112cdd07af25befb67e587a3688", + "frontend-modern/src/components/Workloads/guestAssistantContextModel.ts": "deleted", + "frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts": "0daedd457745a5f74bad7f5e951f7c6a4bcac845ece3d2de59c1704b037d1534", + "frontend-modern/src/components/patrol/ApprovalSection.tsx": "92139695915c2c47be18ec7c8e4f9c5deb6afa50c3e6ffdf16651c975a50606f", + "frontend-modern/src/components/patrol/InvestigationMessages.tsx": "195811b88a0353bb4da12a1ae57547842abc6852602a11f458f5ba985ee0edfa", + "frontend-modern/src/components/patrol/RunHistoryEntry.tsx": "c842eb3c9b1863c6170a352ec9e02965eaad93d3d6cbf0e25a77371119721710", + "frontend-modern/src/components/patrol/ToolExecutionBlock.tsx": "17fd64b050a013d1be783da9ca154402c98c63d52787f8ce407ab1054eae562f", + "frontend-modern/src/components/patrol/toolPresentation.ts": "155d96ffd703352017b7c4196654b315d25b5f2b3eb282f80a39be9d5177bc0d", + "frontend-modern/src/components/shared/AIModelPicker.tsx": "73a06ba8775f0306ea60d764e8a014ce89d4362443dafa78bea33549a03f726c", + "frontend-modern/src/components/shared/MobileNavBar.tsx": "2a7689b464520a646bad3d53ccaed84106ed905cdc29d20dacc946ad74d9c6aa", + "frontend-modern/src/components/shared/commandPaletteModel.ts": "11a47d18c203e99ca4fe0659ca4554f0896c275cb75ad230229dbd7e043e7c54", + "frontend-modern/src/components/shared/useCommandPaletteState.ts": "5ac21ed7c5b100a5756e6a637eac1a29c27e8ee34100c9fd0fc7bdf926d68cf2", + "frontend-modern/src/components/shared/useUpgradeNavigation.ts": "deleted", + "frontend-modern/src/constants/apiScopes.ts": "c66759f3e2122ad5f3b2a7fd9034ef80fdd3bff58671a362cb07f0607663663a", + "frontend-modern/src/content/help/ai.ts": "243b2a084e531c109c6e42a4e79624b962b56ee9c195c29e9bdbda5802e6a65d", + "frontend-modern/src/features/alerts/AlertHistoryItemActions.tsx": "c929b66ebdcc056e262cb93b5cd1ceecb49bfd9b191d69a35f1a08c4afa06f97", + "frontend-modern/src/features/alerts/AlertHistoryMobileList.tsx": "e2c44817251a013fed4f4663412fea2e5b952f551391fbcd043ef39148c431a4", + "frontend-modern/src/features/alerts/AlertOverviewAlertCard.tsx": "3892f338eacde2b0669b2462fc737df8a95510f9148b71901fb12fb7b8e025ee", + "frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx": "bacec4fb0d58c3fd2dce859626df9767171bf26ca6ee1934741428b876dce84e", + "frontend-modern/src/features/alerts/MobileAlertHistoryInvestigationDialog.tsx": "b88c28b9e00787aa17f8e714d0066fa201925ef3a492339c501e86bb74947594", + "frontend-modern/src/features/alerts/alertHistoryModel.ts": "1b41f6e8d1d42a88db0e20b6a2b398c94a5ca4ce81cb7e95f3a63839366b7ea0", + "frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "5d19f4bd608746abe6ab573e25c9ef9adba2a802095433d013822d2b165bcb3f", + "frontend-modern/src/features/patrol/PatrolSetupPanel.tsx": "7cb868d6c3f8d002c044ab02e3e38544a983786838a72530e46c95326353ecf3", + "frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts": "142188a1d9c64ac0881ea79d703e93fc912818c33ce028542101ead4b155e139", + "frontend-modern/src/features/patrol/patrolSetupModel.ts": "863a659b40263fcb8bb007d24bd4363ece8284c6cadce9b6fde4369ef54c8a49", + "frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts": "29bd1b96b3ef7f44302f413ec8022840d24c207020347ddf57d19ee9c902d988", + "frontend-modern/src/i18n/messages.de.ts": "e927870bd927b27b5467758f6886942e6afef1d1bc75e0e9935dc1c3bc68e039", + "frontend-modern/src/i18n/messages.es.ts": "4251ef01a22604a6317c2223dc0bd5bcc0ef3e14c837097f0123710f3dc2070a", + "frontend-modern/src/i18n/messages.ts": "25a1b37e8900cdf88a0bfd1a3e33b5edcfdfe5774a4bd2c03095c361fa7e2950", + "frontend-modern/src/i18n/policy.ts": "2e1413fffb077884cedebc3ea477bd01b4df32f1d094f3b261f0cd97241d8467", + "frontend-modern/src/index.css": "09ed04d0f2a88c27184eba9226e797985a0774f786623eb581928cedb4bfd4a2", + "frontend-modern/src/pages/Actions.tsx": "f3277e8be19f81afca020238b62106527208a91367d20e5fe734a1e32d40ed3b", + "frontend-modern/src/routing/resourceLinks.ts": "7c48bb2d5cccfcc3c65b35b56b3170504a9be909aa838b5eb602eb0ee632f588", + "frontend-modern/src/stores/aiAvailability.ts": "f1b78ce248568fc013c797b1221574effbc4a71539cbbd4bd4bf95d2816e6754", + "frontend-modern/src/stores/aiChat.ts": "deleted", + "frontend-modern/src/stores/aiIntelligence.ts": "664d8c881270e2a0d98bab354b799e71239327a8ed178ad68e271c2fe31f3a63", + "frontend-modern/src/stores/sessionPresentationPolicy.ts": "3c220d46cce14697bb2b631b6386e6fda4e164e276573052017630d055bf477f", + "frontend-modern/src/stores/sessionSettingsCapabilities.ts": "7279bbe6dbac0e59a42b87698bb3874557fbc82e526d2319353f0905f4dafaf3", + "frontend-modern/src/types/ai.ts": "f8b31a55997c7ce17939bf97bf42c0419878e2af42ec0d12472363bea804b2a6", + "frontend-modern/src/types/config.ts": "464fd8a6f618c3a564f888f59ea14ef09ed7903a2fabf934ae8ab5eea990b97b", + "frontend-modern/src/useAppRuntimeState.ts": "85a42cefa9f121a92652524f117e823848af344a03af9b920fb0cf14778634bc", + "frontend-modern/src/utils/aiChatPresentation.ts": "deleted", + "frontend-modern/src/utils/aiControlLevelPresentation.ts": "deleted", + "frontend-modern/src/utils/aiCostPresentation.ts": "c10444ea568a534178d9b739219e606b4ce546e4b2c6d1f7a79f26795c6b11ca", + "frontend-modern/src/utils/aiFindingPresentation.ts": "4545e3fd08bc09d170d2d2a51216ea28c8af89836dad86ecb2f15a03707414a9", + "frontend-modern/src/utils/aiSettingsPresentation.ts": "daccb31a891291f72e99a2e859c98aa3fa867a3ac10fa9ab0fb2f6ccd78b7bd8", + "frontend-modern/src/utils/alertFormatters.ts": "deleted", + "frontend-modern/src/utils/alertTargetTypes.ts": "deleted", + "frontend-modern/src/utils/alerts.ts": "c39e61c7eb96021a6b321528db46c06786ba428c56fc0f8a80fdcfb774ed0d86", + "frontend-modern/src/utils/assistantPageContext.ts": "deleted", + "frontend-modern/src/utils/chatIdentifiers.ts": "deleted", + "frontend-modern/src/utils/resourceAssistantContextModel.ts": "deleted" + }, + "routes": [ + "/proxmox/overview", + "/alerts/overview", + "/alerts/history", + "/patrol", + "/patrol/activity", + "/settings/pulse-intelligence/assistant#external-agent-setup", + "/settings?tab=assistant#external-agent-setup", + "/settings/pulse-intelligence/agents#external-agent-setup", + "/settings/pulse-intelligence/provider", + "/settings/security/api" + ], + "viewports": [ + { + "width": 1280, + "height": 800 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "Private mock stack (embedded build of this change, mock mode, AI available, admin session) at 1280x800 and 390x844", + "Desktop and phone app shell: no Assistant launcher, no element naming the Assistant; Patrol and Actions utility tabs present", + "Command palette search for \"assist\" returns no matches", + "Active alert cards show \"Have Patrol investigate\"; on phone it sits under the card's More disclosure with Timeline and Monitoring", + "Alert History rows show the shield Investigate icon, named \"Have Patrol investigate on ,