mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
Show Assistant last turn summary
This commit is contained in:
@@ -1515,8 +1515,9 @@ deriving an older display status from `workflowStatusHistory`.
|
||||
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 usage in composer
|
||||
chrome while the drawer header stays limited to drawer/session commands.
|
||||
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`
|
||||
@@ -1536,20 +1537,23 @@ deriving an older display status from `workflowStatusHistory`.
|
||||
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 the
|
||||
composer chrome instead: OpenCode commit
|
||||
`9ed17da55ab1f7360cc0e01075f763e27fa899e9`
|
||||
`packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx` derives
|
||||
prompt usage from the last assistant message with output tokens in `Prompt`
|
||||
lines 246-264 and renders it in the prompt footer at lines 1652-1667. Pulse
|
||||
adapts that source-backed contract by deriving the latest completed
|
||||
`ChatMessage.tokens` with positive output tokens and rendering only the
|
||||
backend-proven total token count as low-priority Assistant composer chrome.
|
||||
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. Cost and context-limit percentages
|
||||
stay absent until the runtime exposes those values through a governed
|
||||
contract.
|
||||
`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`
|
||||
@@ -2243,6 +2247,18 @@ 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 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
|
||||
|
||||
@@ -25,6 +25,7 @@ import { QuestionCard } from './QuestionCard';
|
||||
import { ThinkingBlock } from './ThinkingBlock';
|
||||
import { getAssistantAnswerText } from './assistantAnswerText';
|
||||
import { stripAssistantOutputArtifacts } from './assistantOutputHygiene';
|
||||
import { formatAssistantTurnDuration } from './assistantTurnSummary';
|
||||
import { formatAssistantWorkflowStatus } from './activeTurnStatus';
|
||||
import { groupStreamEventsForDisplay } from './streamEventGrouping';
|
||||
import {
|
||||
@@ -64,24 +65,6 @@ interface MessageItemProps {
|
||||
onCancelQueued?: () => void;
|
||||
}
|
||||
|
||||
const formatAssistantTurnDuration = (startedAt: Date, completedAt?: Date): string => {
|
||||
if (!completedAt) return '';
|
||||
const durationMs = completedAt.getTime() - startedAt.getTime();
|
||||
if (!Number.isFinite(durationMs) || durationMs < 0) return '';
|
||||
if (durationMs < 1000) return '<1s';
|
||||
|
||||
const totalSeconds = Math.max(1, Math.round(durationMs / 1000));
|
||||
if (totalSeconds < 60) return `${totalSeconds}s`;
|
||||
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
if (minutes < 60) return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return remainingMinutes ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
||||
};
|
||||
|
||||
const markdownClass =
|
||||
'text-sm prose prose-slate prose-sm dark:prose-invert max-w-none prose-p:leading-relaxed prose-p:my-2 prose-pre:bg-slate-900 prose-pre:text-slate-100 prose-pre:rounded-md prose-pre:text-xs prose-pre:border prose-pre:border-slate-800 prose-code:text-blue-700 dark:prose-code:text-blue-300 prose-code:bg-blue-50 dark:prose-code:bg-blue-900 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:font-mono prose-code:text-[0.9em] prose-code:border prose-code:border-blue-100 dark:prose-code:border-blue-800 prose-code:before:content-none prose-code:after:content-none prose-headings:font-semibold prose-hr:border-slate-200 dark:prose-hr:border-slate-700 prose-ul:my-2 prose-ol:my-2 prose-li:my-1';
|
||||
|
||||
|
||||
@@ -5894,42 +5894,45 @@ describe('AIChat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Last-turn usage footer ────────────────────────────────────────────────
|
||||
// ── Last-turn summary footer ───────────────────────────────────────────────
|
||||
|
||||
describe('last-turn usage footer', () => {
|
||||
it('shows completed assistant token usage in the composer chrome', () => {
|
||||
describe('last-turn summary footer', () => {
|
||||
it('shows completed assistant route, duration, and token usage in the composer chrome', () => {
|
||||
mockChat.messages.mockReturnValue([
|
||||
{
|
||||
id: 'asst-1',
|
||||
role: 'assistant' as const,
|
||||
content: 'Done.',
|
||||
timestamp: new Date(),
|
||||
completedAt: new Date(),
|
||||
timestamp: new Date('2026-06-06T12:00:00Z'),
|
||||
completedAt: new Date('2026-06-06T12:00:03Z'),
|
||||
isStreaming: false,
|
||||
model: 'openrouter:qwen/qwen3.7-plus',
|
||||
tokens: { input: 500, output: 200 },
|
||||
},
|
||||
]);
|
||||
|
||||
renderChat();
|
||||
|
||||
const usage = screen.getByLabelText(/Last assistant turn usage/);
|
||||
expect(usage).toHaveTextContent('Last turn: 700 tokens');
|
||||
expect(usage).not.toHaveTextContent('500 in');
|
||||
expect(usage).not.toHaveTextContent('200 out');
|
||||
expect(usage).toHaveAttribute(
|
||||
const summary = screen.getByLabelText(/Last assistant turn summary/);
|
||||
expect(summary).toHaveTextContent(
|
||||
'Last turn: Qwen: Qwen3.7 Plus via OpenRouter · 3s · 700 tokens',
|
||||
);
|
||||
expect(summary).not.toHaveTextContent('500 in');
|
||||
expect(summary).not.toHaveTextContent('200 out');
|
||||
expect(summary).toHaveAttribute(
|
||||
'title',
|
||||
'Last assistant turn usage: 700 total, 500 input, 200 output',
|
||||
'Last assistant turn summary: Model: Qwen: Qwen3.7 Plus via OpenRouter. Duration: 3s. Usage: 700 total, 500 input, 200 output',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the latest completed assistant turn with output tokens', () => {
|
||||
it('uses the latest completed assistant turn and skips the active streaming turn', () => {
|
||||
mockChat.messages.mockReturnValue([
|
||||
{
|
||||
id: 'asst-1',
|
||||
role: 'assistant' as const,
|
||||
content: 'Earlier result.',
|
||||
timestamp: new Date(),
|
||||
completedAt: new Date(),
|
||||
timestamp: new Date('2026-06-06T12:00:00Z'),
|
||||
completedAt: new Date('2026-06-06T12:00:02Z'),
|
||||
isStreaming: false,
|
||||
tokens: { input: 100, output: 50 },
|
||||
},
|
||||
@@ -5945,29 +5948,50 @@ describe('AIChat', () => {
|
||||
|
||||
renderChat();
|
||||
|
||||
const usage = screen.getByLabelText(/Last assistant turn usage/);
|
||||
expect(usage).toHaveTextContent('Last turn: 150 tokens');
|
||||
expect(usage).not.toHaveTextContent('100 in');
|
||||
expect(usage).not.toHaveTextContent('50 out');
|
||||
expect(usage).not.toHaveTextContent('1,700 tokens');
|
||||
const summary = screen.getByLabelText(/Last assistant turn summary/);
|
||||
expect(summary).toHaveTextContent('Last turn: 2s · 150 tokens');
|
||||
expect(summary).not.toHaveTextContent('100 in');
|
||||
expect(summary).not.toHaveTextContent('50 out');
|
||||
expect(summary).not.toHaveTextContent('1,700 tokens');
|
||||
});
|
||||
|
||||
it('does not show usage before an assistant turn has output tokens', () => {
|
||||
it('still summarizes completed turns when token usage is missing', () => {
|
||||
mockChat.messages.mockReturnValue([
|
||||
{
|
||||
id: 'asst-1',
|
||||
role: 'assistant' as const,
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
completedAt: new Date(),
|
||||
content: 'Done.',
|
||||
timestamp: new Date('2026-06-06T12:00:00Z'),
|
||||
completedAt: new Date('2026-06-06T12:00:05Z'),
|
||||
isStreaming: false,
|
||||
model: 'deepseek:deepseek-chat',
|
||||
tokens: { input: 500, output: 0 },
|
||||
},
|
||||
]);
|
||||
|
||||
renderChat();
|
||||
|
||||
expect(screen.queryByLabelText(/Last assistant turn usage/)).not.toBeInTheDocument();
|
||||
const summary = screen.getByLabelText(/Last assistant turn summary/);
|
||||
expect(summary).toHaveTextContent('Last turn: DeepSeek: DeepSeek Chat · 5s');
|
||||
expect(summary).not.toHaveTextContent('tokens');
|
||||
});
|
||||
|
||||
it('does not show the summary for an active streaming turn', () => {
|
||||
mockChat.messages.mockReturnValue([
|
||||
{
|
||||
id: 'asst-1',
|
||||
role: 'assistant' as const,
|
||||
content: 'Streaming.',
|
||||
timestamp: new Date('2026-06-06T12:00:00Z'),
|
||||
isStreaming: true,
|
||||
model: 'openrouter:qwen/qwen3.7-plus',
|
||||
tokens: { input: 500, output: 200 },
|
||||
},
|
||||
]);
|
||||
|
||||
renderChat();
|
||||
|
||||
expect(screen.queryByLabelText(/Last assistant turn summary/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
formatAssistantTurnDuration,
|
||||
getAssistantTurnSummary,
|
||||
} from '../assistantTurnSummary';
|
||||
import type { ChatMessage } from '../types';
|
||||
|
||||
const makeAssistantMessage = (overrides: Partial<ChatMessage> = {}): ChatMessage => ({
|
||||
id: 'asst-1',
|
||||
role: 'assistant',
|
||||
content: 'Done.',
|
||||
timestamp: new Date('2026-06-06T12:00:00Z'),
|
||||
completedAt: new Date('2026-06-06T12:00:03Z'),
|
||||
isStreaming: false,
|
||||
model: 'openrouter:qwen/qwen3.7-plus',
|
||||
tokens: { input: 500, output: 200 },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('assistantTurnSummary', () => {
|
||||
it('formats assistant turn duration for short and longer completed turns', () => {
|
||||
expect(
|
||||
formatAssistantTurnDuration(
|
||||
new Date('2026-06-06T12:00:00Z'),
|
||||
new Date('2026-06-06T12:00:00.400Z'),
|
||||
),
|
||||
).toBe('<1s');
|
||||
expect(
|
||||
formatAssistantTurnDuration(
|
||||
new Date('2026-06-06T12:00:00Z'),
|
||||
new Date('2026-06-06T12:01:05Z'),
|
||||
),
|
||||
).toBe('1m 5s');
|
||||
expect(
|
||||
formatAssistantTurnDuration(
|
||||
new Date('2026-06-06T12:00:00Z'),
|
||||
new Date('2026-06-06T11:59:59Z'),
|
||||
),
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('summarizes completed assistant route, duration, and billed usage', () => {
|
||||
expect(
|
||||
getAssistantTurnSummary(makeAssistantMessage(), {
|
||||
getModelRouteLabel: () => 'Qwen via OpenRouter',
|
||||
}),
|
||||
).toEqual({
|
||||
label: 'Last turn: Qwen via OpenRouter · 3s · 700 tokens',
|
||||
title:
|
||||
'Last assistant turn summary: Model: Qwen via OpenRouter. Duration: 3s. Usage: 700 total, 500 input, 200 output',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps route and duration visible when provider usage is missing', () => {
|
||||
expect(
|
||||
getAssistantTurnSummary(
|
||||
makeAssistantMessage({
|
||||
tokens: { input: 500, output: 0 },
|
||||
}),
|
||||
{
|
||||
getModelRouteLabel: () => 'DeepSeek direct',
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
label: 'Last turn: DeepSeek direct · 3s',
|
||||
title: 'Last assistant turn summary: Model: DeepSeek direct. Duration: 3s',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not summarize active streaming or non-assistant turns', () => {
|
||||
expect(getAssistantTurnSummary(makeAssistantMessage({ isStreaming: true }))).toBeNull();
|
||||
expect(
|
||||
getAssistantTurnSummary(
|
||||
makeAssistantMessage({
|
||||
role: 'user',
|
||||
content: 'Question',
|
||||
}),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { AI_CHAT_LAST_TURN_SUMMARY_LABEL } from '@/utils/aiChatPresentation';
|
||||
import type { ChatMessage } from './types';
|
||||
|
||||
export interface AssistantTurnSummary {
|
||||
label: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const tokenNumberFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 });
|
||||
|
||||
const normalizeAssistantTokenCount = (value: number | undefined): number => {
|
||||
if (!Number.isFinite(value) || !value || value < 0) return 0;
|
||||
return Math.floor(value);
|
||||
};
|
||||
|
||||
const formatAssistantTokenCount = (count: number): string => tokenNumberFormat.format(count);
|
||||
|
||||
export const formatAssistantTurnDuration = (startedAt: Date, completedAt?: Date): string => {
|
||||
if (!completedAt) return '';
|
||||
const durationMs = completedAt.getTime() - startedAt.getTime();
|
||||
if (!Number.isFinite(durationMs) || durationMs < 0) return '';
|
||||
if (durationMs < 1000) return '<1s';
|
||||
|
||||
const totalSeconds = Math.max(1, Math.round(durationMs / 1000));
|
||||
if (totalSeconds < 60) return `${totalSeconds}s`;
|
||||
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
if (minutes < 60) return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return remainingMinutes ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
||||
};
|
||||
|
||||
const assistantTokenSummary = (message: ChatMessage) => {
|
||||
const input = normalizeAssistantTokenCount(message.tokens?.input);
|
||||
const output = normalizeAssistantTokenCount(message.tokens?.output);
|
||||
if (output <= 0) return null;
|
||||
|
||||
const total = input + output;
|
||||
return {
|
||||
label: `${formatAssistantTokenCount(total)} ${total === 1 ? 'token' : 'tokens'}`,
|
||||
detail: `${formatAssistantTokenCount(total)} total, ${formatAssistantTokenCount(
|
||||
input,
|
||||
)} input, ${formatAssistantTokenCount(output)} output`,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAssistantTurnSummary = (
|
||||
message: ChatMessage,
|
||||
options: { getModelRouteLabel?: (modelId: string) => string } = {},
|
||||
): AssistantTurnSummary | null => {
|
||||
if (message.role !== 'assistant' || message.isStreaming) return null;
|
||||
|
||||
const model = message.model?.trim();
|
||||
const modelLabel = model ? options.getModelRouteLabel?.(model) || model : '';
|
||||
const durationLabel = formatAssistantTurnDuration(message.timestamp, message.completedAt);
|
||||
const tokenSummary = assistantTokenSummary(message);
|
||||
|
||||
const labelParts = [modelLabel, durationLabel, tokenSummary?.label].filter(
|
||||
(part): part is string => Boolean(part?.trim()),
|
||||
);
|
||||
if (labelParts.length === 0) return null;
|
||||
|
||||
const titleParts = [
|
||||
modelLabel ? `Model: ${modelLabel}` : undefined,
|
||||
durationLabel ? `Duration: ${durationLabel}` : undefined,
|
||||
tokenSummary ? `Usage: ${tokenSummary.detail}` : undefined,
|
||||
].filter((part): part is string => Boolean(part));
|
||||
|
||||
return {
|
||||
label: `Last turn: ${labelParts.join(' · ')}`,
|
||||
title: `${AI_CHAT_LAST_TURN_SUMMARY_LABEL}: ${titleParts.join('. ')}`,
|
||||
};
|
||||
};
|
||||
@@ -75,7 +75,6 @@ import {
|
||||
AI_CHAT_FORK_SESSION_LOADING_MESSAGE,
|
||||
AI_CHAT_FORK_SESSION_SUCCESS_MESSAGE,
|
||||
AI_CHAT_INPUT_PLACEHOLDER,
|
||||
AI_CHAT_LAST_TURN_USAGE_LABEL,
|
||||
AI_CHAT_NEW_SESSION_BUTTON_TITLE,
|
||||
AI_CHAT_NEW_SESSION_MENU_ARIA_LABEL,
|
||||
AI_CHAT_NEW_SESSION_MENU_LABEL,
|
||||
@@ -134,6 +133,7 @@ import {
|
||||
import { normalizeChatMentionKeyPart } from '@/utils/chatIdentifiers';
|
||||
import { getGlobalWebSocketStore } from '@/stores/websocket-global';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { getAssistantTurnSummary } from './assistantTurnSummary';
|
||||
import {
|
||||
getPreferredResourceDisplayName,
|
||||
getPreferredResourceHostname,
|
||||
@@ -255,11 +255,6 @@ interface ComposerDraftStash {
|
||||
restoredPromptDraft: RestoredPromptDraft | null;
|
||||
}
|
||||
|
||||
interface AssistantUsageSummary {
|
||||
label: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface TranscriptCopyFallback {
|
||||
generatedAt: Date;
|
||||
transcript: string;
|
||||
@@ -297,35 +292,6 @@ const compactText = (items: Array<string | undefined>): string[] =>
|
||||
const pluralizeCount = (count: number, singular: string, plural: string) =>
|
||||
`${count} ${count === 1 ? singular : plural}`;
|
||||
|
||||
const tokenNumberFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 });
|
||||
|
||||
const normalizeAssistantTokenCount = (value: number | undefined): number => {
|
||||
if (!Number.isFinite(value) || !value || value < 0) return 0;
|
||||
return Math.floor(value);
|
||||
};
|
||||
|
||||
const formatAssistantTokenCount = (count: number): string => tokenNumberFormat.format(count);
|
||||
|
||||
const getAssistantUsageSummary = (message: ChatMessage): AssistantUsageSummary | null => {
|
||||
if (message.role !== 'assistant' || message.isStreaming) return null;
|
||||
const input = normalizeAssistantTokenCount(message.tokens?.input);
|
||||
const output = normalizeAssistantTokenCount(message.tokens?.output);
|
||||
if (output <= 0) return null;
|
||||
|
||||
const total = input + output;
|
||||
const totalLabel = `${formatAssistantTokenCount(total)} ${total === 1 ? 'token' : 'tokens'}`;
|
||||
const titleDetail = [
|
||||
`${formatAssistantTokenCount(total)} total`,
|
||||
`${formatAssistantTokenCount(input)} input`,
|
||||
`${formatAssistantTokenCount(output)} output`,
|
||||
].join(', ');
|
||||
|
||||
return {
|
||||
label: `Last turn: ${totalLabel}`,
|
||||
title: `${AI_CHAT_LAST_TURN_USAGE_LABEL}: ${titleDetail}`,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizePromptHistoryEntry = (value: unknown): PromptHistoryEntry | null => {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
const record = value as Record<string, unknown>;
|
||||
@@ -2475,11 +2441,13 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
]).join('. '),
|
||||
};
|
||||
});
|
||||
const lastAssistantUsage = createMemo(() => {
|
||||
const lastAssistantTurnSummary = createMemo(() => {
|
||||
const messages = chat.messages();
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const usage = getAssistantUsageSummary(messages[index]);
|
||||
if (usage) return usage;
|
||||
const summary = getAssistantTurnSummary(messages[index], {
|
||||
getModelRouteLabel: formatChatMessageModelRoute,
|
||||
});
|
||||
if (summary) return summary;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -4917,14 +4885,14 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={lastAssistantUsage()}>
|
||||
{(usage) => (
|
||||
<Show when={lastAssistantTurnSummary()}>
|
||||
{(summary) => (
|
||||
<div
|
||||
class="flex h-5 min-w-0 items-center justify-end self-end text-[10px] font-medium text-muted sm:h-7 sm:max-w-[12rem] sm:shrink-0 sm:self-auto"
|
||||
aria-label={usage().title}
|
||||
title={usage().title}
|
||||
class="flex h-5 min-w-0 items-center justify-end self-end text-[10px] font-medium text-muted sm:h-7 sm:max-w-[14rem] sm:shrink-0 sm:self-auto"
|
||||
aria-label={summary().title}
|
||||
title={summary().title}
|
||||
>
|
||||
<span class="truncate">{usage().label}</span>
|
||||
<span class="truncate">{summary().label}</span>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
AI_CHAT_DRAWER_SUBTITLE,
|
||||
AI_CHAT_DRAWER_TITLE,
|
||||
AI_CHAT_LAUNCHER_ARIA_LABEL,
|
||||
AI_CHAT_LAST_TURN_USAGE_LABEL,
|
||||
AI_CHAT_LAST_TURN_SUMMARY_LABEL,
|
||||
AI_CHAT_MODEL_SELECTOR_EMPTY_STATE,
|
||||
AI_CHAT_NEW_SESSION_BUTTON_TITLE,
|
||||
AI_CHAT_NEW_SESSION_MENU_ARIA_LABEL,
|
||||
@@ -116,7 +116,7 @@ describe('aiChatPresentation', () => {
|
||||
expect(AI_CHAT_QUESTION_CARD_TITLE).toBe('Pulse Assistant needs your input');
|
||||
expect(AI_CHAT_ASSISTANT_MESSAGE_LABEL).toBe('Pulse Assistant');
|
||||
expect(AI_CHAT_CONTEXT_USED_LABEL).toBe('Context used');
|
||||
expect(AI_CHAT_LAST_TURN_USAGE_LABEL).toBe('Last assistant turn usage');
|
||||
expect(AI_CHAT_LAST_TURN_SUMMARY_LABEL).toBe('Last assistant turn summary');
|
||||
expect(AI_CHAT_PROVIDER_READINESS_SETTINGS_HREF).toBe('/settings/system-ai');
|
||||
expect(AI_CHAT_PROVIDER_READINESS_SETTINGS_LABEL).toBe('Open settings');
|
||||
expect(AI_CHAT_PROVIDER_READINESS_RETRY_LABEL).toBe('Retry');
|
||||
|
||||
@@ -67,7 +67,7 @@ export const AI_CHAT_QUESTION_CARD_TITLE = 'Pulse Assistant needs your input';
|
||||
export const AI_CHAT_QUESTION_CARD_PLACEHOLDER = 'Type your answer...';
|
||||
export const AI_CHAT_ASSISTANT_MESSAGE_LABEL = 'Pulse Assistant';
|
||||
export const AI_CHAT_CONTEXT_USED_LABEL = 'Context used';
|
||||
export const AI_CHAT_LAST_TURN_USAGE_LABEL = 'Last assistant turn usage';
|
||||
export const AI_CHAT_LAST_TURN_SUMMARY_LABEL = 'Last assistant turn summary';
|
||||
export const AI_CHAT_PROVIDER_READINESS_SETTINGS_HREF = '/settings/system-ai';
|
||||
export const AI_CHAT_PROVIDER_READINESS_SETTINGS_LABEL = 'Open settings';
|
||||
export const AI_CHAT_PROVIDER_READINESS_RETRY_LABEL = 'Retry';
|
||||
|
||||
Reference in New Issue
Block a user