fix: address audit findings across status-line and verbose-mode features

- useStatusLine: clamp used/remaining percentage to [0,100], track
  totalLinesRemoved as trigger, clean up debounceRef on unmount
- AppContainer: use drainQueue from useMessageQueue instead of manual
  messageQueueRef to avoid stale-ref reads between renders
- builtin-agents: add WRITE_FILE tool to statusline-setup agent, improve
  PS1 parsing instructions (unquoted assignments, \[/\]/\e escapes),
  strip ANSI colors, remove unreachable symlink instruction
- CompactToolGroupDisplay: fix misleading hint "show full tool output"
  to "toggle verbose mode" across all 6 locales
- AppContainer.test: add missing drainQueue mock
This commit is contained in:
wenshao 2026-04-08 18:45:44 +08:00
parent c36953816c
commit 520ed4e040
11 changed files with 69 additions and 39 deletions

View file

@ -1971,6 +1971,6 @@ export default {
verbose: 'ausführlich',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'Vollständige Tool-Ausgabe und Denkprozess im ausführlichen Modus anzeigen (mit Strg+O umschalten).',
'Press Ctrl+O to show full tool output':
'Strg+O für vollständige Tool-Ausgabe drücken',
'Press Ctrl+O to toggle verbose mode':
'Strg+O zum Umschalten des ausführlichen Modus drücken',
};

View file

@ -2011,6 +2011,5 @@ export default {
verbose: 'verbose',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).',
'Press Ctrl+O to show full tool output':
'Press Ctrl+O to show full tool output',
'Press Ctrl+O to toggle verbose mode': 'Press Ctrl+O to toggle verbose mode',
};

View file

@ -1463,5 +1463,5 @@ export default {
verbose: '詳細',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'詳細モードで完全なツール出力と思考を表示しますCtrl+O で切り替え)。',
'Press Ctrl+O to show full tool output': 'Ctrl+O で完全なツール出力を表示',
'Press Ctrl+O to toggle verbose mode': 'Ctrl+O で詳細モードを切り替え',
};

View file

@ -1961,6 +1961,6 @@ export default {
verbose: 'detalhado',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'Mostrar saída completa da ferramenta e raciocínio no modo detalhado (alternar com Ctrl+O).',
'Press Ctrl+O to show full tool output':
'Pressione Ctrl+O para exibir a saída completa da ferramenta',
'Press Ctrl+O to toggle verbose mode':
'Pressione Ctrl+O para alternar o modo detalhado',
};

View file

@ -1968,6 +1968,6 @@ export default {
verbose: 'подробный',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'Показывать полный вывод инструментов и процесс рассуждений в подробном режиме (переключить с помощью Ctrl+O).',
'Press Ctrl+O to show full tool output':
'Нажмите Ctrl+O для показа полного вывода инструментов',
'Press Ctrl+O to toggle verbose mode':
'Нажмите Ctrl+O для переключения подробного режима',
};

View file

@ -1816,5 +1816,5 @@ export default {
verbose: '详细',
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
'详细模式下显示完整工具输出和思考过程Ctrl+O 切换)。',
'Press Ctrl+O to show full tool output': '按 Ctrl+O 查看详细工具调用结果',
'Press Ctrl+O to toggle verbose mode': '按 Ctrl+O 切换详细模式',
};

View file

@ -243,6 +243,7 @@ describe('AppContainer State Management', () => {
addMessage: vi.fn(),
clearQueue: vi.fn(),
getQueuedMessagesText: vi.fn().mockReturnValue(''),
drainQueue: vi.fn().mockReturnValue([]),
});
mockedUseAutoAcceptIndicator.mockReturnValue(false);
mockedUseGitBranchName.mockReturnValue('main');
@ -455,6 +456,7 @@ describe('AppContainer State Management', () => {
addMessage: mockQueueMessage,
clearQueue: vi.fn(),
getQueuedMessagesText: vi.fn().mockReturnValue(''),
drainQueue: vi.fn().mockReturnValue([]),
});
render(

View file

@ -776,24 +776,22 @@ export const AppContainer = (props: AppContainerProps) => {
disabled: agentViewState.activeView !== 'main',
});
const { messageQueue, addMessage, clearQueue, getQueuedMessagesText } =
useMessageQueue({
isConfigInitialized,
streamingState,
submitQuery,
});
const {
messageQueue,
addMessage,
clearQueue,
getQueuedMessagesText,
drainQueue,
} = useMessageQueue({
isConfigInitialized,
streamingState,
submitQuery,
});
// Bridge message queue to mid-turn drain via ref.
// Sync ref on every render so the drain callback always reads latest state.
const messageQueueRef = useRef(messageQueue);
messageQueueRef.current = messageQueue;
midTurnDrainRef.current = () => {
const queue = messageQueueRef.current;
if (queue.length === 0) return [];
messageQueueRef.current = [];
clearQueue();
return [...queue];
};
// drainQueue reads from the synchronous queueRef inside useMessageQueue,
// so it always sees the latest state even between renders.
midTurnDrainRef.current = drainQueue;
// Callback for handling final submit (must be after addMessage from useMessageQueue)
const handleFinalSubmit = useCallback(

View file

@ -101,7 +101,7 @@ export const CompactToolGroupDisplay: React.FC<
{/* Hint line */}
<Text color={theme.text.secondary}>
{t('Press Ctrl+O to show full tool output')}
{t('Press Ctrl+O to toggle verbose mode')}
</Text>
</Box>
);

View file

