mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-28 05:43:57 +02:00
feat: label pending and final agent messages
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { type KeyboardEvent, useEffect, useRef, useState } from 'react';
|
|||||||
import { AlertCircle, ArrowUp, Bot, Loader2, User } from 'lucide-react';
|
import { AlertCircle, ArrowUp, Bot, Loader2, User } from 'lucide-react';
|
||||||
|
|
||||||
import { MarkdownContent } from '@renderer/components/MarkdownContent';
|
import { MarkdownContent } from '@renderer/components/MarkdownContent';
|
||||||
|
import { getAssistantMessagePhase } from '@renderer/lib/messagePhase';
|
||||||
|
|
||||||
import type { PatternDefinition } from '@shared/domain/pattern';
|
import type { PatternDefinition } from '@shared/domain/pattern';
|
||||||
import type { ProjectRecord } from '@shared/domain/project';
|
import type { ProjectRecord } from '@shared/domain/project';
|
||||||
@@ -96,8 +97,22 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="mx-auto max-w-3xl px-6 py-4">
|
<div className="mx-auto max-w-3xl px-6 py-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{session.messages.map((message) => {
|
{session.messages.map((message, index) => {
|
||||||
const isUser = message.role === 'user';
|
const isUser = message.role === 'user';
|
||||||
|
const phase = getAssistantMessagePhase(session, message, index);
|
||||||
|
const assistantContainerClass =
|
||||||
|
phase === 'thinking'
|
||||||
|
? 'border-sky-500/20 bg-sky-500/5'
|
||||||
|
: phase === 'final'
|
||||||
|
? 'border-emerald-500/20 bg-emerald-500/5'
|
||||||
|
: 'border-zinc-800 bg-zinc-900/40';
|
||||||
|
const assistantBadgeClass =
|
||||||
|
phase === 'thinking'
|
||||||
|
? 'border-sky-400/20 bg-sky-400/10 text-sky-300'
|
||||||
|
: 'border-emerald-400/20 bg-emerald-400/10 text-emerald-300';
|
||||||
|
const phaseLabel =
|
||||||
|
phase === 'thinking' ? 'Thinking' : phase === 'final' ? 'Final' : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group py-3" key={message.id}>
|
<div className="group py-3" key={message.id}>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
@@ -111,10 +126,23 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
|
|||||||
{isUser ? <User className="size-3.5" /> : <Bot className="size-3.5" />}
|
{isUser ? <User className="size-3.5" /> : <Bot className="size-3.5" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="mb-1 text-[12px] font-medium text-zinc-400">
|
<div className="mb-1 flex items-center gap-2 text-[12px] font-medium text-zinc-400">
|
||||||
{message.authorName}
|
<span>{message.authorName}</span>
|
||||||
|
{!isUser && phaseLabel && (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.08em] ${assistantBadgeClass}`}
|
||||||
|
>
|
||||||
|
{phaseLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[14px] leading-relaxed text-zinc-200">
|
<div
|
||||||
|
className={
|
||||||
|
isUser
|
||||||
|
? 'text-[14px] leading-relaxed text-zinc-200'
|
||||||
|
: `rounded-xl border px-4 py-3 text-[14px] leading-relaxed text-zinc-200 ${assistantContainerClass}`
|
||||||
|
}
|
||||||
|
>
|
||||||
<MarkdownContent content={message.content} />
|
<MarkdownContent content={message.content} />
|
||||||
{message.pending && message.content && (
|
{message.pending && message.content && (
|
||||||
<span className="mt-1 inline-block h-4 w-[2px] animate-pulse rounded-sm bg-zinc-400" />
|
<span className="mt-1 inline-block h-4 w-[2px] animate-pulse rounded-sm bg-zinc-400" />
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { ChatMessageRecord, SessionRecord } from '@shared/domain/session';
|
||||||
|
|
||||||
|
export type AssistantMessagePhase = 'default' | 'thinking' | 'final';
|
||||||
|
|
||||||
|
export function getAssistantMessagePhase(
|
||||||
|
session: SessionRecord,
|
||||||
|
message: ChatMessageRecord,
|
||||||
|
index: number,
|
||||||
|
): AssistantMessagePhase {
|
||||||
|
if (message.role !== 'assistant') {
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.pending) {
|
||||||
|
return 'thinking';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.status === 'running') {
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCompletedAssistantIndex = findLastCompletedAssistantIndex(session.messages);
|
||||||
|
return index === lastCompletedAssistantIndex ? 'final' : 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLastCompletedAssistantIndex(messages: ChatMessageRecord[]): number {
|
||||||
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||||
|
const message = messages[index];
|
||||||
|
if (message.role === 'assistant' && !message.pending) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
|
import { getAssistantMessagePhase } from '@renderer/lib/messagePhase';
|
||||||
|
import type { SessionRecord } from '@shared/domain/session';
|
||||||
|
|
||||||
|
function createSession(
|
||||||
|
messages: SessionRecord['messages'],
|
||||||
|
status: SessionRecord['status'] = 'idle',
|
||||||
|
): SessionRecord {
|
||||||
|
return {
|
||||||
|
id: 'session-1',
|
||||||
|
projectId: 'project-1',
|
||||||
|
patternId: 'pattern-1',
|
||||||
|
title: 'Test session',
|
||||||
|
createdAt: '2026-03-23T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-03-23T00:00:00.000Z',
|
||||||
|
status,
|
||||||
|
messages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('assistant message phase', () => {
|
||||||
|
test('marks pending assistant messages as thinking', () => {
|
||||||
|
const session = createSession([
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
authorName: 'Triage',
|
||||||
|
content: 'Draft',
|
||||||
|
createdAt: '2026-03-23T00:00:00.000Z',
|
||||||
|
pending: true,
|
||||||
|
},
|
||||||
|
], 'running');
|
||||||
|
|
||||||
|
expect(getAssistantMessagePhase(session, session.messages[0], 0)).toBe('thinking');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('marks the last completed assistant message as final when the session is idle', () => {
|
||||||
|
const session = createSession([
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
authorName: 'Triage',
|
||||||
|
content: 'Earlier',
|
||||||
|
createdAt: '2026-03-23T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-2',
|
||||||
|
role: 'assistant',
|
||||||
|
authorName: 'UX Specialist',
|
||||||
|
content: 'Final answer',
|
||||||
|
createdAt: '2026-03-23T00:00:01.000Z',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(getAssistantMessagePhase(session, session.messages[0], 0)).toBe('default');
|
||||||
|
expect(getAssistantMessagePhase(session, session.messages[1], 1)).toBe('final');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not mark completed assistant messages as final while the session is still running', () => {
|
||||||
|
const session = createSession([
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'assistant',
|
||||||
|
authorName: 'Triage',
|
||||||
|
content: 'In progress',
|
||||||
|
createdAt: '2026-03-23T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
], 'running');
|
||||||
|
|
||||||
|
expect(getAssistantMessagePhase(session, session.messages[0], 0)).toBe('default');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignores non-assistant messages', () => {
|
||||||
|
const session = createSession([
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'user',
|
||||||
|
authorName: 'You',
|
||||||
|
content: 'Hello',
|
||||||
|
createdAt: '2026-03-23T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(getAssistantMessagePhase(session, session.messages[0], 0)).toBe('default');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user