Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
57ce07f
feat(ui): add ui.showToolCallArgs to render tool-call arguments inline
TianYuan1024 Aug 30, 2026
cba4773
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Aug 30, 2026
f8739af
fix(ui): address review on inline tool-call arguments
TianYuan1024 Aug 30, 2026
611f4f2
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Aug 30, 2026
5088a36
fix(ui): wire Ctrl+O into the agent view and disclose the inline args…
TianYuan1024 Aug 31, 2026
c9f145d
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Aug 31, 2026
708e573
fix(cli): correct the context property name in the agent-view test
TianYuan1024 Aug 31, 2026
15bee97
test(cli): pin the agent view's live-area full-detail forwarding
TianYuan1024 Aug 31, 2026
5c6ef8c
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Aug 31, 2026
2d461a3
fix(ui): keep the inline args cap off surrogate pairs
TianYuan1024 Aug 31, 2026
40f91d1
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Aug 31, 2026
ea9bc0b
test(cli): stop the parallel-agent exemption test passing on an empty…
TianYuan1024 Aug 31, 2026
c8f2212
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Aug 31, 2026
49bb0e8
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Aug 31, 2026
f7fd9e5
fix(ui): bound the inline tool-args row in lines, not characters
TianYuan1024 Sep 1, 2026
79bd583
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Sep 1, 2026
54e7a46
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Sep 1, 2026
353d8e7
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Sep 1, 2026
68449c0
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Sep 2, 2026
3bba019
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Sep 2, 2026
5d9e92e
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Sep 4, 2026
35c1e8c
Merge remote-tracking branch 'origin/main' into feat/show-tool-call-args
TianYuan1024 Sep 4, 2026
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
69 changes: 35 additions & 34 deletions docs/users/configuration/settings.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions packages/cli/src/config/settingsSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,18 @@ describe('SettingsSchema', () => {
expect(mouseTracking.requiresRestart).toBe(true);
});

it('should have showToolCallArgs in ui settings', () => {
const showToolCallArgs =
getSettingsSchema().ui.properties.showToolCallArgs;
expect(showToolCallArgs).toBeDefined();
expect(showToolCallArgs.type).toBe('boolean');
// Default must stay false — the compact tool view is the baseline.
expect(showToolCallArgs.default).toBe(false);
expect(showToolCallArgs.showInDialog).toBe(true);
// Read at render time, so no restart is needed.
expect(showToolCallArgs.requiresRestart).toBe(false);
});