@ -175,6 +175,8 @@ export function useStatusLine(): {
const { currentModel, branchName } = uiState;
const totalToolCalls = uiState.sessionStats.metrics.tools.totalCalls;
const totalLinesAdded = uiState.sessionStats.metrics.files.totalLinesAdded;
const totalLinesRemoved =
uiState.sessionStats.metrics.files.totalLinesRemoved;
const effectiveVim = vimEnabled ? vimMode : undefined;
const prevStateRef = useRef<{
promptTokenCount: number;
@ -183,6 +185,7 @@ export function useStatusLine(): {
branchName: string | undefined;
totalToolCalls: number;
totalLinesAdded: number;
totalLinesRemoved: number;
}>({
promptTokenCount: lastPromptTokenCount,
currentModel,
@ -190,6 +193,7 @@ export function useStatusLine(): {
branchName,
totalToolCalls,
totalLinesAdded,
totalLinesRemoved,
});
// Guard: when true, the mount effect has already called doUpdate so the
@ -216,8 +220,15 @@ export function useStatusLine(): {
cfg.getContentGeneratorConfig()?.contextWindowSize || 0;
const usedPercentage =
contextWindowSize > 0
? Math.round((stats.lastPromptTokenCount / contextWindowSize) * 1000) /
10
? Math.min(
100,
Math.max(
0,
Math.round(
(stats.lastPromptTokenCount / contextWindowSize) * 1000,
) / 10,
),
)
: 0;
let totalInputTokens = 0;
@ -238,9 +249,15 @@ export function useStatusLine(): {
used_percentage: usedPercentage,
remaining_percentage:
contextWindowSize > 0
? Math.round(
(1 - stats.lastPromptTokenCount / contextWindowSize) * 1000,
) / 10
? Math.min(
100,
Math.max(
0,
Math.round(
(1 - stats.lastPromptTokenCount / contextWindowSize) * 1000,
) / 10,
),
)
: 100,
current_usage: stats.lastPromptTokenCount,
total_input_tokens: totalInputTokens,
@ -256,7 +273,7 @@ export function useStatusLine(): {
}),
metrics: buildMetricsPayload(m),
...(vimEnabledRef.current && {
vim: { mode: vimModeRef.current ?? 'INSERT' },
vim: { mode: vimModeRef.current },
}),
};
@ -328,7 +345,8 @@ export function useStatusLine(): {
effectiveVim !== prev.effectiveVim ||
branchName !== prev.branchName ||
totalToolCalls !== prev.totalToolCalls ||
totalLinesAdded !== prev.totalLinesAdded
totalLinesAdded !== prev.totalLinesAdded ||
totalLinesRemoved !== prev.totalLinesRemoved
) {
prev.promptTokenCount = lastPromptTokenCount;
prev.currentModel = currentModel;
@ -336,6 +354,7 @@ export function useStatusLine(): {
prev.branchName = branchName;
prev.totalToolCalls = totalToolCalls;
prev.totalLinesAdded = totalLinesAdded;
prev.totalLinesRemoved = totalLinesRemoved;
scheduleUpdate();
}
}, [
@ -346,6 +365,7 @@ export function useStatusLine(): {
branchName,
totalToolCalls,
totalLinesAdded,
totalLinesRemoved,
scheduleUpdate,
]);
@ -379,6 +399,7 @@ export function useStatusLine(): {
genRef.current++;
if (debounceRef.current !== undefined) {
clearTimeout(debounceRef.current);
debounceRef.current = undefined;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps

View file

@ -104,7 +104,12 @@ Notes:
name: 'statusline-setup',
description:
"Use this agent to configure the user's Qwen Code status line setting.",
tools: [ToolNames.READ_FILE, ToolNames.EDIT, ToolNames.ASK_USER_QUESTION],
tools: [
ToolNames.READ_FILE,
ToolNames.WRITE_FILE,
ToolNames.EDIT,
ToolNames.ASK_USER_QUESTION,
],
color: 'orange',
systemPrompt: `You are a status line setup agent for Qwen Code. Your job is to create or update the statusLine command in the user's Qwen Code settings.
@ -115,7 +120,12 @@ When asked to convert the user's shell PS1 configuration, follow these steps:
- ~/.bash_profile
- ~/.profile
2. Extract the PS1 value using this regex pattern: /(?:^|\\n)\\s*(?:export\\s+)?PS1\\s*=\\s*["']([^"']+)["']/m
2. Look for PS1 assignments. PS1 may be quoted or unquoted, e.g.:
- PS1="\\u@\\h:\\w\\$ "
- PS1='\\u@\\h:\\w\\$ '
- PS1=\\u@\\h:\\w\\$
- export PS1="..."
If there are multiple PS1 assignments, use the last one (it takes effect).
3. Convert PS1 escape sequences to shell commands:
- \\u $(whoami)
@ -130,8 +140,10 @@ When asked to convert the user's shell PS1 configuration, follow these steps:
- \\@ $(date +%I:%M%p)
- \\# #
- \\! !
- \\[ and \\] (remove these are readline non-printing markers, not needed in the status line)
- \\e or \\033 (ANSI escape strip the entire color sequence including \\e[...m)
4. When using ANSI color codes, be sure to use \`printf\`. Do not remove colors. Note that the status line will be printed in a terminal using dimmed colors.
4. Strip ANSI color/escape sequences from the PS1 output. The status line already renders in dimmed color, so PS1 colors are not useful and can produce garbled output.
5. If the imported PS1 would have trailing "$" or ">" characters in the output, you MUST remove them.
@ -198,8 +210,6 @@ How to use the statusLine command:
}
Make sure to preserve any existing "ui" settings (theme, etc.) when updating.
4. If ~/.qwen/settings.json is a symlink, update the target file instead.
Guidelines:
- The status line only displays the first line of stdout ensure commands produce exactly one line of output
- Preserve existing settings when updating