diff --git a/packages/cli/src/ui/commands/arenaCommand.ts b/packages/cli/src/ui/commands/arenaCommand.ts
index f17c2ce2e..bf9f44387 100644
--- a/packages/cli/src/ui/commands/arenaCommand.ts
+++ b/packages/cli/src/ui/commands/arenaCommand.ts
@@ -133,12 +133,20 @@ function buildArenaExecutionInput(
const defaultAuthType =
contentGeneratorConfig?.authType ?? AuthType.USE_OPENAI;
- // Build ArenaModelConfig for each model
- const models: ArenaModelConfig[] = parsed.models.map((parsedModel) => ({
- modelId: parsedModel.modelId,
- authType: parsedModel.authType ?? defaultAuthType,
- displayName: parsedModel.modelId,
- }));
+ // Build ArenaModelConfig for each model, resolving display names from
+ // the model registry when available.
+ const modelsConfig = config.getModelsConfig();
+ const models: ArenaModelConfig[] = parsed.models.map((parsedModel) => {
+ const authType =
+ (parsedModel.authType as AuthType | undefined) ?? defaultAuthType;
+ const registryModels = modelsConfig.getAvailableModelsForAuthType(authType);
+ const resolved = registryModels.find((m) => m.id === parsedModel.modelId);
+ return {
+ modelId: parsedModel.modelId,
+ authType,
+ displayName: resolved?.label ?? parsedModel.modelId,
+ };
+ });
return {
task: parsed.task,
diff --git a/packages/cli/src/ui/components/agent-view/AgentChatView.tsx b/packages/cli/src/ui/components/agent-view/AgentChatView.tsx
index 371c8bb27..485316436 100644
--- a/packages/cli/src/ui/components/agent-view/AgentChatView.tsx
+++ b/packages/cli/src/ui/components/agent-view/AgentChatView.tsx
@@ -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 (
@@ -198,6 +210,8 @@ export const AgentChatView = ({ agentId }: AgentChatViewProps) => {
);
}
+ const agentModelId = core.modelConfig.model ?? '';
+
return (
{/* Committed message history.
@@ -206,15 +220,24 @@ export const AgentChatView = ({ agentId }: AgentChatViewProps) => {
all items on the cleared screen. */}
(
-
- ))}
+ items={[
+ ,
+ ...committedItems.map((item) => (
+
+ )),
+ ]}
>
{(item) => item}
diff --git a/packages/cli/src/ui/components/agent-view/AgentComposer.tsx b/packages/cli/src/ui/components/agent-view/AgentComposer.tsx
index 8c4d18b82..3d8062bfa 100644
--- a/packages/cli/src/ui/components/agent-view/AgentComposer.tsx
+++ b/packages/cli/src/ui/components/agent-view/AgentComposer.tsx
@@ -242,7 +242,7 @@ export const AgentComposer: React.FC = ({ agentId }) => {
= ({ agentId }) => {
/>
{/* Footer: approval mode + context usage */}
- {isInputActive && (
-
- )}
+
);
diff --git a/packages/cli/src/ui/components/agent-view/AgentHeader.tsx b/packages/cli/src/ui/components/agent-view/AgentHeader.tsx
new file mode 100644
index 000000000..1bf9d4c34
--- /dev/null
+++ b/packages/cli/src/ui/components/agent-view/AgentHeader.tsx
@@ -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 = ({
+ 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 (
+
+
+ {'Model: '}
+ {modelText}
+
+
+ {'Path: '}
+ {displayPath}
+
+ {gitBranch && (
+
+ {'Branch: '}
+ {gitBranch}
+
+ )}
+
+ );
+};
diff --git a/packages/cli/src/ui/components/agent-view/AgentTabBar.tsx b/packages/cli/src/ui/components/agent-view/AgentTabBar.tsx
index a502363b4..c7b0b113c 100644
--- a/packages/cli/src/ui/components/agent-view/AgentTabBar.tsx
+++ b/packages/cli/src/ui/components/agent-view/AgentTabBar.tsx
@@ -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} `}
{` ${symbol}`}
diff --git a/packages/cli/src/ui/components/agent-view/index.ts b/packages/cli/src/ui/components/agent-view/index.ts
index caa00a18a..c1e595c22 100644
--- a/packages/cli/src/ui/components/agent-view/index.ts
+++ b/packages/cli/src/ui/components/agent-view/index.ts
@@ -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';
diff --git a/packages/cli/src/ui/components/arena/ArenaStatusDialog.tsx b/packages/cli/src/ui/components/arena/ArenaStatusDialog.tsx
index 1a126c102..a6409b793 100644
--- a/packages/cli/src/ui/components/arena/ArenaStatusDialog.tsx
+++ b/packages/cli/src/ui/components/arena/ArenaStatusDialog.tsx
@@ -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({
)}
- {/* In-process mode: show extra detail row with thought/cached tokens */}
- {live && (live.thoughtTokens > 0 || live.cachedTokens > 0) && (
-
-
- {live.thoughtTokens > 0 &&
- `Thinking: ${live.thoughtTokens.toLocaleString()} tok`}
- {live.thoughtTokens > 0 && live.cachedTokens > 0 && ' · '}
- {live.cachedTokens > 0 &&
- `Cached: ${live.cachedTokens.toLocaleString()} tok`}
-
-
- )}
);
})}
diff --git a/packages/cli/src/ui/contexts/AgentViewContext.tsx b/packages/cli/src/ui/contexts/AgentViewContext.tsx
index cb85ab4f2..b2c35e6d3 100644
--- a/packages/cli/src/ui/contexts/AgentViewContext.tsx
+++ b/packages/cli/src/ui/contexts/AgentViewContext.tsx
@@ -33,7 +33,10 @@ import { useArenaInProcess } from '../hooks/useArenaInProcess.js';
export interface RegisteredAgent {
interactiveAgent: AgentInteractive;
- displayName: string;
+ /** Model identifier shown in tabs and paths (e.g. "glm-5"). */
+ modelId: string;
+ /** Human-friendly model name (e.g. "GLM 5"). */
+ modelName?: string;
color: string;
}
@@ -60,8 +63,9 @@ export interface AgentViewActions {
registerAgent(
agentId: string,
interactiveAgent: AgentInteractive,
- displayName: string,
+ modelId: string,
color: string,
+ modelName?: string,
): void;
unregisterAgent(agentId: string): void;
unregisterAll(): void;
@@ -173,12 +177,18 @@ export function AgentViewProvider({
(
agentId: string,
interactiveAgent: AgentInteractive,
- displayName: string,
+ modelId: string,
color: string,
+ modelName?: string,
) => {
setAgents((prev) => {
const next = new Map(prev);
- next.set(agentId, { interactiveAgent, displayName, color });
+ next.set(agentId, {
+ interactiveAgent,
+ modelId,
+ color,
+ modelName,
+ });
return next;
});
// Seed approval mode from the agent's own config
diff --git a/packages/cli/src/ui/hooks/useArenaInProcess.ts b/packages/cli/src/ui/hooks/useArenaInProcess.ts
index c5793490b..c75634a2a 100644
--- a/packages/cli/src/ui/hooks/useArenaInProcess.ts
+++ b/packages/cli/src/ui/hooks/useArenaInProcess.ts
@@ -93,8 +93,9 @@ export function useArenaInProcess(
actionsRef.current.registerAgent(
agentState.agentId,
interactive,
- agentState.model.displayName || agentState.model.modelId,
+ agentState.model.modelId,
nextColor(),
+ agentState.model.displayName,
);
}
}
@@ -115,8 +116,9 @@ export function useArenaInProcess(
actionsRef.current.registerAgent(
event.agentId,
interactive,
- event.model.displayName || event.model.modelId,
+ event.model.modelId,
nextColor(),
+ event.model.displayName,
);
return;
}
diff --git a/packages/core/src/agents/arena/ArenaManager.test.ts b/packages/core/src/agents/arena/ArenaManager.test.ts
index 3ffcaa3b3..a21f15d63 100644
--- a/packages/core/src/agents/arena/ArenaManager.test.ts
+++ b/packages/core/src/agents/arena/ArenaManager.test.ts
@@ -411,10 +411,7 @@ describe('ArenaManager', () => {
expect(mockBackend.cleanup).toHaveBeenCalledTimes(1);
// cleanupSession is called with worktreeDirName (short ID), not the full sessionId.
// For 'test-session', the short ID is 'testsess' (first 8 chars with dashes removed).
- expect(hoistedMockCleanupSession).toHaveBeenCalledWith(
- 'testsess',
- 'arena',
- );
+ expect(hoistedMockCleanupSession).toHaveBeenCalledWith('testsess');
expect(manager.getBackend()).toBeNull();
expect(manager.getSessionId()).toBeUndefined();
});
diff --git a/packages/core/src/agents/arena/ArenaManager.ts b/packages/core/src/agents/arena/ArenaManager.ts
index a14dd3e06..e271de7d2 100644
--- a/packages/core/src/agents/arena/ArenaManager.ts
+++ b/packages/core/src/agents/arena/ArenaManager.ts
@@ -508,7 +508,7 @@ export class ArenaManager {
}
// Clean up worktrees
- await this.worktreeService.cleanupSession(this.worktreeDirName!, 'arena');
+ await this.worktreeService.cleanupSession(this.worktreeDirName!);
this.agents.clear();
this.cachedResult = null;
@@ -758,15 +758,12 @@ export class ArenaManager {
debugLogger.info('Setting up worktrees for Arena agents');
- const worktreeNames = this.arenaConfig.models.map(
- (m) => m.displayName || m.modelId,
- );
+ const worktreeNames = this.arenaConfig.models.map((m) => m.modelId);
const result = await this.worktreeService.setupWorktrees({
sessionId: this.worktreeDirName!,
sourceRepoPath: this.arenaConfig.sourceRepoPath,
worktreeNames,
- branchPrefix: 'arena',
metadata: { arenaSessionId: this.arenaConfig.sessionId },
});
diff --git a/packages/core/src/services/gitWorktreeService.test.ts b/packages/core/src/services/gitWorktreeService.test.ts
index 2eb028d98..f34eb1ca2 100644
--- a/packages/core/src/services/gitWorktreeService.test.ts
+++ b/packages/core/src/services/gitWorktreeService.test.ts
@@ -148,13 +148,13 @@ describe('GitWorktreeService', () => {
'model-a',
);
expect(result.success).toBe(true);
- expect(result.worktree?.branch).toBe('worktrees/s1/model-a');
+ expect(result.worktree?.branch).toBe('main-s1-model-a');
expect(result.worktree?.path).toBe(expectedPath);
expect(hoistedMockRaw).toHaveBeenCalledWith([
'worktree',
'add',
'-b',
- 'worktrees/s1/model-a',
+ 'main-s1-model-a',
expectedPath,
'main',
]);
@@ -228,7 +228,7 @@ describe('GitWorktreeService', () => {
expect(result.success).toBe(false);
expect(result.errors).toContainEqual({ name: 'b', error: 'boom' });
- expect(cleanupSpy).toHaveBeenCalledWith('s1', 'worktrees');
+ expect(cleanupSpy).toHaveBeenCalledWith('s1');
});
it('listWorktrees should return empty array when session dir does not exist', async () => {
@@ -256,31 +256,34 @@ describe('GitWorktreeService', () => {
expect(hoistedMockRaw).toHaveBeenNthCalledWith(2, ['worktree', 'prune']);
});
- it('cleanupSession should remove prefixed branches only', async () => {
+ it('cleanupSession should remove branches from listed worktrees', async () => {
const service = new GitWorktreeService('/repo');
- vi.spyOn(service, 'listWorktrees').mockResolvedValue([]);
- hoistedMockBranch.mockImplementation((args?: string[]) => {
- if (args?.[0] === '-a') {
- return Promise.resolve({
- branches: {
- main: {},
- 'worktrees/s1/a': {},
- 'worktrees/s1/b': {},
- },
- });
- }
- return Promise.resolve({ branches: {} });
- });
+ vi.spyOn(service, 'listWorktrees').mockResolvedValue([
+ {
+ id: 's1/a',
+ name: 'a',
+ path: '/w/a',
+ branch: 'main-s1-a',
+ isActive: true,
+ createdAt: Date.now(),
+ },
+ {
+ id: 's1/b',
+ name: 'b',
+ path: '/w/b',
+ branch: 'main-s1-b',
+ isActive: true,
+ createdAt: Date.now(),
+ },
+ ]);
+ vi.spyOn(service, 'removeWorktree').mockResolvedValue({ success: true });
const result = await service.cleanupSession('s1');
expect(result.success).toBe(true);
- expect(result.removedBranches).toEqual([
- 'worktrees/s1/a',
- 'worktrees/s1/b',
- ]);
- expect(hoistedMockBranch).toHaveBeenCalledWith(['-D', 'worktrees/s1/a']);
- expect(hoistedMockBranch).toHaveBeenCalledWith(['-D', 'worktrees/s1/b']);
+ expect(result.removedBranches).toEqual(['main-s1-a', 'main-s1-b']);
+ expect(hoistedMockBranch).toHaveBeenCalledWith(['-D', 'main-s1-a']);
+ expect(hoistedMockBranch).toHaveBeenCalledWith(['-D', 'main-s1-b']);
expect(hoistedMockRaw).toHaveBeenCalledWith(['worktree', 'prune']);
});
diff --git a/packages/core/src/services/gitWorktreeService.ts b/packages/core/src/services/gitWorktreeService.ts
index 5683fcdf0..6ceebf11e 100644
--- a/packages/core/src/services/gitWorktreeService.ts
+++ b/packages/core/src/services/gitWorktreeService.ts
@@ -6,6 +6,7 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
+import { execSync } from 'node:child_process';
import { simpleGit, CheckRepoActions } from 'simple-git';
import type { SimpleGit } from 'simple-git';
import { Storage } from '../config/storage.js';
@@ -51,8 +52,6 @@ export interface WorktreeSetupConfig {
worktreeNames: string[];
/** Base branch to create worktrees from (defaults to current branch) */
baseBranch?: string;
- /** Branch prefix for worktree branches (default: 'worktrees') */
- branchPrefix?: string;
/** Extra metadata to persist alongside the session config */
metadata?: Record;
}
@@ -226,7 +225,6 @@ export class GitWorktreeService {
sessionId: string,
name: string,
baseBranch?: string,
- branchPrefix: string = WORKTREES_DIR,
): Promise {
try {
const worktreesDir = GitWorktreeService.getWorktreesDir(
@@ -238,7 +236,6 @@ export class GitWorktreeService {
// Sanitize name for use as branch and directory name
const sanitizedName = this.sanitizeName(name);
const worktreePath = path.join(worktreesDir, sanitizedName);
- const branchName = `${branchPrefix}/${sessionId}/${sanitizedName}`;
// Check if worktree already exists
const exists = await this.pathExists(worktreePath);
@@ -251,6 +248,8 @@ export class GitWorktreeService {
// Determine base branch
const base = baseBranch || (await this.getCurrentBranch());
+ const shortSession = sessionId.slice(0, 6);
+ const branchName = `${base}-${shortSession}-${sanitizedName}`;
// Create the worktree with a new branch
await this.git.raw([
@@ -381,15 +380,12 @@ export class GitWorktreeService {
// Non-fatal: proceed without untracked files
}
- const branchPrefix = config.branchPrefix ?? WORKTREES_DIR;
-
// Create worktrees for each entry
for (const name of config.worktreeNames) {
const createResult = await this.createWorktree(
config.sessionId,
name,
config.baseBranch,
- branchPrefix,
);
if (createResult.success && createResult.worktree) {
@@ -406,7 +402,7 @@ export class GitWorktreeService {
// If any worktree failed, clean up all created resources and fail
if (result.errors.length > 0) {
try {
- await this.cleanupSession(config.sessionId, branchPrefix);
+ await this.cleanupSession(config.sessionId);
} catch (error) {
result.errors.push({
name: 'cleanup',
@@ -468,10 +464,7 @@ export class GitWorktreeService {
/**
* Lists all worktrees for a session.
*/
- async listWorktrees(
- sessionId: string,
- branchPrefix: string = WORKTREES_DIR,
- ): Promise {
+ async listWorktrees(sessionId: string): Promise {
const worktreesDir = GitWorktreeService.getWorktreesDir(
sessionId,
this.customBaseDir,
@@ -484,7 +477,18 @@ export class GitWorktreeService {
for (const entry of entries) {
if (entry.isDirectory()) {
const worktreePath = path.join(worktreesDir, entry.name);
- const branchName = `${branchPrefix}/${sessionId}/${entry.name}`;
+
+ // Read the actual branch from the worktree
+ let branchName = '';
+ try {
+ branchName = execSync('git rev-parse --abbrev-ref HEAD', {
+ cwd: worktreePath,
+ encoding: 'utf8',
+ stdio: ['pipe', 'pipe', 'pipe'],
+ }).trim();
+ } catch {
+ // Fallback if git command fails
+ }
// Try to get stats for creation time
let createdAt = Date.now();
@@ -544,10 +548,7 @@ export class GitWorktreeService {
/**
* Cleans up all worktrees and branches for a session.
*/
- async cleanupSession(
- sessionId: string,
- branchPrefix: string = WORKTREES_DIR,
- ): Promise<{
+ async cleanupSession(sessionId: string): Promise<{
success: boolean;
removedWorktrees: string[];
removedBranches: string[];
@@ -560,7 +561,11 @@ export class GitWorktreeService {
errors: [] as string[],
};
- const worktrees = await this.listWorktrees(sessionId, branchPrefix);
+ // Collect actual branch names from worktrees before removing them
+ const worktrees = await this.listWorktrees(sessionId);
+ const worktreeBranches = new Set(
+ worktrees.map((w) => w.branch).filter(Boolean),
+ );
// Remove all worktrees
for (const worktree of worktrees) {
@@ -588,18 +593,14 @@ export class GitWorktreeService {
);
}
- // Clean up branches
- const prefix = `${branchPrefix}/${sessionId}/`;
+ // Clean up branches that belonged to the worktrees
try {
- const branches = await this.git.branch(['-a']);
- for (const branchName of Object.keys(branches.branches)) {
- if (branchName.startsWith(prefix)) {
- try {
- await this.git.branch(['-D', branchName]);
- result.removedBranches.push(branchName);
- } catch {
- // Branch might already be deleted, ignore
- }
+ for (const branchName of worktreeBranches) {
+ try {
+ await this.git.branch(['-D', branchName]);
+ result.removedBranches.push(branchName);
+ } catch {
+ // Branch might already be deleted, ignore
}
}
} catch {