Skip to content

Commit 1b2dcce

Browse files
authored
refactor(chat): execution graph optimize (ValueCell-ai#873)
Co-authored-by: Haze <hazeone@users.noreply.github.com>
1 parent 2f03aa1 commit 1b2dcce

24 files changed

Lines changed: 1445 additions & 537 deletions

src/i18n/locales/en/chat.json

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
"noLogs": "(No logs available yet)",
1414
"toolbar": {
1515
"refresh": "Refresh chat",
16-
"showThinking": "Show thinking",
17-
"hideThinking": "Hide thinking",
1816
"currentAgent": "Talking to {{agent}}"
1917
},
2018
"taskPanel": {
@@ -34,19 +32,12 @@
3432
}
3533
},
3634
"executionGraph": {
37-
"eyebrow": "Conversation Run",
3835
"title": "Execution Graph",
39-
"status": {
40-
"active": "Active",
41-
"latest": "Latest",
42-
"previous": "Previous"
43-
},
4436
"branchLabel": "branch",
45-
"userTrigger": "User Trigger",
46-
"userTriggerHint": "Triggered by the user message above",
37+
"thinkingLabel": "Thinking",
4738
"agentRun": "{{agent}} execution",
48-
"agentReply": "Assistant Reply",
49-
"agentReplyHint": "Resolved in the assistant reply below"
39+
"collapsedSummary": "{{toolCount}} tool calls · {{processCount}} process messages",
40+
"collapseAction": "Collapse execution graph"
5041
},
5142
"composer": {
5243
"attachFiles": "Attach files",

src/i18n/locales/ja/chat.json

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
"noLogs": "(ログはまだありません)",
1414
"toolbar": {
1515
"refresh": "チャットを更新",
16-
"showThinking": "思考を表示",
17-
"hideThinking": "思考を非表示",
1816
"currentAgent": "現在の会話相手: {{agent}}"
1917
},
2018
"taskPanel": {
@@ -34,19 +32,12 @@
3432
}
3533
},
3634
"executionGraph": {
37-
"eyebrow": "会話実行",
3835
"title": "実行グラフ",
39-
"status": {
40-
"active": "進行中",
41-
"latest": "直近",
42-
"previous": "履歴"
43-
},
4436
"branchLabel": "branch",
45-
"userTrigger": "ユーザー入力",
46-
"userTriggerHint": "上のユーザーメッセージがトリガーです",
37+
"thinkingLabel": "考え中",
4738
"agentRun": "{{agent}} の実行",
48-
"agentReply": "アシスタント返信",
49-
"agentReplyHint": "結果は下のアシスタント返信に反映されます"
39+
"collapsedSummary": "ツール呼び出し {{toolCount}} 件 · プロセスメッセージ {{processCount}} 件",
40+
"collapseAction": "実行グラフを折りたたむ"
5041
},
5142
"composer": {
5243
"attachFiles": "ファイルを添付",

src/i18n/locales/ru/chat.json

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
"noLogs": "(Журналы ещё недоступны)",
1414
"toolbar": {
1515
"refresh": "Обновить чат",
16-
"showThinking": "Показать размышления",
17-
"hideThinking": "Скрыть размышления",
1816
"currentAgent": "Общение с {{agent}}"
1917
},
2018
"taskPanel": {
@@ -34,19 +32,12 @@
3432
}
3533
},
3634
"executionGraph": {
37-
"eyebrow": "Выполнение в чате",
3835
"title": "Граф выполнения",
39-
"status": {
40-
"active": "Активно",
41-
"latest": "Последнее",
42-
"previous": "Предыдущее"
43-
},
4436
"branchLabel": "ветвь",
45-
"userTrigger": "Триггер пользователя",
46-
"userTriggerHint": "Запущен пользовательским сообщением выше",
37+
"thinkingLabel": "Думаю",
4738
"agentRun": "Выполнение {{agent}}",
48-
"agentReply": "Ответ ассистента",
49-
"agentReplyHint": "Разрешено в ответе ассистента ниже"
39+
"collapsedSummary": "Вызовов инструментов: {{toolCount}} · Промежуточных сообщений: {{processCount}}",
40+
"collapseAction": "Свернуть граф выполнения"
5041
},
5142
"composer": {
5243
"attachFiles": "Прикрепить файлы",

src/i18n/locales/zh/chat.json

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
"noLogs": "(暂无日志)",
1414
"toolbar": {
1515
"refresh": "刷新聊天",
16-
"showThinking": "显示思考过程",
17-
"hideThinking": "隐藏思考过程",
1816
"currentAgent": "当前对话对象:{{agent}}"
1917
},
2018
"taskPanel": {
@@ -34,19 +32,12 @@
3432
}
3533
},
3634
"executionGraph": {
37-
"eyebrow": "对话执行",
3835
"title": "执行关系图",
39-
"status": {
40-
"active": "执行中",
41-
"latest": "最近一次",
42-
"previous": "历史"
43-
},
4436
"branchLabel": "分支",
45-
"userTrigger": "用户触发",
46-
"userTriggerHint": "对应上方这条用户消息",
37+
"thinkingLabel": "思考中",
4738
"agentRun": "{{agent}} 执行",
48-
"agentReply": "助手回复",
49-
"agentReplyHint": "结果体现在下方这条助手回复里"
39+
"collapsedSummary": "{{toolCount}} 个工具调用,{{processCount}} 条过程消息",
40+
"collapseAction": "收起执行关系图"
5041
},
5142
"composer": {
5243
"attachFiles": "添加文件",

src/pages/Chat/ChatInput.tsx

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
114114
useEffect(() => {
115115
if (textareaRef.current) {
116116
textareaRef.current.style.height = 'auto';
117-
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`;
117+
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 240)}px`;
118118
}
119119
}, [input]);
120120

@@ -407,33 +407,54 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
407407
</div>
408408
)}
409409

