diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index e069196b0..b2c7ea98d 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -299,6 +299,12 @@ runtime cost control, and shared AI transport surfaces. the selected model owns investigation and action choice inside Pulse policy. Direct text turns such as exact-reply diagnostics must withhold tools and use a scoped system prompt that does not advertise unavailable tools. + Short alert and finding prompts are not direct-text turns: prompts such as + "Alerts count", "active alerts", or "findings count" must keep the full + governed manifest, including `pulse_alerts`, so the selected model can + choose the canonical Pulse alert/finding tool instead of receiving a + no-tools system prompt and asking the operator to paste context Pulse + already owns. Inventory, count, overview, status, list, and breakdown prompts that only need canonical Pulse resource state must expose the canonical query/clarification path instead of shell/read/control tools, so prompts @@ -364,7 +370,14 @@ runtime cost control, and shared AI transport surfaces. 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. + 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. 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 diff --git a/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx b/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx index c109f243f..1a6c3e08d 100644 --- a/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx +++ b/frontend-modern/src/components/AI/Chat/ToolExecutionBlock.tsx @@ -168,27 +168,31 @@ export const PendingToolBlock: Component = (props) => { }); return ( -
- } - > - - +
+
+ } + > + + - - {toolLabel()} - - - {inputSummary()} - - - {statusLabel()} - - {elapsedLabel()} + + {inputSummary()} + {statusLabel()} + + {elapsedLabel()} + +
+ +
+ + {progressText()} + +
); diff --git a/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx index ad4ea0654..f55997755 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx +++ b/frontend-modern/src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx @@ -312,7 +312,10 @@ describe('PendingToolBlock', () => { )); expect(screen.getByText('running')).toBeInTheDocument(); - expect(screen.getByText('Running command.')).toBeInTheDocument(); + const progress = screen.getByText('Running command.'); + expect(progress).toBeInTheDocument(); + expect(progress).toHaveAttribute('title', 'Running command.'); + expect(progress.className).not.toContain('hidden'); }); it('renders waiting status without a spinner', () => { diff --git a/internal/ai/chat/service.go b/internal/ai/chat/service.go index 883ab3e27..f8e02049c 100644 --- a/internal/ai/chat/service.go +++ b/internal/ai/chat/service.go @@ -4338,6 +4338,10 @@ func assistantPromptNeedsFullToolManifest(normalized string) bool { " snapshot ", " approve ", " deny ", + " alert ", + " alerts ", + " finding ", + " findings ", " cpu ", " memory ", " disk ", diff --git a/internal/ai/chat/service_tooling_test.go b/internal/ai/chat/service_tooling_test.go index 38ff0685e..5052cbf61 100644 --- a/internal/ai/chat/service_tooling_test.go +++ b/internal/ai/chat/service_tooling_test.go @@ -223,6 +223,35 @@ func TestToolsForAssistantTurn_ActionAndDiagnosticsKeepFullManifest(t *testing.T } } +func TestToolsForAssistantTurn_AlertPromptsKeepFullManifest(t *testing.T) { + exec := tools.NewPulseToolExecutor(tools.ExecutorConfig{ + StateProvider: fakeStateProvider{}, + AgentServer: fakeAgentServer{}, + ReadState: &fakeCanonicalReadState{}, + ControlLevel: tools.ControlLevelControlled, + }) + + svc := &Service{executor: exec} + for _, prompt := range []string{ + "Alerts count", + "active alerts", + "findings count", + } { + toolsList := svc.toolsForAssistantTurn(prompt, false, false, false) + set := toolNameSet(toolsList) + if !set["pulse_alerts"] || !set["pulse_query"] { + t.Fatalf("expected full manifest for %q, got %#v", prompt, set) + } + promptText := svc.buildSystemPromptForOfferedTools(toolsList) + if strings.Contains(promptText, "No Pulse tools are offered for this turn") { + t.Fatalf("expected alert prompt to avoid text-only tool boundary, got %q", promptText) + } + if !strings.Contains(promptText, "pulse_alerts: mode=mixed") { + t.Fatalf("expected alert prompt system prompt to advertise pulse_alerts, got %q", promptText) + } + } +} + func TestToolsForAssistantTurn_ModelHandoffKeepsFullManifest(t *testing.T) { exec := tools.NewPulseToolExecutor(tools.ExecutorConfig{ StateProvider: fakeStateProvider{},