Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions frontend/src/components/MessageBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import type { Message } from '../pages/ChatView';
Expand All @@ -8,8 +7,6 @@ interface Props {
}

export function MessageBubble({ message }: Props) {
const [expanded, setExpanded] = useState(false);

if (message.role === 'user') {
return (
<div className="msg-bubble msg-bubble--user">
Expand All @@ -25,35 +22,6 @@ export function MessageBubble({ message }: Props) {
);
}

if (message.role === 'tool') {
return (
<div className="msg-bubble msg-bubble--tool">
<button className="msg-tool-header" onClick={() => setExpanded((e) => !e)}>
<span className="msg-tool-name">{message.toolName}</span>
<span className="msg-tool-chevron">{expanded ? '▾' : '▸'}</span>
</button>
{expanded && (
<div className="msg-tool-detail">
<div className="msg-tool-section">
<span className="msg-tool-label">Input</span>
<pre className="msg-tool-pre">{message.toolInput}</pre>
</div>
{message.toolResult !== undefined && (
<div className="msg-tool-section">
<span className="msg-tool-label">Result</span>
<pre className="msg-tool-pre">{message.toolResult}</pre>
</div>
)}
{message.toolResult === undefined && <div className="msg-tool-running">Running...</div>}
</div>
)}
{!expanded && message.toolResult === undefined && (
<div className="msg-tool-running-inline">Running...</div>
)}
</div>
);
}

return (
<div className="msg-bubble msg-bubble--assistant">
<div className="msg-bubble-markdown">
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/components/ToolGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState, useEffect } from 'react';
import { ToolPill } from './ToolPill';
import type { Message } from '../pages/ChatView';

interface Props {
tools: Message[];
}

export function ToolGroup({ tools }: Props) {
const allDone = tools.every((t) => t.toolResult !== undefined);
const [expanded, setExpanded] = useState(!allDone);

useEffect(() => {
if (allDone) setExpanded(false);
}, [allDone]);

const doneCount = tools.filter((t) => t.toolResult !== undefined).length;

return (
<div className="tool-group">
<button className="tool-group-header" onClick={() => setExpanded((e) => !e)}>
<div className="tool-group-dots">
{tools.map((t, i) => (
<span
key={i}
className={`tool-pill-dot ${t.toolResult !== undefined ? 'tool-pill-dot--done' : 'tool-pill-dot--pending'}`}
/>
))}
</div>
<span className="tool-group-label">
{allDone ? `${tools.length} tool calls` : `${doneCount}/${tools.length} running...`}
</span>
<span className="tool-group-chevron">{expanded ? '▾' : '▸'}</span>
</button>
{expanded && (
<div className="tool-group-list">
{tools.map((t, i) => (
<ToolPill key={t.toolId || i} message={t} />
))}
</div>
)}
</div>
);
}
41 changes: 41 additions & 0 deletions frontend/src/components/ToolPill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useState } from 'react';
import type { Message } from '../pages/ChatView';

interface Props {
message: Message;
}

export function ToolPill({ message }: Props) {
const [expanded, setExpanded] = useState(false);
const done = message.toolResult !== undefined;
const input = message.toolInput || '';
const truncatedInput = input.length > 60 ? input.slice(0, 60) + '...' : input;

return (
<div className={`tool-pill ${done ? 'tool-pill--done' : 'tool-pill--running'}`}>
<button className="tool-pill-header" onClick={() => setExpanded((e) => !e)}>
<span
className={`tool-pill-dot ${done ? 'tool-pill-dot--done' : 'tool-pill-dot--pending'}`}
/>
<span className="tool-pill-name">{message.toolName}</span>
<span className="tool-pill-input">{truncatedInput}</span>
{!done && <span className="tool-pill-status">Running...</span>}
{expanded && <span className="tool-pill-chevron">▾</span>}
</button>
{expanded && (
<div className="tool-pill-detail">
<div className="tool-pill-section">
<span className="tool-pill-label">Input</span>
<pre className="tool-pill-pre">{input}</pre>
</div>
{done && (
<div className="tool-pill-section">
<span className="tool-pill-label">Result</span>
<pre className="tool-pill-pre">{message.toolResult}</pre>
</div>
)}
</div>
)}
</div>
);
}
49 changes: 45 additions & 4 deletions frontend/src/pages/ChatView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
import { MessageBubble } from '../components/MessageBubble';
import { ToolPill } from '../components/ToolPill';
import { ToolGroup } from '../components/ToolGroup';
import { PermissionBanner } from '../components/PermissionBanner';
import { ChatInput, type ImageAttachment } from '../components/ChatInput';

Expand All @@ -15,6 +17,36 @@
streaming?: boolean;
}

type GroupedItem = { type: 'message'; message: Message } | { type: 'tool-group'; tools: Message[] };

function groupMessages(messages: Message[]): GroupedItem[] {
const result: GroupedItem[] = [];
let toolBuffer: Message[] = [];

function flushTools() {
if (toolBuffer.length === 0) return;
if (toolBuffer.length >= 3) {
result.push({ type: 'tool-group', tools: toolBuffer });
} else {
for (const t of toolBuffer) {
result.push({ type: 'message', message: t });
}
}
toolBuffer = [];
}

for (const msg of messages) {
if (msg.role === 'tool') {
toolBuffer.push(msg);
} else {
flushTools();
result.push({ type: 'message', message: msg });
}
}
flushTools();
return result;
}

interface PermissionRequest {
permId: string;
toolName: string;
Expand Down Expand Up @@ -71,7 +103,7 @@
fetch(`/api/sessions/${sessionId}/messages`)
.then((r) => (r.ok ? r.json() : []))
.then(
(msgs: Array<{ role: string; text?: string; toolCalls?: any[]; toolResults?: any[] }>) => {

Check warning on line 106 in frontend/src/pages/ChatView.tsx

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type

Check warning on line 106 in frontend/src/pages/ChatView.tsx

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
const loaded: Message[] = [];
for (const m of msgs) {
if (m.text) {
Expand Down Expand Up @@ -253,7 +285,7 @@
setRunning(true);
streamBuf.current = '';

const payload: Record<string, any> = { type: 'send', prompt: text, model, mode };

Check warning on line 288 in frontend/src/pages/ChatView.tsx

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
if (currentSessionId) payload.resume = currentSessionId;
if (images?.length) {
payload.images = images.map((img) => ({ data: img.data, mediaType: img.mediaType }));
Expand Down Expand Up @@ -291,6 +323,8 @@
}
}

const grouped = useMemo(() => groupMessages(messages), [messages]);

return (
<div className="chat-page">
<header className="chat-header">
Expand Down Expand Up @@ -332,9 +366,16 @@

<div className="chat-messages" ref={scrollRef}>
{messages.length === 0 && !running && <p className="chat-empty">Send a message to start</p>}
{messages.map((msg, i) => (
<MessageBubble key={i} message={msg} />
))}
{grouped.map((item, i) => {
if (item.type === 'tool-group') {
return <ToolGroup key={`tg-${i}`} tools={item.tools} />;
}
const msg = item.message;
if (msg.role === 'tool') {
return <ToolPill key={msg.toolId || `t-${i}`} message={msg} />;
}
return <MessageBubble key={`m-${i}`} message={msg} />;
})}
</div>

{permission && (
Expand Down
Loading
Loading