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
2 changes: 1 addition & 1 deletion app/shared/src/chatview/components/ChatViewTranscript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export const ChatViewTranscript = memo(function ChatViewTranscript({ transcript,
<span>{pinnedAnnouncement.content}</span>
</div>
<div className="chatview-pinned-meta">
由 <a>{pinnedAnnouncement.author ?? '系统'}</a> 置顶
{t('pinnedAnnouncement.meta', { author: pinnedAnnouncement.author ?? t('pinnedAnnouncement.systemAuthor') })}
{pinnedAnnouncement.time && <span className="chatview-pinned-time">{pinnedAnnouncement.time}</span>}
</div>
</div>
Expand Down
9 changes: 6 additions & 3 deletions app/shared/src/chatview/components/CompactDivider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
══════════════════════════════════════════════════════════════════════ */

import React, { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { CHATVIEW_I18N_NAMESPACE } from '../i18n/resources'

interface CompactDividerProps {
/** Compaction trigger, e.g. "auto" or "manual". */
Expand All @@ -20,9 +22,10 @@ function formatPreTokens(n: number): string {

/** Render a compact-boundary divider between transcript message groups. */
export const CompactDivider = memo(function CompactDivider({ trigger, preTokens }: CompactDividerProps) {
const parts: string[] = ['上下文已压缩']
if (trigger === 'auto') parts.push('(自动)')
if (preTokens != null) parts.push(`${formatPreTokens(preTokens)} tokens 前`)
const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE)
const parts: string[] = [t('compactDivider.label')]
if (trigger === 'auto') parts.push(t('compactDivider.auto'))
if (preTokens != null) parts.push(t('compactDivider.tokensAgo', { tokens: formatPreTokens(preTokens) }))

const label = parts.join(' ')

Expand Down
32 changes: 32 additions & 0 deletions app/shared/src/chatview/i18n/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,21 @@ export const chatviewResources = {
'inlineDelegation.status.done': '完成 ✓',
'inlineDelegation.status.failed': '失败 ✗',
'inlineDelegation.status.cancelled': '已取消',
// ── CompactDivider ──
'compactDivider.label': '上下文已压缩',
'compactDivider.auto': '(自动)',
'compactDivider.tokensAgo': '压缩前 {{tokens}} tokens',
// ── AgentStreamingBar ──
'agentStreaming.status.dispatching': '调度中…',
'agentStreaming.status.thinking': '思考中…',
'agentStreaming.status.streaming': '输出中…',
'agentStreaming.status.done': '完成',
'agentStreaming.status.failed': '失败',
'agentStreaming.toolCalls': '{{count}} 次工具调用',
'agentStreaming.multiAgent': '{{count}} 个 Agent 运行中',
// ── Pinned announcement ──
'pinnedAnnouncement.meta': '由 {{author}} 置顶',
'pinnedAnnouncement.systemAuthor': '系统',
'card.fail.retry': '重试',
'card.expand': '展开',
'card.collapse': '收起',
Expand Down Expand Up @@ -718,6 +733,23 @@ export const chatviewResources = {
'inlineDelegation.status.done': 'Done ✓',
'inlineDelegation.status.failed': 'Failed ✗',
'inlineDelegation.status.cancelled': 'Cancelled',
// ── CompactDivider ──
'compactDivider.label': 'Context compressed',
'compactDivider.auto': '(auto)',
'compactDivider.tokensAgo': '{{tokens}} tokens before compaction',
// ── AgentStreamingBar ──
'agentStreaming.status.dispatching': 'Dispatching…',
'agentStreaming.status.thinking': 'Thinking…',
'agentStreaming.status.streaming': 'Streaming…',
'agentStreaming.status.done': 'Done',
'agentStreaming.status.failed': 'Failed',
'agentStreaming.toolCalls_one': '{{count}} tool call',
'agentStreaming.toolCalls_other': '{{count}} tool calls',
'agentStreaming.multiAgent_one': '{{count}} agent running',
'agentStreaming.multiAgent_other': '{{count}} agents running',
// ── Pinned announcement ──
'pinnedAnnouncement.meta': 'Pinned by {{author}}',
'pinnedAnnouncement.systemAuthor': 'System',
'card.fail.retry': 'Retry',
'card.expand': 'Expand',
'card.collapse': 'Collapse',
Expand Down
11 changes: 10 additions & 1 deletion app/shared/src/ui/AgentStreamingBar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import React from 'react';
import { describe, it, expect, afterEach } from 'vitest';
import { describe, it, expect, afterEach, afterAll, beforeAll } from 'vitest';
import { render } from '@testing-library/react';
import { AgentStreamingBar } from './AgentStreamingBar';
import { getAgentActivityStore } from '../transcript/agentActivity';
import { useTestI18nLanguage, TEST_I18N_DEFAULT_LNG } from '../testing/i18n';

beforeAll(async () => {
await useTestI18nLanguage('zh');
});
Comment thread
DeliciousBuding marked this conversation as resolved.

afterAll(async () => {
await useTestI18nLanguage(TEST_I18N_DEFAULT_LNG);
});

afterEach(() => {
getAgentActivityStore().reset();
Expand Down
27 changes: 14 additions & 13 deletions app/shared/src/ui/AgentStreamingBar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useSyncExternalStore } from 'react';
import { useTranslation } from 'react-i18next';
import {
getAgentActivityStore,
type AgentActivitySnapshot,
type AgentActivityStatus,
} from '../transcript/agentActivity';
import { CHATVIEW_I18N_NAMESPACE } from '../chatview/i18n/resources';
import styles from './AgentStreamingBar.module.css';

// ── Status icons ──────────────────────────────────────────────────────────
Expand All @@ -16,6 +18,14 @@ const STATUS_ICON: Record<AgentActivityStatus, string> = {
failed: '\u{274C}',
};

const STATUS_LABEL_KEY: Record<AgentActivityStatus, string> = {
dispatching: 'agentStreaming.status.dispatching',
thinking: 'agentStreaming.status.thinking',
streaming: 'agentStreaming.status.streaming',
done: 'agentStreaming.status.done',
failed: 'agentStreaming.status.failed',
};

// ── Store binding ─────────────────────────────────────────────────────────

const emptySnapshot: AgentActivitySnapshot = { activeAgents: [] };
Expand Down Expand Up @@ -44,6 +54,7 @@ export interface AgentStreamingBarProps {
* Hidden when no agents are active. Uses `useSyncExternalStore` for reactivity.
*/
export function AgentStreamingBar({ className }: AgentStreamingBarProps): React.ReactElement | null {
const { t } = useTranslation(CHATVIEW_I18N_NAMESPACE);
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);

const agents = snapshot.activeAgents;
Expand All @@ -58,7 +69,7 @@ export function AgentStreamingBar({ className }: AgentStreamingBarProps): React.
// No real progress percentage exists for agent runs, so surface the one
// quantitative signal we do have: the number of tool calls.
const totalToolCalls = agents.reduce((sum, a) => sum + (a.toolCalls ?? 0), 0);
const toolCallsLabel = totalToolCalls > 0 ? ` · ${totalToolCalls} 次工具调用` : '';
const toolCallsLabel = totalToolCalls > 0 ? ` · ${t('agentStreaming.toolCalls', { count: totalToolCalls })}` : '';

return (
<div className={`${styles.bar} ${className ?? ''}`} role="status" aria-live="polite">
Expand All @@ -67,14 +78,14 @@ export function AgentStreamingBar({ className }: AgentStreamingBarProps): React.
<span className={`${styles.icon} ${isActive(agents[0]!.status) ? styles.iconPulse : ''}`} aria-hidden="true">{STATUS_ICON[agents[0]!.status]}</span>
<span className={styles.name}>{agents[0]!.name}</span>
<span className={`${styles.status} ${statusClassName(agents[0]!.status)}`}>
{statusLabel(agents[0]!.status)}
{t(STATUS_LABEL_KEY[agents[0]!.status])}
{toolCallsLabel}
</span>
</div>
) : (
<div className={styles.agent}>
<span className={`${styles.icon} ${styles.iconPulse}`} aria-hidden="true">{'\u{1F916}'}</span>
<span className={styles.name}>{activeCount} 个 Agent 运行中{toolCallsLabel}</span>
<span className={styles.name}>{t('agentStreaming.multiAgent', { count: activeCount })}{toolCallsLabel}</span>
<span className={styles.detail}>
{agents.map((a) => STATUS_ICON[a.status]).join(' ')}
</span>
Expand All @@ -86,16 +97,6 @@ export function AgentStreamingBar({ className }: AgentStreamingBarProps): React.

// ── Helpers ───────────────────────────────────────────────────────────────

function statusLabel(status: AgentActivityStatus): string {
switch (status) {
case 'dispatching': return '调度中…';
case 'thinking': return '思考中…';
case 'streaming': return '输出中…';
case 'done': return '完成';
case 'failed': return '失败';
}
}

function statusClassName(status: AgentActivityStatus): string {
switch (status) {
case 'streaming': return styles.statusStreaming ?? '';
Expand Down
Loading