it('should expose response tokens/sec as an opt-in UI setting', () => {
const responseTokensPerSecond =
getSettingsSchema().ui.properties.showResponseTokensPerSecond;
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,16 @@ const SETTINGS_SCHEMA = {
'Enable in-app SGR mouse tracking. While enabled, Qwen Code captures mouse events for text selection, click-to-position in text inputs, row hover, history-item toggling, and viewport scrolling. Because the terminal forwards all mouse events to the app, it cannot show native right-click context menus or open OSC 8 hyperlink clicks. Disable to restore native right-click and clickable URL links; this turns off all in-app mouse interaction, and in Virtualized History the wheel no longer scrolls the transcript — use Shift+↑/↓, PgUp/PgDn, or Ctrl+Home/End instead (pair with ui.useTerminalBuffer: false to restore native terminal scrollback).',
showInDialog: true,
},
showToolCallArgs: {
type: 'boolean',
label: 'Show Tool Call Arguments',
category: 'UI',
requiresRestart: false,
default: false,
description:
'Render tool calls on their own line with their raw arguments inline, instead of the type-based compact summary that folds read/search/list batches into "Read 3 files". Useful when debugging MCP integrations or tool schemas. Applies wherever the arguments are available: live, resumed, agent-view and speculated turns. Inline rows longer than 1000 characters are truncated with a `+N chars` marker; press Ctrl+O for the complete payload. Groups of running parallel subagents keep their compact roster, and daemon-attached sessions carry no arguments, so both keep the compact view — press Ctrl+O there. Does not change result-output truncation.',
showInDialog: true,
},
shellOutputMaxLines: {
type: 'number',
label: 'Shell Output Max Lines',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/ca.js
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ export default {
'Hide Window Title': 'Amaga el títol de la finestra',
'Show Status in Title': "Mostra l'estat al títol",
'Hide Tips': 'Amaga els consells',
'Show Tool Call Arguments': 'Mostra els arguments de les crides a eines',
'Show Line Numbers in Code': 'Mostra els números de línia al codi',
'Show Citations': 'Mostra les cites',
'Custom Witty Phrases': 'Frases enginyoses personalitzades',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ export default {
'Hide Window Title': 'Fenstertitel ausblenden',
'Show Status in Title': 'Status im Titel anzeigen',
'Hide Tips': 'Tipps ausblenden',
'Show Tool Call Arguments': 'Tool-Aufrufargumente anzeigen',
'Show Line Numbers in Code': 'Zeilennummern im Code anzeigen',
'Show Citations': 'Quellenangaben anzeigen',
'Custom Witty Phrases': 'Benutzerdefinierte Witzige Sprüche',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,7 @@ export default {
'Hide Window Title': 'Hide Window Title',
'Show Status in Title': 'Show Status in Title',
'Hide Tips': 'Hide Tips',
'Show Tool Call Arguments': 'Show Tool Call Arguments',
'Show Line Numbers in Code': 'Show Line Numbers in Code',
'Show Citations': 'Show Citations',
'Custom Witty Phrases': 'Custom Witty Phrases',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ export default {
'Hide Window Title': 'Masquer le titre de la fenêtre',
'Show Status in Title': 'Afficher le statut dans le titre',
'Hide Tips': 'Masquer les conseils',
'Show Tool Call Arguments': 'Afficher les arguments des appels d’outils',
'Show Line Numbers in Code': 'Afficher les numéros de ligne dans le code',
'Show Citations': 'Afficher les citations',
'Custom Witty Phrases': 'Phrases personnalisées spirituelles',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ export default {
'Vim Mode': 'Vim モード',
'Output Format': '出力形式',
'Hide Tips': 'ヒントを非表示',
'Show Tool Call Arguments': 'ツール呼び出し引数を表示',
Text: 'テキスト',
JSON: 'JSON',
Plan: 'プラン',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ export default {
'Hide Window Title': 'Ocultar Título da Janela',
'Show Status in Title': 'Mostrar Status no Título',
'Hide Tips': 'Ocultar Dicas',
'Show Tool Call Arguments': 'Mostrar Argumentos das Chamadas de Ferramenta',
'Show Line Numbers in Code': 'Mostrar Números de Linhas no Código',
'Show Citations': 'Mostrar Citações',
'Custom Witty Phrases': 'Frases de Efeito Personalizadas',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ export default {
'Hide Window Title': 'Скрыть заголовок окна',
'Show Status in Title': 'Показывать статус в заголовке',
'Hide Tips': 'Скрыть подсказки',
'Show Tool Call Arguments': 'Показывать аргументы вызовов инструментов',
'Show Line Numbers in Code': 'Показывать номера строк в коде',
'Show Citations': 'Показывать цитаты',
'Custom Witty Phrases': 'Пользовательские остроумные фразы',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,7 @@ export default {
'Hide Window Title': '隱藏窗口標題',
'Show Status in Title': '在標題中顯示狀態',
'Hide Tips': '隱藏提示',
'Show Tool Call Arguments': '顯示工具呼叫參數',
'Show Line Numbers in Code': '在代碼中顯示行號',
'Show Citations': '顯示引用',
'Custom Witty Phrases': '自定義詼諧短語',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,7 @@ export default {
'Hide Window Title': '隐藏窗口标题',
'Show Status in Title': '在标题中显示状态',
'Hide Tips': '隐藏提示',
'Show Tool Call Arguments': '显示工具调用参数',
'Show Line Numbers in Code': '在代码中显示行号',
'Show Citations': '显示引用',
'Custom Witty Phrases': '自定义诙谐短语',
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/serve/routes/workspace-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ describe('POST /workspace/settings', () => {
expect(persistSetting).not.toHaveBeenCalled();
});

it.each(['ui.mouseTracking', 'ui.showScrollbar'])(
it.each(['ui.mouseTracking', 'ui.showScrollbar', 'ui.showToolCallArgs'])(
'rejects a TUI-only key (%s) that has no effect in the web shell',
async (key) => {
// These keys are read only inside the ink TUI (mouseTracking also
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/serve/routes/workspace-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const TUI_ONLY_SETTINGS = new Set([
'general.outputLanguage',
'ide.enabled',
'ui.showLineNumbers',
'ui.showToolCallArgs',
'ui.renderMode',
'ui.useTerminalBuffer',
'ui.mouseTracking',
Expand Down
28 changes: 28 additions & 0 deletions packages/cli/src/ui/AppContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { useContext, useState, useReducer, useEffect, act } from 'react';
import {
AppContainer,
dedupeNewestFirst,
buildSpeculativeToolDisplays,
getSpeculativeToolResult,
getNextRenderMode,
isInputActiveForState,
Expand Down Expand Up @@ -594,6 +595,33 @@ describe('AppContainer State Management', () => {
status: ToolCallStatus.Success,
});
});

it('carries the functionCall args onto the display object', () => {
// The fourth builder of IndividualToolCallDisplay. Without the args the
// setting half-applies: an accepted speculation falls back to the
// compact summary while live and resumed turns of the same shape show
// their arguments.
const args = { file_path: 'src/a.ts', old_string: 'x', new_string: 'y' };
const tools = buildSpeculativeToolDisplays(
[{ functionCall: { name: 'replace', args } }],
[{ functionResponse: { response: { output: 'done' } } }],
);

expect(tools).toHaveLength(1);
expect(tools[0]!.args).toEqual(args);
expect(tools[0]!.name).toBe('replace');
expect(tools[0]!.status).toBe(ToolCallStatus.Success);
});

it('falls back to an empty args object when the call carries none', () => {
const tools = buildSpeculativeToolDisplays(
[{ functionCall: { name: 'ls' } }],
[],
);
// formatInlineToolArgs skips empty objects, so this renders no args row.
expect(tools[0]!.args).toEqual({});
expect(tools[0]!.description).toBe('ls');
});
});

afterEach(() => {
Expand Down
58 changes: 41 additions & 17 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import {
type UIActions,
} from './contexts/UIActionsContext.js';
import { ConfigContext } from './contexts/ConfigContext.js';
import type { Part } from '@google/genai';
import {
type HistoryItem,
type HistoryItemUser,
type IndividualToolCallDisplay,
ToolCallStatus,
type HistoryItemWithoutId,
} from './types.js';
Expand Down Expand Up @@ -618,6 +620,41 @@ export function getSpeculativeToolResult(response: unknown): {
};
}

/**
* Builds the tool display rows for an accepted speculation.
*
* Extracted from the submit handler so the fourth `IndividualToolCallDisplay`
* builder is unit-testable like its siblings (`mapToDisplay`, the resume path,
* the agent-view adapter) — in particular that it carries the raw `args` that
* `ui.showToolCallArgs` renders.
*/
export function buildSpeculativeToolDisplays(
toolCalls: Part[],
toolResults: Part[],
): IndividualToolCallDisplay[] {
return toolCalls.map((tc, i) => {
const name = tc.functionCall?.name ?? 'unknown';
const args = (tc.functionCall?.args ?? {}) as Record<string, unknown>;
const resp = toolResults[i]?.functionResponse?.response;
const speculativeResult = getSpeculativeToolResult(resp);
return {
callId: `spec-${name}-${i}`,
name,
description:
Object.entries(args)
.map(([k, v]) => `${k}: ${String(v).slice(0, 80)}`)
.join(', ') || name,
// Carried like the live, resume and agent-view builders so
// `ui.showToolCallArgs` renders the args row for an accepted
// speculation too.
args,
resultDisplay: speculativeResult.text.slice(0, 500),
status: speculativeResult.status,
confirmationDetails: undefined,
};
});
}

function getResponseCandidateTokens(
pendingLlmHistoryItems: HistoryItemWithoutId[],
): number {
Expand Down Expand Up @@ -2997,23 +3034,10 @@ export const AppContainer = (props: AppContainerProps) => {
const toolResults =
nextMsg?.parts?.filter((p) => p.functionResponse) ?? [];

const tools = toolCalls.map((tc, i) => {
const name = tc.functionCall?.name ?? 'unknown';
const args = tc.functionCall?.args ?? {};
const resp = toolResults[i]?.functionResponse?.response;
const speculativeResult = getSpeculativeToolResult(resp);
return {
callId: `spec-${name}-${i}`,
name,
description:
Object.entries(args)
.map(([k, v]) => `${k}: ${String(v).slice(0, 80)}`)
.join(', ') || name,
resultDisplay: speculativeResult.text.slice(0, 500),
status: speculativeResult.status,
confirmationDetails: undefined,
};
});
const tools = buildSpeculativeToolDisplays(
toolCalls,
toolResults,
);

const toolGroupItem: HistoryItemWithoutId = {
type: 'tool_group' as const,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Ctrl+O full-detail must reach the agent view's transcript items.
*
* Thinking blocks already honored the toggle here (HistoryItemDisplay reads
* ThoughtExpandedContext itself), but the tool side never received
* `fullDetail` — so `ui.showToolCallArgs` could render a truncated args row
* advertising `(ctrl+o)` for a key that did nothing in this view.
*/

import { describe, it, expect, vi } from 'vitest';
import { render } from 'ink-testing-library';
import { Text } from 'ink';
import { EventEmitter } from 'node:events';
import { ThoughtExpandedProvider } from '../../contexts/ThoughtExpandedContext.js';
import type { AgentCore } from '@qwen-code/qwen-code-core';

const receivedFullDetail: Array<boolean | undefined> = [];

vi.mock('../HistoryItemDisplay.js', () => ({
HistoryItemDisplay: ({ fullDetail }: { fullDetail?: boolean }) => {
receivedFullDetail.push(fullDetail);
return <Text>item</Text>;
},
}));

vi.mock('../../contexts/UIStateContext.js', () => ({
useUIState: () => ({
historyRemountKey: 0,
availableTerminalHeight: 40,
constrainHeight: false,
}),
}));

vi.mock('../../hooks/useTerminalSize.js', () => ({
useTerminalSize: () => ({ columns: 100, rows: 40 }),
}));

vi.mock('../../hooks/useKeypress.js', () => ({ useKeypress: () => {} }));

vi.mock('../../contexts/AgentViewContext.js', () => ({
useAgentViewActions: () => ({ setAgentShellFocused: () => {} }),
}));

const { AgentChatContent } = await import('./AgentChatContent.js');

function makeCore(): AgentCore {
const emitter = new EventEmitter();
return {
getEventEmitter: () => emitter,
getMessages: () => [
{
role: 'assistant',
content: 'hello from the subagent',
timestamp: 0,
},
// An unmatched tool_call maps to a tool_group whose tool is still
// Executing. `splitIndex` keeps such a group (and everything after it)
// in the live area, so this fixture is what makes the second,
// `isPending` render site run at all — without it the live-area
// forwarding is unpinned.
{
role: 'tool_call',
content: 'Tool call: replace',
timestamp: 0,
metadata: {
callId: 'c1',
toolName: 'replace',
description: 'src/a.ts',
args: { file_path: 'src/a.ts' },
},
},
],
getPendingApprovals: () => new Map(),
getLiveOutputs: () => new Map(),
getShellPids: () => new Map(),
runtimeContext: { getTargetDir: () => '' },
modelConfig: { model: 'test-model' },
} as unknown as AgentCore;
}

const renderAt = (allExpanded: boolean) => {
receivedFullDetail.length = 0;
render(
<ThoughtExpandedProvider
value={{
allExpanded,
expandedHeadIds: new Set<number>(),
toggle: () => {},
}}
>
<AgentChatContent core={makeCore()} instanceKey="a@team" />
</ThoughtExpandedProvider>,
);
return receivedFullDetail;
};

describe('AgentChatContent — Ctrl+O full detail', () => {
// Both render sites must be covered: the committed <Static> tree and the
// live area that holds executing/confirming tool groups. Pinning only the
// former let a mutation dropping the live-area prop survive, which would
// leave a running tool group advertising `(ctrl+o)` for a dead key.
const COMMITTED_AND_LIVE = 2;

it('forwards the expanded state to both committed and live items', () => {
const seen = renderAt(true);
expect(seen.length).toBe(COMMITTED_AND_LIVE);
expect(seen).toEqual([true, true]);
});

it('leaves both sites collapsed when the toggle is off', () => {
const seen = renderAt(false);
expect(seen.length).toBe(COMMITTED_AND_LIVE);
expect(seen).toEqual([false, false]);
});
});
Loading
Loading