mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 04:30:48 +00:00
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:
parent
d7aa98a0c0
commit
cecc960254
13 changed files with 200 additions and 108 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
64
packages/cli/src/ui/components/agent-view/AgentHeader.tsx
Normal file
64
packages/cli/src/ui/components/agent-view/AgentHeader.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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}`}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue