mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Show Assistant context tools as activity rows
This commit is contained in:
@@ -1364,6 +1364,20 @@ runtime cost control, and shared AI transport surfaces.
|
||||
out` cannot read like assistant output. 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.
|
||||
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
|
||||
That shared drawer ownership also covers passive resource reads while the
|
||||
@@ -2003,6 +2017,13 @@ runtime cost control, and shared AI transport surfaces.
|
||||
|
||||
## Current State
|
||||
|
||||
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.
|
||||
|
||||
Assistant provider retries are a first-class visible workflow state, not a
|
||||
hidden server log. The referenced OpenCode source at fetched `dev` commit
|
||||
`7ae856a9e97130f664f6f11fa5871a2795de9902` defines retry session status in
|
||||
|
||||
@@ -128,6 +128,12 @@ 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.
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -307,6 +307,13 @@ describe('AIChatAPI', () => {
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
expect(onEvent.mock.calls[7][0]).toMatchObject({
|
||||
type: 'content',
|
||||
data: {
|
||||
text: expect.stringContaining('separate visible activity rows'),
|
||||
},
|
||||
});
|
||||
expect(onEvent.mock.calls[7][0].data.text).not.toContain('one compact context activity row');
|
||||
expect(onEvent.mock.calls[8][0]).toMatchObject({
|
||||
type: 'done',
|
||||
data: {
|
||||
|
||||
@@ -358,7 +358,7 @@ const buildContextGroupFixtureEvents = (model?: string): AIChatStreamEvent[] =>
|
||||
{
|
||||
type: 'content',
|
||||
data: {
|
||||
text: 'The context-group fixture gathered the resource identity and recent CPU history as one compact context activity row.',
|
||||
text: 'The context fixture gathered the resource identity and recent CPU history as separate visible activity rows.',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
onCleanup,
|
||||
} from 'solid-js';
|
||||
import CheckIcon from 'lucide-solid/icons/check';
|
||||
import ChevronRightIcon from 'lucide-solid/icons/chevron-right';
|
||||
import CircleAlertIcon from 'lucide-solid/icons/circle-alert';
|
||||
import ClockIcon from 'lucide-solid/icons/clock';
|
||||
import CopyIcon from 'lucide-solid/icons/copy';
|
||||
@@ -32,27 +31,16 @@ import {
|
||||
latestWorkflowStatus,
|
||||
normalizeWorkflowStatusSequence,
|
||||
} from './workflowStatusPresentation';
|
||||
import {
|
||||
isPlaceholderToolInputSummary,
|
||||
parseToolInputSummary,
|
||||
toolValueText,
|
||||
} from './toolPresentation';
|
||||
import type {
|
||||
ChatMessage,
|
||||
ModelRouteRecoveryOption,
|
||||
PendingApproval,
|
||||
PendingQuestion,
|
||||
PendingTool,
|
||||
StreamDisplayEvent,
|
||||
ToolExecution,
|
||||
WorkflowStatus,
|
||||
} from './types';
|
||||
import {
|
||||
AI_CHAT_ASSISTANT_MESSAGE_LABEL,
|
||||
AI_CHAT_CONTEXT_USED_LABEL,
|
||||
} from '@/utils/aiChatPresentation';
|
||||
import { AI_CHAT_ASSISTANT_MESSAGE_LABEL } from '@/utils/aiChatPresentation';
|
||||
import { formatAIModelRouteLabel } from '@/utils/aiProviderPresentation';
|
||||
import { formatIdentifierLabel } from '@/utils/textPresentation';
|
||||
|
||||
interface MessageItemProps {
|
||||
message: ChatMessage;
|
||||
@@ -99,94 +87,6 @@ const markdownClass =
|
||||
const TEXT_RENDER_PACE_MS = 24;
|
||||
const TEXT_RENDER_SNAP = /[\s.,!?;:)\]]/;
|
||||
|
||||
type ContextToolStreamEvent =
|
||||
| (StreamDisplayEvent & { type: 'pending_tool'; pendingTool: PendingTool })
|
||||
| (StreamDisplayEvent & { type: 'tool'; tool: ToolExecution });
|
||||
|
||||
type DisplayStreamItem =
|
||||
| { kind: 'event'; event: StreamDisplayEvent }
|
||||
| { kind: 'context_tool_group'; events: ContextToolStreamEvent[]; key: string };
|
||||
|
||||
const CONTEXT_TOOL_NAMES = new Set([
|
||||
'read',
|
||||
'query',
|
||||
'fetch_url',
|
||||
'get_infrastructure_state',
|
||||
'get_active_alerts',
|
||||
'get_metrics',
|
||||
'get_metrics_history',
|
||||
'get_baselines',
|
||||
'get_patterns',
|
||||
'get_disk_health',
|
||||
'get_storage',
|
||||
'get_storage_config',
|
||||
'get_resource_details',
|
||||
]);
|
||||
|
||||
const normalizedContextToolName = (name?: string) => name?.trim().replace(/^pulse_/, '') || '';
|
||||
|
||||
const isContextToolName = (name?: string) =>
|
||||
CONTEXT_TOOL_NAMES.has(normalizedContextToolName(name));
|
||||
|
||||
const asContextToolStreamEvent = (event: StreamDisplayEvent): ContextToolStreamEvent | null => {
|
||||
if (
|
||||
event.type === 'pending_tool' &&
|
||||
event.pendingTool &&
|
||||
isContextToolName(event.pendingTool.name)
|
||||
) {
|
||||
return event as ContextToolStreamEvent;
|
||||
}
|
||||
if (event.type === 'tool' && event.tool && isContextToolName(event.tool.name)) {
|
||||
return event as ContextToolStreamEvent;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const contextToolEventKey = (event: ContextToolStreamEvent) =>
|
||||
[
|
||||
event.type,
|
||||
event.toolId,
|
||||
event.type === 'pending_tool' ? event.pendingTool.id : event.tool.name,
|
||||
event.startedAt,
|
||||
event.updatedAt,
|
||||
]
|
||||
.map((value) => String(value ?? ''))
|
||||
.join(':');
|
||||
|
||||
const groupContextToolStreamItems = (events: StreamDisplayEvent[]): DisplayStreamItem[] => {
|
||||
const items: DisplayStreamItem[] = [];
|
||||
let pendingGroup: ContextToolStreamEvent[] = [];
|
||||
|
||||
const flushGroup = () => {
|
||||
if (pendingGroup.length >= 2) {
|
||||
items.push({
|
||||
kind: 'context_tool_group',
|
||||
events: pendingGroup,
|
||||
key: `context-tool:${pendingGroup.map(contextToolEventKey).join('|')}`,
|
||||
});
|
||||
} else {
|
||||
for (const event of pendingGroup) {
|
||||
items.push({ kind: 'event', event });
|
||||
}
|
||||
}
|
||||
pendingGroup = [];
|
||||
};
|
||||
|
||||
for (const event of events) {
|
||||
const contextToolEvent = asContextToolStreamEvent(event);
|
||||
if (contextToolEvent) {
|
||||
pendingGroup.push(contextToolEvent);
|
||||
continue;
|
||||
}
|
||||
|
||||
flushGroup();
|
||||
items.push({ kind: 'event', event });
|
||||
}
|
||||
|
||||
flushGroup();
|
||||
return items;
|
||||
};
|
||||
|
||||
const textRenderStep = (size: number) => {
|
||||
if (size <= 12) return 2;
|
||||
if (size <= 48) return 4;
|
||||
@@ -332,88 +232,6 @@ const AssistantMarkdownBlock: Component<{
|
||||
);
|
||||
};
|
||||
|
||||
const ContextToolActivityGroup: Component<{
|
||||
events: ContextToolStreamEvent[];
|
||||
live: boolean;
|
||||
}> = (props) => {
|
||||
const [expanded, setExpanded] = createSignal(false);
|
||||
const active = createMemo(() => props.events.some((event) => event.type === 'pending_tool'));
|
||||
const count = createMemo(() => props.events.length);
|
||||
const countLabel = createMemo(() => `${count()} context ${count() === 1 ? 'check' : 'checks'}`);
|
||||
const statusLabel = createMemo(() => (active() ? 'Gathering context' : 'Context gathered'));
|
||||
const title = createMemo(() => `${statusLabel()} · ${countLabel()}`);
|
||||
const toggle = () => setExpanded((value) => !value);
|
||||
|
||||
return (
|
||||
<div
|
||||
class="my-1 overflow-hidden rounded-md border border-blue-200 bg-blue-50/60 text-[11px] dark:border-blue-900/60 dark:bg-blue-950/20"
|
||||
data-testid="context-tool-group"
|
||||
role="group"
|
||||
aria-label={title()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full min-w-0 items-center gap-2 px-2.5 py-2 text-left transition-colors hover:bg-blue-100/60 focus:outline-none focus:ring-2 focus:ring-blue-500/30 focus:ring-inset dark:hover:bg-blue-950/30"
|
||||
aria-expanded={expanded()}
|
||||
onClick={toggle}
|
||||
>
|
||||
<span
|
||||
class={`h-1.5 w-1.5 shrink-0 rounded-full ${
|
||||
active() ? 'animate-pulse bg-blue-500' : 'bg-emerald-500'
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="shrink-0 text-[10px] font-semibold uppercase tracking-wide text-muted">
|
||||
{statusLabel()}
|
||||
</span>
|
||||
<span class="min-w-0 truncate text-[12px] font-medium text-base-content">
|
||||
{countLabel()}
|
||||
</span>
|
||||
<ChevronRightIcon
|
||||
class={`ml-auto h-3.5 w-3.5 shrink-0 text-muted transition-transform ${
|
||||
expanded() ? 'rotate-90' : ''
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
<Show when={expanded()}>
|
||||
<div class="border-t border-blue-200/70 px-2 py-1.5 dark:border-blue-900/60">
|
||||
<For each={props.events}>
|
||||
{(event) => {
|
||||
const pendingTool = event.type === 'pending_tool' ? event.pendingTool : undefined;
|
||||
const tool = event.type === 'tool' ? event.tool : undefined;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={pendingTool}>
|
||||
{(pending) => <PendingToolBlock tool={pending()} />}
|
||||
</Match>
|
||||
<Match when={tool}>
|
||||
{(completedTool) => (
|
||||
<ToolExecutionBlock
|
||||
startedAt={event.startedAt}
|
||||
completedAt={event.updatedAt}
|
||||
live={props.live}
|
||||
tool={{
|
||||
name: completedTool().name || 'unknown',
|
||||
input: completedTool().input || '{}',
|
||||
rawInput: completedTool().rawInput,
|
||||
output: completedTool().output || '',
|
||||
success: completedTool().success ?? true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* MessageItem - Renders a single message in the chat.
|
||||
*
|
||||
@@ -441,10 +259,6 @@ export const MessageItem: Component<MessageItemProps> = (props) => {
|
||||
const groupedEvents = createMemo(() =>
|
||||
groupStreamEventsForDisplay(props.message.streamEvents || []),
|
||||
);
|
||||
const displayStreamItems = createMemo(() => groupContextToolStreamItems(groupedEvents()));
|
||||
const hasContextToolActivityGroup = createMemo(() =>
|
||||
displayStreamItems().some((item) => item.kind === 'context_tool_group'),
|
||||
);
|
||||
const isSelectedModelRouteEvent = (evt: StreamDisplayEvent) =>
|
||||
evt.type === 'model_switch' && evt.modelEvent === 'selected' && !evt.failedModel?.trim();
|
||||
const isConcreteStreamActivity = (evt: StreamDisplayEvent) => {
|
||||
@@ -510,27 +324,6 @@ export const MessageItem: Component<MessageItemProps> = (props) => {
|
||||
.slice(0, index)
|
||||
.every((evt) => evt.type === 'thinking');
|
||||
|
||||
const contextToolSummaries = createMemo(() => {
|
||||
const events = props.message.streamEvents || [];
|
||||
const summaries = new Set<string>();
|
||||
|
||||
for (const evt of events) {
|
||||
if (evt.type === 'tool' && evt.tool?.name) {
|
||||
const summary = parseToolInputSummary(
|
||||
toolValueText(evt.tool.input),
|
||||
evt.tool.name,
|
||||
evt.tool.rawInput,
|
||||
);
|
||||
const label =
|
||||
summary && !isPlaceholderToolInputSummary(summary)
|
||||
? summary
|
||||
: formatIdentifierLabel(evt.tool.name, { stripPrefix: 'pulse_' });
|
||||
summaries.add(label);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(summaries);
|
||||
});
|
||||
const visibleMessageContent = () =>
|
||||
stripAssistantOutputArtifacts(props.message.content || '').text;
|
||||
const modelRouteLabel = (route?: string) => {
|
||||
@@ -765,23 +558,13 @@ export const MessageItem: Component<MessageItemProps> = (props) => {
|
||||
|
||||
{/* Stream events - chronological display */}
|
||||
<Show when={hasRenderableStreamEvents()}>
|
||||
<For each={displayStreamItems()}>
|
||||
{(item, index) => {
|
||||
const contextGroup = item.kind === 'context_tool_group' ? item : undefined;
|
||||
const evt = item.kind === 'event' ? item.event : undefined;
|
||||
<For each={groupedEvents()}>
|
||||
{(evt, index) => {
|
||||
const contentText = () =>
|
||||
stripAssistantOutputArtifacts(evt?.content || '').text;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={contextGroup}>
|
||||
{(group) => (
|
||||
<ContextToolActivityGroup
|
||||
events={group().events}
|
||||
live={props.message.isStreaming === true}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
<Match
|
||||
when={
|
||||
evt?.type === 'thinking' &&
|
||||
@@ -1019,30 +802,6 @@ export const MessageItem: Component<MessageItemProps> = (props) => {
|
||||
<Show when={isStreamingText() && !isWaitingForFirstToken()}>
|
||||
<span class="inline-block w-1.5 h-4 ml-0.5 align-middle bg-blue-500 dark:bg-blue-400 animate-pulse rounded-full" />
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={
|
||||
!props.message.isStreaming &&
|
||||
!hasContextToolActivityGroup() &&
|
||||
contextToolSummaries().length > 0
|
||||
}
|
||||
>
|
||||
<div class="mt-4 pt-3 border-t border-border-subtle flex flex-wrap gap-2">
|
||||
<span class="text-[10px] uppercase font-semibold text-muted">
|
||||
{AI_CHAT_CONTEXT_USED_LABEL}
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{contextToolSummaries().map((summary) => (
|
||||
<span
|
||||
class="max-w-[18rem] truncate px-1.5 py-0.5 rounded text-[10px] bg-surface-hover text-muted border border-border font-medium"
|
||||
title={summary}
|
||||
>
|
||||
{summary}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1447,7 +1447,7 @@ describe('MessageItem', () => {
|
||||
});
|
||||
|
||||
describe('context tools display', () => {
|
||||
it('groups consecutive context checks into one expandable transcript row', () => {
|
||||
it('shows consecutive completed context checks as individual transcript rows', () => {
|
||||
const events: StreamDisplayEvent[] = [
|
||||
{
|
||||
type: 'tool',
|
||||
@@ -1480,17 +1480,20 @@ describe('MessageItem', () => {
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByTestId('context-tool-group')).toHaveTextContent('Context gathered');
|
||||
expect(screen.getByTestId('context-tool-group')).toHaveTextContent('2 context checks');
|
||||
expect(screen.queryByTestId('tool-execution-block')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Context used')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Context gathered/i }));
|
||||
|
||||
expect(screen.getAllByTestId('tool-execution-block')).toHaveLength(2);
|
||||
expect(screen.getAllByTestId('tool-execution-block')[0]).toHaveAttribute(
|
||||
'data-tool-name',
|
||||
'pulse_read',
|
||||
);
|
||||
expect(screen.getAllByTestId('tool-execution-block')[1]).toHaveAttribute(
|
||||
'data-tool-name',
|
||||
'pulse_get_metrics',
|
||||
);
|
||||
expect(screen.queryByTestId('context-tool-group')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Context used')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows consecutive pending context checks as one active expandable transcript row', () => {
|
||||
it('shows consecutive pending context checks as individual live transcript rows', () => {
|
||||
const events: StreamDisplayEvent[] = [
|
||||
{
|
||||
type: 'pending_tool',
|
||||
@@ -1523,13 +1526,16 @@ describe('MessageItem', () => {
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByTestId('context-tool-group')).toHaveTextContent('Gathering context');
|
||||
expect(screen.getByTestId('context-tool-group')).toHaveTextContent('2 context checks');
|
||||
expect(screen.queryByTestId('pending-tool-block')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Gathering context/i }));
|
||||
|
||||
expect(screen.getAllByTestId('pending-tool-block')).toHaveLength(2);
|
||||
expect(screen.getAllByTestId('pending-tool-block')[0]).toHaveAttribute(
|
||||
'data-tool-name',
|
||||
'pulse_get_resource_details',
|
||||
);
|
||||
expect(screen.getAllByTestId('pending-tool-block')[1]).toHaveAttribute(
|
||||
'data-tool-name',
|
||||
'pulse_get_metrics_history',
|
||||
);
|
||||
expect(screen.queryByTestId('context-tool-group')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps action tools visible as individual transcript rows', () => {
|
||||
@@ -1569,47 +1575,7 @@ describe('MessageItem', () => {
|
||||
expect(screen.getAllByTestId('tool-execution-block')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('shows "Context used" with unique tool summaries when context checks are not grouped', () => {
|
||||
const events: StreamDisplayEvent[] = [
|
||||
{
|
||||
type: 'tool',
|
||||
tool: {
|
||||
name: 'pulse_read',
|
||||
input: '{"action":"exec","target_host":"current_resource","command":"ls /dev | wc -l"}',
|
||||
output: 'ok',
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
{ type: 'content', content: 'I checked the device node count.' },
|
||||
{
|
||||
type: 'tool',
|
||||
tool: {
|
||||
name: 'pulse_get_metrics',
|
||||
input: '{"action":"history","resource":"vm-101"}',
|
||||
output: 'ok',
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render(() => (
|
||||
<MessageItem
|
||||
message={makeMessage({
|
||||
role: 'assistant',
|
||||
streamEvents: events,
|
||||
isStreaming: false,
|
||||
})}
|
||||
{...makeHandlers()}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText('Context used')).toBeInTheDocument();
|
||||
expect(screen.getByText('Inspect devices on current resource')).toBeInTheDocument();
|
||||
expect(screen.getByText('history')).toBeInTheDocument();
|
||||
expect(screen.queryByText('read')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('deduplicates tool summaries in context footer', () => {
|
||||
it('does not duplicate visible context rows with a context footer', () => {
|
||||
const events: StreamDisplayEvent[] = [
|
||||
{
|
||||
type: 'tool',
|
||||
@@ -1648,8 +1614,8 @@ describe('MessageItem', () => {
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getAllByText('Inspect devices on current resource')).toHaveLength(1);
|
||||
expect(screen.getByText('get metrics')).toBeInTheDocument();
|
||||
expect(screen.getAllByTestId('tool-execution-block')).toHaveLength(3);
|
||||
expect(screen.queryByText('Context used')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show context tools section when streaming', () => {
|
||||
@@ -1691,7 +1657,7 @@ describe('MessageItem', () => {
|
||||
expect(screen.queryByText('Context used')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('formats tool names correctly (strips pulse_ prefix and replaces underscores)', () => {
|
||||
it('passes prefixed tool names through to visible tool rows', () => {
|
||||
const events: StreamDisplayEvent[] = [
|
||||
{
|
||||
type: 'tool',
|
||||
@@ -1715,10 +1681,13 @@ describe('MessageItem', () => {
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText('get container status')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('tool-execution-block')).toHaveAttribute(
|
||||
'data-tool-name',
|
||||
'pulse_get_container_status',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles tool names without pulse_ prefix', () => {
|
||||
it('passes tool names without pulse_ prefix through to visible tool rows', () => {
|
||||
const events: StreamDisplayEvent[] = [
|
||||
{
|
||||
type: 'tool',
|
||||
@@ -1737,7 +1706,10 @@ describe('MessageItem', () => {
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText('run command')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('tool-execution-block')).toHaveAttribute(
|
||||
'data-tool-name',
|
||||
'run_command',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2860,7 +2860,7 @@ class SubsystemLookupTest(unittest.TestCase):
|
||||
{
|
||||
"heading": "## Shared Boundaries",
|
||||
"path": "internal/api/access_control_handlers.go",
|
||||
"line": 398,
|
||||
"line": 404,
|
||||
"heading_line": 117,
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user