mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
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:
parent
c36953816c
commit
520ed4e040
11 changed files with 69 additions and 39 deletions
|
|
@ -1971,6 +1971,6 @@ export default {
|
||||||
verbose: 'ausführlich',
|
verbose: 'ausführlich',
|
||||||
'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).':
|
||||||
'Vollständige Tool-Ausgabe und Denkprozess im ausführlichen Modus anzeigen (mit Strg+O umschalten).',
|
'Vollständige Tool-Ausgabe und Denkprozess im ausführlichen Modus anzeigen (mit Strg+O umschalten).',
|
||||||
'Press Ctrl+O to show full tool output':
|
'Press Ctrl+O to toggle verbose mode':
|
||||||
'Strg+O für vollständige Tool-Ausgabe drücken',
|
'Strg+O zum Umschalten des ausführlichen Modus drücken',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2011,6 +2011,5 @@ export default {
|
||||||
verbose: 'verbose',
|
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).':
|
||||||
'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 toggle verbose mode': 'Press Ctrl+O to toggle verbose mode',
|
||||||
'Press Ctrl+O to show full tool output',
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1463,5 +1463,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).':
|
||||||
'詳細モードで完全なツール出力と思考を表示します(Ctrl+O で切り替え)。',
|
'詳細モードで完全なツール出力と思考を表示します(Ctrl+O で切り替え)。',
|
||||||
'Press Ctrl+O to show full tool output': 'Ctrl+O で完全なツール出力を表示',
|
'Press Ctrl+O to toggle verbose mode': 'Ctrl+O で詳細モードを切り替え',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1961,6 +1961,6 @@ export default {
|
||||||
verbose: 'detalhado',
|
verbose: 'detalhado',
|
||||||
'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).':
|
||||||
'Mostrar saída completa da ferramenta e raciocínio no modo detalhado (alternar com 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':
|
'Press Ctrl+O to toggle verbose mode':
|
||||||
'Pressione Ctrl+O para exibir a saída completa da ferramenta',
|
'Pressione Ctrl+O para alternar o modo detalhado',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1968,6 +1968,6 @@ 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).':
|
||||||
'Показывать полный вывод инструментов и процесс рассуждений в подробном режиме (переключить с помощью Ctrl+O).',
|
'Показывать полный вывод инструментов и процесс рассуждений в подробном режиме (переключить с помощью Ctrl+O).',
|
||||||
'Press Ctrl+O to show full tool output':
|
'Press Ctrl+O to toggle verbose mode':
|
||||||
'Нажмите Ctrl+O для показа полного вывода инструментов',
|
'Нажмите Ctrl+O для переключения подробного режима',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1816,5 +1816,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).':
|
||||||
'详细模式下显示完整工具输出和思考过程(Ctrl+O 切换)。',
|
'详细模式下显示完整工具输出和思考过程(Ctrl+O 切换)。',
|
||||||
'Press Ctrl+O to show full tool output': '按 Ctrl+O 查看详细工具调用结果',
|
'Press Ctrl+O to toggle verbose mode': '按 Ctrl+O 切换详细模式',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,7 @@ describe('AppContainer State Management', () => {
|
||||||
addMessage: vi.fn(),
|
addMessage: vi.fn(),
|
||||||
clearQueue: vi.fn(),
|
clearQueue: vi.fn(),
|
||||||
getQueuedMessagesText: vi.fn().mockReturnValue(''),
|
getQueuedMessagesText: vi.fn().mockReturnValue(''),
|
||||||
|
drainQueue: vi.fn().mockReturnValue([]),
|
||||||
});
|
});
|
||||||
mockedUseAutoAcceptIndicator.mockReturnValue(false);
|
mockedUseAutoAcceptIndicator.mockReturnValue(false);
|
||||||
mockedUseGitBranchName.mockReturnValue('main');
|
mockedUseGitBranchName.mockReturnValue('main');
|
||||||
|
|
@ -455,6 +456,7 @@ describe('AppContainer State Management', () => {
|
||||||
addMessage: mockQueueMessage,
|
addMessage: mockQueueMessage,
|
||||||
clearQueue: vi.fn(),
|
clearQueue: vi.fn(),
|
||||||
getQueuedMessagesText: vi.fn().mockReturnValue(''),
|
getQueuedMessagesText: vi.fn().mockReturnValue(''),
|
||||||
|
drainQueue: vi.fn().mockReturnValue([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
|
|
||||||
|
|
@ -776,24 +776,22 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||||
disabled: agentViewState.activeView !== 'main',
|
disabled: agentViewState.activeView !== 'main',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { messageQueue, addMessage, clearQueue, getQueuedMessagesText } =
|
const {
|
||||||
useMessageQueue({
|
messageQueue,
|
||||||
|
addMessage,
|
||||||
|
clearQueue,
|
||||||
|
getQueuedMessagesText,
|
||||||
|
drainQueue,
|
||||||
|
} = useMessageQueue({
|
||||||
isConfigInitialized,
|
isConfigInitialized,
|
||||||
streamingState,
|
streamingState,
|
||||||
submitQuery,
|
submitQuery,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bridge message queue to mid-turn drain via ref.
|
// Bridge message queue to mid-turn drain via ref.
|
||||||
// Sync ref on every render so the drain callback always reads latest state.
|
// drainQueue reads from the synchronous queueRef inside useMessageQueue,
|
||||||
const messageQueueRef = useRef(messageQueue);
|
// so it always sees the latest state even between renders.
|
||||||
messageQueueRef.current = messageQueue;
|
midTurnDrainRef.current = drainQueue;
|
||||||
midTurnDrainRef.current = () => {
|
|
||||||
const queue = messageQueueRef.current;
|
|
||||||
if (queue.length === 0) return [];
|
|
||||||
messageQueueRef.current = [];
|
|
||||||
clearQueue();
|
|
||||||
return [...queue];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Callback for handling final submit (must be after addMessage from useMessageQueue)
|
// Callback for handling final submit (must be after addMessage from useMessageQueue)
|
||||||
const handleFinalSubmit = useCallback(
|
const handleFinalSubmit = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ export const CompactToolGroupDisplay: React.FC<
|
||||||
|
|
||||||
{/* Hint line */}
|
{/* Hint line */}
|
||||||
<Text color={theme.text.secondary}>
|
<Text color={theme.text.secondary}>
|
||||||
{t('Press Ctrl+O to show full tool output')}
|
{t('Press Ctrl+O to toggle verbose mode')}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,8 @@ export function useStatusLine(): {
|
||||||
const { currentModel, branchName } = uiState;
|
const { currentModel, branchName } = uiState;
|
||||||
const totalToolCalls = uiState.sessionStats.metrics.tools.totalCalls;
|
const totalToolCalls = uiState.sessionStats.metrics.tools.totalCalls;
|
||||||
const totalLinesAdded = uiState.sessionStats.metrics.files.totalLinesAdded;
|
const totalLinesAdded = uiState.sessionStats.metrics.files.totalLinesAdded;
|
||||||
|
const totalLinesRemoved =
|
||||||
|
uiState.sessionStats.metrics.files.totalLinesRemoved;
|
||||||
const effectiveVim = vimEnabled ? vimMode : undefined;
|
const effectiveVim = vimEnabled ? vimMode : undefined;
|
||||||
const prevStateRef = useRef<{
|
const prevStateRef = useRef<{
|
||||||
promptTokenCount: number;
|
promptTokenCount: number;
|
||||||
|
|
@ -183,6 +185,7 @@ export function useStatusLine(): {
|
||||||
branchName: string | undefined;
|
branchName: string | undefined;
|
||||||
totalToolCalls: number;
|
totalToolCalls: number;
|
||||||
totalLinesAdded: number;
|
totalLinesAdded: number;
|
||||||
|
totalLinesRemoved: number;
|
||||||
}>({
|
}>({
|
||||||
promptTokenCount: lastPromptTokenCount,
|
promptTokenCount: lastPromptTokenCount,
|
||||||
currentModel,
|
currentModel,
|
||||||
|
|
@ -190,6 +193,7 @@ export function useStatusLine(): {
|
||||||
branchName,
|
branchName,
|
||||||
totalToolCalls,
|
totalToolCalls,
|
||||||
totalLinesAdded,
|
totalLinesAdded,
|
||||||
|
totalLinesRemoved,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Guard: when true, the mount effect has already called doUpdate so the
|
// Guard: when true, the mount effect has already called doUpdate so the
|
||||||
|
|
@ -216,8 +220,15 @@ export function useStatusLine(): {
|
||||||
cfg.getContentGeneratorConfig()?.contextWindowSize || 0;
|
cfg.getContentGeneratorConfig()?.contextWindowSize || 0;
|
||||||
const usedPercentage =
|
const usedPercentage =
|
||||||
contextWindowSize > 0
|
contextWindowSize > 0
|
||||||
? Math.round((stats.lastPromptTokenCount / contextWindowSize) * 1000) /
|
? Math.min(
|
||||||
10
|
100,
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Math.round(
|
||||||
|
(stats.lastPromptTokenCount / contextWindowSize) * 1000,
|
||||||
|
) / 10,
|
||||||
|
),
|
||||||
|
)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
let totalInputTokens = 0;
|
let totalInputTokens = 0;
|
||||||
|
|
@ -238,9 +249,15 @@ export function useStatusLine(): {
|
||||||
used_percentage: usedPercentage,
|
used_percentage: usedPercentage,
|
||||||
remaining_percentage:
|
remaining_percentage:
|
||||||
contextWindowSize > 0
|
contextWindowSize > 0
|
||||||
? Math.round(
|
? Math.min(
|
||||||
|
100,
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Math.round(
|
||||||
(1 - stats.lastPromptTokenCount / contextWindowSize) * 1000,
|
(1 - stats.lastPromptTokenCount / contextWindowSize) * 1000,
|
||||||
) / 10
|
) / 10,
|
||||||
|
),
|
||||||
|
)
|
||||||
: 100,
|
: 100,
|
||||||
current_usage: stats.lastPromptTokenCount,
|
current_usage: stats.lastPromptTokenCount,
|
||||||
total_input_tokens: totalInputTokens,
|
total_input_tokens: totalInputTokens,
|
||||||
|
|
@ -256,7 +273,7 @@ export function useStatusLine(): {
|
||||||
}),
|
}),
|
||||||
metrics: buildMetricsPayload(m),
|
metrics: buildMetricsPayload(m),
|
||||||
...(vimEnabledRef.current && {
|
...(vimEnabledRef.current && {
|
||||||
vim: { mode: vimModeRef.current ?? 'INSERT' },
|
vim: { mode: vimModeRef.current },
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -328,7 +345,8 @@ export function useStatusLine(): {
|
||||||
effectiveVim !== prev.effectiveVim ||
|
effectiveVim !== prev.effectiveVim ||
|
||||||
branchName !== prev.branchName ||
|
branchName !== prev.branchName ||
|
||||||
totalToolCalls !== prev.totalToolCalls ||
|
totalToolCalls !== prev.totalToolCalls ||
|
||||||
totalLinesAdded !== prev.totalLinesAdded
|
totalLinesAdded !== prev.totalLinesAdded ||
|
||||||
|
totalLinesRemoved !== prev.totalLinesRemoved
|
||||||
) {
|
) {
|
||||||
prev.promptTokenCount = lastPromptTokenCount;
|
prev.promptTokenCount = lastPromptTokenCount;
|
||||||
prev.currentModel = currentModel;
|
prev.currentModel = currentModel;
|
||||||
|
|
@ -336,6 +354,7 @@ export function useStatusLine(): {
|
||||||
prev.branchName = branchName;
|
prev.branchName = branchName;
|
||||||
prev.totalToolCalls = totalToolCalls;
|
prev.totalToolCalls = totalToolCalls;
|
||||||
prev.totalLinesAdded = totalLinesAdded;
|
prev.totalLinesAdded = totalLinesAdded;
|
||||||
|
prev.totalLinesRemoved = totalLinesRemoved;
|
||||||
scheduleUpdate();
|
scheduleUpdate();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|
@ -346,6 +365,7 @@ export function useStatusLine(): {
|
||||||
branchName,
|
branchName,
|
||||||
totalToolCalls,
|
totalToolCalls,
|
||||||
totalLinesAdded,
|
totalLinesAdded,
|
||||||
|
totalLinesRemoved,
|
||||||
scheduleUpdate,
|
scheduleUpdate,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -379,6 +399,7 @@ export function useStatusLine(): {
|
||||||
genRef.current++;
|
genRef.current++;
|
||||||
if (debounceRef.current !== undefined) {
|
if (debounceRef.current !== undefined) {
|
||||||
clearTimeout(debounceRef.current);
|
clearTimeout(debounceRef.current);
|
||||||
|
debounceRef.current = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,12 @@ Notes:
|
||||||
name: 'statusline-setup',
|
name: 'statusline-setup',
|
||||||
description:
|
description:
|
||||||
"Use this agent to configure the user's Qwen Code status line setting.",
|
"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',
|
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.
|
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
|
- ~/.bash_profile
|
||||||
- ~/.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:
|
3. Convert PS1 escape sequences to shell commands:
|
||||||
- \\u → $(whoami)
|
- \\u → $(whoami)
|
||||||
|
|
@ -130,8 +140,10 @@ When asked to convert the user's shell PS1 configuration, follow these steps:
|
||||||
- \\@ → $(date +%I:%M%p)
|
- \\@ → $(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.
|
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.
|
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:
|
Guidelines:
|
||||||
- The status line only displays the first line of stdout — ensure commands produce exactly one line of output
|
- The status line only displays the first line of stdout — ensure commands produce exactly one line of output
|
||||||
- Preserve existing settings when updating
|
- Preserve existing settings when updating
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue