Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions core/config/sharedConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const sharedConfigSchema = z
displayRawMarkdown: z.boolean(),
showChatScrollbar: z.boolean(),
autoAcceptEditToolDiffs: z.boolean(),
continueAfterToolRejection: z.boolean(),

// `tabAutocompleteOptions` in `ContinueConfig`
useAutocompleteCache: z.boolean(),
Expand Down Expand Up @@ -158,6 +159,11 @@ export function modifyAnyConfigWithSharedConfig<
configCopy.ui.showSessionTabs = sharedConfig.showSessionTabs;
}

if (sharedConfig.continueAfterToolRejection !== undefined) {
configCopy.ui.continueAfterToolRejection =
sharedConfig.continueAfterToolRejection;
}

configCopy.experimental = {
...configCopy.experimental,
};
Expand Down
1 change: 1 addition & 0 deletions core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,7 @@ export interface ContinueUIConfig {
codeWrap?: boolean;
showSessionTabs?: boolean;
autoAcceptEditToolDiffs?: boolean;
continueAfterToolRejection?: boolean;
}

export interface ContextMenuConfig {
Expand Down
12 changes: 12 additions & 0 deletions gui/src/pages/config/UserSettingsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export function UserSettingsForm() {

// TODO defaults are in multiple places, should be consolidated and probably not explicit here
const showSessionTabs = config.ui?.showSessionTabs ?? false;
const continueAfterToolRejection =
config.ui?.continueAfterToolRejection ?? false;
const codeWrap = config.ui?.codeWrap ?? false;
const showChatScrollbar = config.ui?.showChatScrollbar ?? false;
const readResponseTTS = config.experimental?.readResponseTTS ?? false;
Expand Down Expand Up @@ -449,6 +451,16 @@ export function UserSettingsForm() {
text="@Codebase: use tool calling only"
/>

<ToggleSwitch
isToggled={continueAfterToolRejection}
onToggle={() =>
handleUpdate({
continueAfterToolRejection: !continueAfterToolRejection,
})
}
text="Stream after tool rejection"
/>

{hasContinueEmail && (
<ContinueFeaturesMenu
enableStaticContextualization={
Expand Down
33 changes: 32 additions & 1 deletion gui/src/redux/thunks/cancelToolCall.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import posthog from "posthog-js";
import { cancelToolCall as cancelToolCallAction } from "../slices/sessionSlice";
import {
cancelToolCall as cancelToolCallAction,
updateToolCallOutput,
} from "../slices/sessionSlice";
import { ThunkApiType } from "../store";
import { findToolCallById } from "../util";
import { streamResponseAfterToolCall } from "./streamResponseAfterToolCall";

const DEFAULT_USER_REJECTION_MESSAGE = `The user skipped the tool call.
If the tool call is optional or non-critical to the main goal, skip it and continue with the next step.
If the tool call is essential, try an alternative approach.
If no alternatives exist, offer to pause here.`;

export const cancelToolCallThunk = createAsyncThunk<
void,
{ toolCallId: string },
ThunkApiType
>("chat/cancelToolCall", async ({ toolCallId }, { dispatch, getState }) => {
const state = getState();
const continueAfterToolRejection =
state.config.config.ui?.continueAfterToolRejection;
const toolCallState = findToolCallById(state.session.history, toolCallId);

if (toolCallState) {
Expand All @@ -21,6 +32,26 @@ export const cancelToolCallThunk = createAsyncThunk<
});
}

if (continueAfterToolRejection) {
// Update tool call output with rejection message
dispatch(
updateToolCallOutput({
toolCallId,
contextItems: [
{
icon: "problems",
name: "Tool Call Rejected",
description: "User skipped the tool call",
content: DEFAULT_USER_REJECTION_MESSAGE,
hidden: true,
},
],
}),
);
}

// Dispatch the actual cancel action
dispatch(cancelToolCallAction({ toolCallId }));

void dispatch(streamResponseAfterToolCall({ toolCallId }));
});
13 changes: 11 additions & 2 deletions gui/src/redux/thunks/streamResponseAfterToolCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { streamThunkWrapper } from "./streamThunkWrapper";
*/
function areAllToolsDoneStreaming(
assistantMessage: ChatHistoryItemWithMessageId | undefined,
continueAfterToolRejection: boolean | undefined,
): boolean {
// This might occur because of race conditions, if so, the tools are completed
if (!assistantMessage?.toolCallStates) {
Expand All @@ -24,7 +25,10 @@ function areAllToolsDoneStreaming(

// Only continue if all tool calls are complete
const completedToolCalls = assistantMessage.toolCallStates.filter(
(tc) => tc.status === "done" || tc.status === "errored",
(tc) =>
tc.status === "done" ||
tc.status === "errored" ||
(continueAfterToolRejection && tc.status === "canceled"),
);

return completedToolCalls.length === assistantMessage.toolCallStates.length;
Expand Down Expand Up @@ -71,7 +75,12 @@ export const streamResponseAfterToolCall = createAsyncThunk<
item.toolCallStates?.some((tc) => tc.toolCallId === toolCallId),
);

if (areAllToolsDoneStreaming(assistantMessage)) {
if (
areAllToolsDoneStreaming(
assistantMessage,
state.config.config.ui?.continueAfterToolRejection,
)
) {
unwrapResult(await dispatch(streamNormalInput({})));
}
}),
Expand Down
Loading