feat(arena): improve agent UI with header info and simplify worktree branches

- Add AgentHeader component showing model, path, and git branch
- Separate modelId and modelName in RegisteredAgent for cleaner display
- Simplify worktree branch naming from worktrees/session/name to base-session-name
- Change loading text from "Agent is working…" to "Thinking…"
- Make agent footer always visible (not just when input is active)

This improves the agent collaboration UX by providing context about each
agent's environment and simplifies the git worktree management.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-03-11 11:04:46 +08:00
parent d7aa98a0c0
commit cecc960254
13 changed files with 200 additions and 108 deletions

View file

@ -26,6 +26,7 @@ import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
import {
AgentStatus,
AgentEventType,
getGitBranch,
type AgentStatusChangeEvent,
} from '@qwen-code/qwen-code-core';
import {
@ -40,6 +41,7 @@ import { theme } from '../../semantic-colors.js';
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
import { useKeypress } from '../../hooks/useKeypress.js';
import { agentMessagesToHistoryItems } from './agentHistoryAdapter.js';
import { AgentHeader } from './AgentHeader.js';
// ─── Main Component ─────────────────────────────────────────
@ -188,7 +190,17 @@ export const AgentChatView = ({ agentId }: AgentChatViewProps) => {
const committedItems = allItems.slice(0, splitIndex);
const pendingItems = allItems.slice(splitIndex);
if (!agent || !interactiveAgent) {
const core = interactiveAgent?.getCore();
const agentWorkingDir = core?.runtimeContext.getTargetDir() ?? '';
// Cache the branch — it won't change during the agent's lifetime and
// getGitBranch uses synchronous execSync which blocks the render loop.
const agentGitBranch = useMemo(
() => (agentWorkingDir ? getGitBranch(agentWorkingDir) : ''),
// eslint-disable-next-line react-hooks/exhaustive-deps
[agentId],
);
if (!agent || !interactiveAgent || !core) {
return (
<Box marginX={2}>
<Text color={theme.status.error}>
@ -198,6 +210,8 @@ export const AgentChatView = ({ agentId }: AgentChatViewProps) => {
);
}
const agentModelId = core.modelConfig.model ?? '';
return (
<Box flexDirection="column">
{/* Committed message history.
@ -206,15 +220,24 @@ export const AgentChatView = ({ agentId }: AgentChatViewProps) => {
all items on the cleared screen. */}
<Static
key={`agent-${agentId}-${historyRemountKey}`}
items={committedItems.map((item) => (
<HistoryItemDisplay
key={item.id}
item={item}
isPending={false}
terminalWidth={terminalWidth}
mainAreaWidth={contentWidth}
/>
))}
items={[
<AgentHeader
key="agent-header"
modelId={agentModelId}
modelName={agent.modelName}
workingDirectory={agentWorkingDir}
gitBranch={agentGitBranch}
/>,
...committedItems.map((item) => (
<HistoryItemDisplay
key={item.id}
item={item}
isPending={false}
terminalWidth={terminalWidth}
mainAreaWidth={contentWidth}
/>
)),
]}
>
{(item) => item}
</Static>

View file

@ -242,7 +242,7 @@ export const AgentComposer: React.FC<AgentComposerProps> = ({ agentId }) => {
<LoadingIndicator
currentLoadingPhrase={
streamingState === StreamingState.Responding
? t('Agent is working…')
? t('Thinking…')
: undefined
}
elapsedTime={elapsedTime}
@ -268,16 +268,14 @@ export const AgentComposer: React.FC<AgentComposerProps> = ({ agentId }) => {
/>
{/* Footer: approval mode + context usage */}
{isInputActive && (
<AgentFooter
approvalMode={agentApprovalMode}
promptTokenCount={lastPromptTokenCount}
contextWindowSize={
config.getContentGeneratorConfig()?.contextWindowSize
}
terminalWidth={terminalWidth}
/>
)}
<AgentFooter
approvalMode={agentApprovalMode}
promptTokenCount={lastPromptTokenCount}
contextWindowSize={
config.getContentGeneratorConfig()?.contextWindowSize
}
terminalWidth={terminalWidth}
/>
</Box>
</StreamingContext.Provider>
);

View file

@ -0,0 +1,64 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Compact header for agent tabs, visually distinct from the
* main view's boxed logo header. Shows model, working directory, and git
* branch in a bordered info panel.
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { shortenPath, tildeifyPath } from '@qwen-code/qwen-code-core';
import { theme } from '../../semantic-colors.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
interface AgentHeaderProps {
modelId: string;
modelName?: string;
workingDirectory: string;
gitBranch?: string;
}
export const AgentHeader: React.FC<AgentHeaderProps> = ({
modelId,
modelName,
workingDirectory,
gitBranch,
}) => {
const { columns: terminalWidth } = useTerminalSize();
const maxPathLen = Math.max(20, terminalWidth - 12);
const displayPath = shortenPath(tildeifyPath(workingDirectory), maxPathLen);
const modelText =
modelName && modelName !== modelId ? `${modelId} (${modelName})` : modelId;
return (
<Box
flexDirection="column"
marginX={2}
marginTop={1}
borderStyle="round"
borderColor={theme.border.default}
paddingX={1}
>
<Text>
<Text color={theme.text.secondary}>{'Model: '}</Text>
<Text color={theme.text.primary}>{modelText}</Text>
</Text>
<Text>
<Text color={theme.text.secondary}>{'Path: '}</Text>
<Text color={theme.text.primary}>{displayPath}</Text>
</Text>
{gitBranch && (
<Text>
<Text color={theme.text.secondary}>{'Branch: '}</Text>
<Text color={theme.text.primary}>{gitBranch}</Text>
</Text>
)}
</Box>
);
};

View file

@ -149,7 +149,7 @@ export const AgentTabBar: React.FC = () => {
backgroundColor={isActive ? theme.border.default : undefined}
color={isActive ? undefined : agent.color || theme.text.secondary}
>
{` ${agent.displayName} `}
{` ${agent.modelId} `}
</Text>
<Text dimColor={!isFocused} color={indicatorColor}>
{` ${symbol}`}

View file

@ -6,6 +6,7 @@
export { AgentTabBar } from './AgentTabBar.js';
export { AgentChatView } from './AgentChatView.js';
export { AgentHeader } from './AgentHeader.js';
export { AgentComposer } from './AgentComposer.js';
export { AgentFooter } from './AgentFooter.js';
export { agentMessagesToHistoryItems } from './agentHistoryAdapter.js';

View file

@ -213,7 +213,7 @@ export function ArenaStatusDialog({
{/* Agent rows */}
{agents.map((agent) => {
const label = agent.model.displayName || agent.model.modelId;
const label = agent.model.modelId;
const { text: statusText, color } = getArenaStatusLabel(agent.status);
const elapsed = getElapsedMs(agent);
@ -270,18 +270,6 @@ export function ArenaStatusDialog({
)}
</Box>
</Box>
{/* In-process mode: show extra detail row with thought/cached tokens */}
{live && (live.thoughtTokens > 0 || live.cachedTokens > 0) && (
<Box marginLeft={2}>
<Text color={theme.text.secondary}>
{live.thoughtTokens > 0 &&
`Thinking: ${live.thoughtTokens.toLocaleString()} tok`}
{live.thoughtTokens > 0 && live.cachedTokens > 0 && ' · '}
{live.cachedTokens > 0 &&
`Cached: ${live.cachedTokens.toLocaleString()} tok`}
</Text>
</Box>
)}
</Box>
);
})}