mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 18:53:37 +00:00
Align Assistant tool context and progress visibility
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -168,27 +168,31 @@ export const PendingToolBlock: Component<PendingToolBlockProps> = (props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="my-0.5 font-mono text-[11px] flex items-center gap-1.5 px-2 py-1 rounded bg-surface-alt border border-border">
|
||||
<Show
|
||||
when={status() === 'waiting'}
|
||||
fallback={<LoaderCircleIcon class={activityIconClass()} aria-label={statusLabel()} />}
|
||||
>
|
||||
<ClockIcon class={activityIconClass()} aria-label={statusLabel()} />
|
||||
</Show>
|
||||
<div class="my-0.5 font-mono text-[11px] rounded border border-border bg-surface-alt px-2 py-1">
|
||||
<div class="flex min-w-0 items-center gap-1.5">
|
||||
<Show
|
||||
when={status() === 'waiting'}
|
||||
fallback={<LoaderCircleIcon class={activityIconClass()} aria-label={statusLabel()} />}
|
||||
>
|
||||
<ClockIcon class={activityIconClass()} aria-label={statusLabel()} />
|
||||
</Show>
|
||||
|
||||
<span class="text-muted uppercase text-[9px] font-medium tracking-wider min-w-[50px]">
|
||||
{toolLabel()}
|
||||
</span>
|
||||
|
||||
<span class="text-base-content truncate flex-1">{inputSummary()}</span>
|
||||
<Show when={progressText()}>
|
||||
<span class="hidden sm:inline-block max-w-[180px] truncate text-[10px] text-muted">
|
||||
{progressText()}
|
||||
<span class="text-muted uppercase text-[9px] font-medium tracking-wider min-w-[50px]">
|
||||
{toolLabel()}
|
||||
</span>
|
||||
</Show>
|
||||
<span class="shrink-0 text-[10px] text-muted">{statusLabel()}</span>
|
||||
<Show when={elapsedLabel()}>
|
||||
<span class="shrink-0 text-[10px] text-muted">{elapsedLabel()}</span>
|
||||
|
||||
<span class="min-w-0 flex-1 truncate text-base-content">{inputSummary()}</span>
|
||||
<span class="shrink-0 text-[10px] text-muted">{statusLabel()}</span>
|
||||
<Show when={elapsedLabel()}>
|
||||
<span class="shrink-0 text-[10px] text-muted">{elapsedLabel()}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={progressText()}>
|
||||
<div class="mt-0.5 min-w-0 pl-[calc(0.75rem+56px)] text-[10px] leading-snug text-muted">
|
||||
<span class="block truncate" title={progressText()}>
|
||||
{progressText()}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -4338,6 +4338,10 @@ func assistantPromptNeedsFullToolManifest(normalized string) bool {
|
||||
" snapshot ",
|
||||
" approve ",
|
||||
" deny ",
|
||||
" alert ",
|
||||
" alerts ",
|
||||
" finding ",
|
||||
" findings ",
|
||||
" cpu ",
|
||||
" memory ",
|
||||
" disk ",
|
||||
|
||||
@@ -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{},
|
||||
|
||||
Reference in New Issue
Block a user