410-
{/* Input Row */}
411-
<div className={`relative bg-white dark:bg-card rounded-[28px] shadow-sm border p-1.5 transition-all ${dragOver ? 'border-primary ring-1 ring-primary' : 'border-black/10 dark:border-white/10'}`}>
410+
{/* Input Container */}
411+
<div className={`relative bg-white dark:bg-card rounded-2xl shadow-sm border px-3 pt-2.5 pb-1.5 transition-all ${dragOver ? 'border-primary ring-1 ring-primary' : 'border-black/10 dark:border-white/10'}`}>
412412
{selectedTarget && (
413-
<div className="px-2.5 pt-2 pb-1">
413+
<div className="pb-1.5">
414414
<button
415415
type="button"
416416
onClick={() => setTargetAgentId(null)}
417-
className="inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-[13px] font-medium text-foreground transition-colors hover:bg-primary/10"
417+
className="inline-flex items-center gap-1.5 rounded-lg border border-primary/20 bg-primary/5 px-2.5 py-1 text-[13px] font-medium text-foreground transition-colors hover:bg-primary/10"
418418
title={t('composer.clearTarget')}
419419
>
420420
<span>{t('composer.targetChip', { agent: selectedTarget.name })}</span>
421-
<X className="h-3.5 w-3.5 text-muted-foreground" />
421+
<X className="h-3 w-3 text-muted-foreground" />
422422
</button>
423423
</div>
424424
)}
425425

426-
<div className="flex items-end gap-1.5">
426+
{/* Text Row — flush-left */}
427+
<Textarea
428+
ref={textareaRef}
429+
value={input}
430+
onChange={(e) => setInput(e.target.value)}
431+
onKeyDown={handleKeyDown}
432+
onCompositionStart={() => {
433+
isComposingRef.current = true;
434+
}}
435+
onCompositionEnd={() => {
436+
isComposingRef.current = false;
437+
}}
438+
onPaste={handlePaste}
439+
placeholder={disabled ? t('composer.gatewayDisconnectedPlaceholder') : ''}
440+
disabled={disabled}
441+
data-testid="chat-composer-input"
442+
className="min-h-[48px] max-h-[240px] resize-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none bg-transparent p-0 text-[15px] placeholder:text-muted-foreground/60 leading-relaxed"
443+
rows={1}
444+
/>
445+
446+
{/* Action Row — icons on their own line */}
447+
<div className="mt-1.5 flex items-center gap-1">
427448
{/* Attach Button */}
428449
<Button
429450
variant="ghost"
430451
size="icon"
431-
className="shrink-0 h-10 w-10 rounded-full text-muted-foreground hover:bg-black/5 dark:hover:bg-white/10 hover:text-foreground transition-colors"
452+
className="shrink-0 h-8 w-8 rounded-lg text-muted-foreground hover:bg-black/5 dark:hover:bg-white/10 hover:text-foreground transition-colors"
432453
onClick={pickFiles}
433454
disabled={disabled || sending}
434455
title={t('composer.attachFiles')}
435456
>
436-
<Paperclip className="h-4 w-4" />
457+
<Paperclip className="h-3.5 w-3.5" />
437458
</Button>
438459

