summaryrefslogtreecommitdiff
path: root/packages/cli/src/core/history-updater.ts
diff options
context:
space:
mode:
authorJaana Dogan <[email protected]>2025-04-17 12:03:02 -0700
committerN. Taylor Mullen <[email protected]>2025-04-17 14:15:20 -0700
commit81ba61df7f967f141cd8abc57d58a3612dfd4c2b (patch)
treee4cf1a244c0b79fb7167105672c037954144783c /packages/cli/src/core/history-updater.ts
parent898a83031c695f6da8705848ebe9998a7b626019 (diff)
Improve readability issues
This is only the first change of many changes. * Remove redundant autogenerated comments * Use the recommended file name style * Use camelCase for variable names * Don't introduce submodules for relevant types * Don't introduce constants like modules, these are implementation details * Remove empty files
Diffstat (limited to 'packages/cli/src/core/history-updater.ts')
-rw-r--r--packages/cli/src/core/history-updater.ts168
1 files changed, 168 insertions, 0 deletions
diff --git a/packages/cli/src/core/history-updater.ts b/packages/cli/src/core/history-updater.ts
new file mode 100644
index 00000000..4013728f
--- /dev/null
+++ b/packages/cli/src/core/history-updater.ts
@@ -0,0 +1,168 @@
+import { Part } from "@google/genai";
+import { toolRegistry } from "../tools/tool-registry.js";
+import { HistoryItem, IndividualToolCallDisplay, ToolCallEvent, ToolCallStatus, ToolConfirmationOutcome, ToolEditConfirmationDetails, ToolExecuteConfirmationDetails } from "../ui/types.js";
+import { ToolResultDisplay } from "../tools/tool.js";
+
+/**
+ * Processes a tool call chunk and updates the history state accordingly.
+ * Manages adding new tool groups or updating existing ones.
+ * Resides here as its primary effect is updating history based on tool events.
+ */
+export const handleToolCallChunk = (
+ chunk: ToolCallEvent,
+ setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
+ submitQuery: (query: Part) => Promise<void>,
+ getNextMessageId: () => number,
+ currentToolGroupIdRef: React.MutableRefObject<number | null>
+): void => {
+ const toolDefinition = toolRegistry.getTool(chunk.name);
+ const description = toolDefinition?.getDescription
+ ? toolDefinition.getDescription(chunk.args)
+ : '';
+ const toolDisplayName = toolDefinition?.displayName ?? chunk.name;
+ let confirmationDetails = chunk.confirmationDetails;
+ if (confirmationDetails) {
+ const originalConfirmationDetails = confirmationDetails;
+ const historyUpdatingConfirm = async (outcome: ToolConfirmationOutcome) => {
+ originalConfirmationDetails.onConfirm(outcome);
+
+ if (outcome === ToolConfirmationOutcome.Cancel) {
+ let resultDisplay: ToolResultDisplay | undefined;
+ if ('fileDiff' in originalConfirmationDetails) {
+ resultDisplay = { fileDiff: (originalConfirmationDetails as ToolEditConfirmationDetails).fileDiff };
+ } else {
+ resultDisplay = `~~${(originalConfirmationDetails as ToolExecuteConfirmationDetails).command}~~`;
+ }
+ handleToolCallChunk({ ...chunk, status: ToolCallStatus.Canceled, confirmationDetails: undefined, resultDisplay, }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef);
+ const functionResponse: Part = {
+ functionResponse: {
+ name: chunk.name,
+ response: { "error": "User rejected function call." },
+ },
+ }
+ await submitQuery(functionResponse);
+ } else {
+ const tool = toolRegistry.getTool(chunk.name)
+ if (!tool) {
+ throw new Error(`Tool "${chunk.name}" not found or is not registered.`);
+ }
+ handleToolCallChunk({ ...chunk, status: ToolCallStatus.Invoked, resultDisplay: "Executing...", confirmationDetails: undefined }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef);
+ const result = await tool.execute(chunk.args);
+ handleToolCallChunk({ ...chunk, status: ToolCallStatus.Invoked, resultDisplay: result.returnDisplay, confirmationDetails: undefined }, setHistory, submitQuery, getNextMessageId, currentToolGroupIdRef);
+ const functionResponse: Part = {
+ functionResponse: {
+ name: chunk.name,
+ id: chunk.callId,
+ response: { "output": result.llmContent },
+ },
+ }
+ await submitQuery(functionResponse);
+ }
+ }
+
+ confirmationDetails = {
+ ...originalConfirmationDetails,
+ onConfirm: historyUpdatingConfirm,
+ };
+ }
+ const toolDetail: IndividualToolCallDisplay = {
+ callId: chunk.callId,
+ name: toolDisplayName,
+ description,
+ resultDisplay: chunk.resultDisplay,
+ status: chunk.status,
+ confirmationDetails: confirmationDetails,
+ };
+
+ const activeGroupId = currentToolGroupIdRef.current;
+ setHistory(prev => {
+ if (chunk.status === ToolCallStatus.Pending) {
+ if (activeGroupId === null) {
+ // Start a new tool group
+ const newGroupId = getNextMessageId();
+ currentToolGroupIdRef.current = newGroupId;
+ return [
+ ...prev,
+ { id: newGroupId, type: 'tool_group', tools: [toolDetail] } as HistoryItem
+ ];
+ }
+
+ // Add to existing tool group
+ return prev.map(item =>
+ item.id === activeGroupId && item.type === 'tool_group'
+ ? item.tools.some(t => t.callId === toolDetail.callId)
+ ? item // Tool already listed as pending
+ : { ...item, tools: [...item.tools, toolDetail] }
+ : item
+ );
+ }
+
+ // Update the status of a pending tool within the active group
+ if (activeGroupId === null) {
+ // Log if an invoked tool arrives without an active group context
+ console.warn("Received invoked tool status without an active tool group ID:", chunk);
+ return prev;
+ }
+
+ return prev.map(item =>
+ item.id === activeGroupId && item.type === 'tool_group'
+ ? {
+ ...item,
+ tools: item.tools.map(t =>
+ t.callId === toolDetail.callId
+ ? { ...t, ...toolDetail, status: chunk.status } // Update details & status
+ : t
+ )
+ }
+ : item
+ );
+ });
+};
+
+/**
+ * Appends an error or informational message to the history, attempting to attach
+ * it to the last non-user message or creating a new entry.
+ */
+export const addErrorMessageToHistory = (
+ error: any,
+ setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
+ getNextMessageId: () => number
+): void => {
+ const isAbort = error.name === 'AbortError';
+ const errorType = isAbort ? 'info' : 'error';
+ const errorText = isAbort
+ ? '[Request cancelled by user]'
+ : `[Error: ${error.message || 'Unknown error'}]`;
+
+ setHistory(prev => {
+ const reversedHistory = [...prev].reverse();
+ // Find the last message that isn't from the user to append the error/info to
+ const lastBotMessageIndex = reversedHistory.findIndex(item => item.type !== 'user');
+ const originalIndex = lastBotMessageIndex !== -1 ? prev.length - 1 - lastBotMessageIndex : -1;
+
+ if (originalIndex !== -1) {
+ // Append error to the last relevant message
+ return prev.map((item, index) => {
+ if (index === originalIndex) {
+ let baseText = '';
+ // Determine base text based on item type
+ if (item.type === 'gemini') baseText = item.text ?? '';
+ else if (item.type === 'tool_group') baseText = `Tool execution (${item.tools.length} calls)`;
+ else if (item.type === 'error' || item.type === 'info') baseText = item.text ?? '';
+ // Safely handle potential undefined text
+
+ const updatedText = (baseText + (baseText && !baseText.endsWith('\n') ? '\n' : '') + errorText).trim();
+ // Reuse existing ID, update type and text
+ return { ...item, type: errorType, text: updatedText };
+ }
+ return item;
+ });
+ } else {
+ // No previous message to append to, add a new error item
+ return [
+ ...prev,
+ { id: getNextMessageId(), type: errorType, text: errorText } as HistoryItem
+ ];
+ }
+ });
+}; \ No newline at end of file