Merge remote-tracking branch 'origin/main' into feature/status-line-customization

# Conflicts:
#	packages/cli/src/ui/components/Footer.tsx
This commit is contained in:
wenshao 2026-04-08 05:05:04 +08:00
commit 51964fa4b9
49 changed files with 1414 additions and 2370 deletions

View file

@ -143,6 +143,7 @@ describe('useShellCommandProcessor', () => {
status: ToolCallStatus.Executing,
}),
],
isUserInitiated: true,
});
const tmpFile = path.join(os.tmpdir(), 'shell_pwd_abcdef.tmp');
const wrappedCommand = `{ ls -l; }; __code=$?; pwd > "${tmpFile}"; exit $__code`;

View file

@ -131,6 +131,7 @@ export const useShellCommandProcessor = (
setPendingHistoryItem({
type: 'tool_group',
tools: [initialToolDisplay],
isUserInitiated: true,
});
let executionPid: number | undefined;
@ -304,6 +305,7 @@ export const useShellCommandProcessor = (
{
type: 'tool_group',
tools: [finalToolDisplay],
isUserInitiated: true,
} as HistoryItemWithoutId,
userMessageTimestamp,
);

View file

@ -173,6 +173,7 @@ export const useGeminiStream = (
setShellInputFocused: (value: boolean) => void,
terminalWidth: number,
terminalHeight: number,
midTurnDrainRef?: React.RefObject<(() => string[]) | null>,
) => {
const [initError, setInitError] = useState<string | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
@ -1572,6 +1573,23 @@ export const useGeminiStream = (
return;
}
// Mid-turn queue drain: inject queued user messages alongside tool
// results so the model sees them in the next API call.
// Skip if the turn was cancelled — messages stay in queue for next turn.
const drained =
turnCancelledRef.current || abortControllerRef.current?.signal.aborted
? []
: (midTurnDrainRef?.current?.() ?? []);
if (drained.length > 0) {
for (const msg of drained) {
responsesToSend.push({
text: `\n[User message received during tool execution]: ${msg}`,
});
// Record in UI history so the transcript stays complete.
addItem({ type: MessageType.USER, text: msg }, Date.now());
}
}
submitQuery(responsesToSend, SendMessageType.ToolResult, prompt_ids[0]);
},
[
@ -1582,6 +1600,8 @@ export const useGeminiStream = (
performMemoryRefresh,
modelSwitchedFromQuotaError,
config,
midTurnDrainRef,
addItem,
],
);

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { StreamingState } from '../types.js';
export interface UseMessageQueueOptions {
@ -18,6 +18,12 @@ export interface UseMessageQueueReturn {
addMessage: (message: string) => void;
clearQueue: () => void;
getQueuedMessagesText: () => string;
/**
* Atomically drain all queued messages. Returns the drained messages
* and clears both the synchronous ref and React state. Safe to call
* from non-React contexts (e.g., tool completion callbacks).
*/
drainQueue: () => string[];
}
/**
@ -31,17 +37,22 @@ export function useMessageQueue({
submitQuery,
}: UseMessageQueueOptions): UseMessageQueueReturn {
const [messageQueue, setMessageQueue] = useState<string[]>([]);
// Synchronous ref mirrors React state so non-React callbacks (e.g.,
// mid-turn drain in handleCompletedTools) always see the latest queue.
const queueRef = useRef<string[]>([]);
// Add a message to the queue
const addMessage = useCallback((message: string) => {
const trimmedMessage = message.trim();
if (trimmedMessage.length > 0) {
setMessageQueue((prev) => [...prev, trimmedMessage]);
queueRef.current = [...queueRef.current, trimmedMessage];
setMessageQueue(queueRef.current);
}
}, []);
// Clear the entire queue
const clearQueue = useCallback(() => {
queueRef.current = [];
setMessageQueue([]);
}, []);
@ -51,6 +62,15 @@ export function useMessageQueue({
return messageQueue.join('\n\n');
}, [messageQueue]);
// Atomically drain all queued messages (synchronous, safe from callbacks).
const drainQueue = useCallback((): string[] => {
const drained = queueRef.current;
if (drained.length === 0) return [];
queueRef.current = [];
setMessageQueue([]);
return drained;
}, []);
// Process queued messages when streaming becomes idle
useEffect(() => {
if (
@ -61,15 +81,22 @@ export function useMessageQueue({
// Combine all messages with double newlines for clarity
const combinedMessage = messageQueue.join('\n\n');
// Clear the queue and submit
setMessageQueue([]);
clearQueue();
submitQuery(combinedMessage);
}
}, [isConfigInitialized, streamingState, messageQueue, submitQuery]);
}, [
isConfigInitialized,
streamingState,
messageQueue,
submitQuery,
clearQueue,
]);
return {
messageQueue,
addMessage,
clearQueue,
getQueuedMessagesText,
drainQueue,
};
}