439460
{showAgentPicker && (
@@ -442,14 +463,14 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
442463
variant="ghost"
443464
size="icon"
444465
className={cn(
445-
'h-10 w-10 rounded-full text-muted-foreground hover:bg-black/5 dark:hover:bg-white/10 hover:text-foreground transition-colors',
466+
'h-8 w-8 rounded-lg text-muted-foreground hover:bg-black/5 dark:hover:bg-white/10 hover:text-foreground transition-colors',
446467
(pickerOpen || selectedTarget) && 'bg-primary/10 text-primary hover:bg-primary/20'
447468
)}
448469
onClick={() => setPickerOpen((open) => !open)}
449470
disabled={disabled || sending}
450471
title={t('composer.pickAgent')}
451472
>
452-
<AtSign className="h-4 w-4" />
473+
<AtSign className="h-3.5 w-3.5" />
453474
</Button>
454475
{pickerOpen && (
455476
<div className="absolute left-0 bottom-full z-20 mb-2 w-72 overflow-hidden rounded-2xl border border-black/10 bg-white p-1.5 shadow-xl dark:border-white/10 dark:bg-card">
@@ -475,35 +496,13 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
475496
</div>
476497
)}
477498

478-
{/* Textarea */}
479-
<div className="flex-1 relative">
480-
<Textarea
481-
ref={textareaRef}
482-
value={input}
483-
onChange={(e) => setInput(e.target.value)}
484-
onKeyDown={handleKeyDown}
485-
onCompositionStart={() => {
486-
isComposingRef.current = true;
487-
}}
488-
onCompositionEnd={() => {
489-
isComposingRef.current = false;
490-
}}
491-
onPaste={handlePaste}
492-
placeholder={disabled ? t('composer.gatewayDisconnectedPlaceholder') : ''}
493-
disabled={disabled}
494-
data-testid="chat-composer-input"
495-
className="min-h-[40px] max-h-[200px] resize-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none bg-transparent py-2.5 px-2 text-[15px] placeholder:text-muted-foreground/60 leading-relaxed"
496-
rows={1}
497-
/>
498-
</div>
499-
500-
{/* Send Button */}
499+
{/* Send Button — pushed to the right */}
501500
<Button
502501
onClick={sending ? handleStop : handleSend}
503502
disabled={sending ? !canStop : !canSend}
504503
size="icon"
505504
data-testid="chat-composer-send"
506-
className={`shrink-0 h-10 w-10 rounded-full transition-colors ${
505+
className={`ml-auto shrink-0 h-8 w-8 rounded-lg transition-colors ${
507506
(sending || canSend)
508507
? 'bg-black/5 dark:bg-white/10 text-foreground hover:bg-black/10 dark:hover:bg-white/20'
509508
: 'text-muted-foreground/50 hover:bg-transparent bg-transparent'
@@ -512,9 +511,9 @@ export function ChatInput({ onSend, onStop, disabled = false, sending = false, i
512511
title={sending ? t('composer.stop') : t('composer.send')}
513512
>
514513
{sending ? (
515-
<Square className="h-4 w-4" fill="currentColor" />
514+
<Square className="h-3.5 w-3.5" fill="currentColor" />
516515
) : (
517-
<SendHorizontal className="h-[18px] w-[18px]" strokeWidth={2} />
516+
<SendHorizontal className="h-4 w-4" strokeWidth={2} />
518517
)}
519518
</Button>
520519
</div>

src/pages/Chat/ChatMessage.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@ import { extractText, extractThinking, extractImages, extractToolUse, formatTime
1616

1717
interface ChatMessageProps {
1818
message: RawMessage;
19-
showThinking: boolean;
19+
textOverride?: string;
2020
suppressToolCards?: boolean;
2121
suppressProcessAttachments?: boolean;
22+
/**
23+
* When true, hides the assistant text bubble (and any thinking block that
24+
* would be shown above it). Used when the message's text is being folded
25+
* into an ExecutionGraphCard as a narration step, to prevent the same text
26+
* from appearing both inside the graph and as an orphan bubble in the chat
27+
* stream.
28+
*/
29+
suppressAssistantText?: boolean;
2230
isStreaming?: boolean;
2331
streamingTools?: Array<{
2432
id?: string;
@@ -41,21 +49,27 @@ function imageSrc(img: ExtractedImage): string | null {
4149

4250
export const ChatMessage = memo(function ChatMessage({
4351
message,
44-
showThinking,
52+
textOverride,
4553
suppressToolCards = false,
4654
suppressProcessAttachments = false,
55+
suppressAssistantText = false,
4756
isStreaming = false,
4857
streamingTools = [],
4958
}: ChatMessageProps) {
5059
const isUser = message.role === 'user';
5160
const role = typeof message.role === 'string' ? message.role.toLowerCase() : '';
5261
const isToolResult = role === 'toolresult' || role === 'tool_result';
53-
const text = extractText(message);
54-
const hasText = text.trim().length > 0;
55-
const thinking = extractThinking(message);
62+
const text = textOverride ?? extractText(message);
63+
// When text is folded into an ExecutionGraphCard, treat the message as
64+
// having no text for rendering purposes. Keeping this behind a flag (vs
65+
// blanking `text` outright) lets future hover affordances still read the
66+
// original content without surfacing the bubble.
67+
const hideAssistantText = suppressAssistantText && !isUser;
68+
const hasText = !hideAssistantText && text.trim().length > 0;
69+
const visibleThinkingRaw = extractThinking(message);
70+
const visibleThinking = hideAssistantText ? null : visibleThinkingRaw;
5671
const images = extractImages(message);
5772
const tools = extractToolUse(message);
58-
const visibleThinking = showThinking ? thinking : null;
5973
const visibleTools = suppressToolCards ? [] : tools;
6074
const shouldHideProcessAttachments = suppressProcessAttachments
6175
&& (hasText || !!visibleThinking || images.length > 0 || visibleTools.length > 0);

src/pages/Chat/ChatToolbar.tsx

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/**
22
* Chat Toolbar
3-
* Session selector, new session, refresh, and thinking toggle.
3+
* Session selector, new session, and refresh.
44
* Rendered in the Header when on the Chat page.
55
*/
66
import { useMemo } from 'react';
7-
import { RefreshCw, Brain, Bot } from 'lucide-react';
7+
import { RefreshCw, Bot } from 'lucide-react';
88
import { Button } from '@/components/ui/button';
99
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
1010
import { useChatStore } from '@/stores/chat';
@@ -15,8 +15,6 @@ import { useTranslation } from 'react-i18next';
1515
export function ChatToolbar() {
1616
const refresh = useChatStore((s) => s.refresh);
1717
const loading = useChatStore((s) => s.loading);
18-
const showThinking = useChatStore((s) => s.showThinking);
19-
const toggleThinking = useChatStore((s) => s.toggleThinking);
2018
const currentAgentId = useChatStore((s) => s.currentAgentId);
2119
const agents = useAgentsStore((s) => s.agents);
2220
const { t } = useTranslation('chat');
@@ -48,26 +46,6 @@ export function ChatToolbar() {
4846
<p>{t('toolbar.refresh')}</p>
4947
</TooltipContent>
5048
</Tooltip>
51-
52-
{/* Thinking Toggle */}
53-
<Tooltip>
54-
<TooltipTrigger asChild>
55-
<Button
56-
variant="ghost"
57-
size="icon"
58-
className={cn(
59-
'h-8 w-8',
60-
showThinking && 'bg-primary/10 text-primary',
61-
)}
62-
onClick={toggleThinking}
63-
>
64-
<Brain className="h-4 w-4" />
65-
</Button>
66-
</TooltipTrigger>
67-
<TooltipContent>
68-
<p>{showThinking ? t('toolbar.hideThinking') : t('toolbar.showThinking')}</p>
69-
</TooltipContent>
70-
</Tooltip>
7149
</div>
7250
);
7351
}

0 commit comments

Comments
 (0)