Skip to content
Closed
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
8 changes: 8 additions & 0 deletions core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,13 @@ export interface ToolResultChatMessage {
toolCallId: string;
}

export interface ToolResultChatMessageWithId {
id: string,
role: "tool";
content: string;
toolCallId: string;
}

export interface UserChatMessage {
role: "user";
content: MessageContent;
Expand All @@ -362,6 +369,7 @@ export interface AssistantChatMessage {
role: "assistant";
content: MessageContent;
toolCalls?: ToolCallDelta[];
toolCallId?: string,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toolCallId shouldn't be on user chat message since one user message can have multiple tool calls, should already be within each toolcalldelta

}

export interface SystemChatMessage {
Expand Down
4 changes: 2 additions & 2 deletions gui/src/components/mainInput/Lump/LumpToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { selectCurrentToolCall } from "../../../redux/selectors/selectCurrentToolCall";
import { callCurrentTool } from "../../../redux/thunks/callCurrentTool";
import { cancelCurrentToolCall } from "../../../redux/thunks/cancelCurrentToolCall";
import { cancelStream } from "../../../redux/thunks/cancelStream";
import { cancelButton } from "../../../redux/thunks/cancelButton";
import {
getAltKeyLabel,
getFontSize,
Expand Down Expand Up @@ -96,7 +96,7 @@ export function LumpToolbar() {
<StopButton
className="text-gray-400"
onClick={() => {
dispatch(cancelStream());
dispatch(cancelButton());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semantic nitpick, thinking cancelStream makes more sense as the action performed, and since dispatched on keyboard events not just button click. also reduces file changes

}}
>
{/* JetBrains overrides cmd+backspace, so we have to use another shortcut */}
Expand Down
4 changes: 2 additions & 2 deletions gui/src/pages/gui/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import {
setDialogMessage,
setShowDialog,
} from "../../redux/slices/uiSlice";
import { cancelStream } from "../../redux/thunks/cancelStream";
import { cancelButton } from "../../redux/thunks/cancelButton";
import { streamEditThunk } from "../../redux/thunks/editMode";
import { loadLastSession } from "../../redux/thunks/session";
import { streamResponseThunk } from "../../redux/thunks/streamResponse";
Expand Down Expand Up @@ -142,7 +142,7 @@ export function Chat() {
(jetbrains ? e.altKey : isMetaEquivalentKeyPressed(e)) &&
!e.shiftKey
) {
dispatch(cancelStream());
dispatch(cancelButton());
}
};
window.addEventListener("keydown", listener);
Expand Down
56 changes: 39 additions & 17 deletions gui/src/redux/slices/sessionSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { renderChatMessage } from "core/util/messageContent";
import { findUriInDirs, getUriPathBasename } from "core/util/uri";
import { v4 as uuidv4 } from "uuid";
import { RootState } from "../store";
import { cancelToolCallAndAddResult } from "../thunks/cancelToolCallAndAddResult";
import { clearLastEmptyResponse } from "../thunks/clearLastEmptyResponse";
import { streamResponseThunk } from "../thunks/streamResponse";
import { findCurrentToolCall, findToolCall } from "../util";

Expand Down Expand Up @@ -103,21 +105,6 @@ export const sessionSlice = createSlice({
curMessage.isGatheringContext = payload;
}
},
clearLastEmptyResponse: (state) => {
if (state.history.length < 2) {
return;
}
const lastMessage = state.history[state.history.length - 1];

// Only clear in the case of an empty message
if (!lastMessage.message.content.length) {
state.mainEditorContentTrigger =
state.history[state.history.length - 2].editorState;
state.history = state.history.slice(0, -2);
// TODO is this logic correct for tool use conversations?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO was on the mark. I've followed up with an implementation that follows this line of thinking and peformed extensive testing to validate it.

// Maybe slice at last index of "user" role message?
}
},
// Trigger value picked up by editor with isMainInput to set its content
setMainEditorContentTrigger: (
state,
Expand Down Expand Up @@ -227,6 +214,12 @@ export const sessionSlice = createSlice({
...updates,
};
},
removeLastHistoryItem: (state) => {
state.history.pop();
},
removeHistoryItemById: (state, action: PayloadAction<string>) => {
state.history = state.history.filter(item => item.message.id !== action.payload);
},
addContextItemsAtIndex: (
state,
{
Expand Down Expand Up @@ -651,8 +644,36 @@ export const sessionSlice = createSlice({
},
},
extraReducers: (builder) => {
// Keep existing cases
addPassthroughCases(builder, [streamResponseThunk]);
},

// Add handlers for cancelToolCallAndAddResult
builder.addCase(cancelToolCallAndAddResult.fulfilled, (state, action) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be moved to the thunk so that it's all in one place? I'm not super familiar with the builder paradigm, could you explain the advantage of it vs dispatching a new message/tool call status from the thunk? Stylistic opinion but feels a bit disjointed. Testability related?

if (action.payload) {
const { toolCallId, toolResultMessage } = action.payload;

// First update the tool call status
const toolCallState = findToolCall(state.history, toolCallId);
if (toolCallState) {
toolCallState.status = "canceled";
}

// Then add the tool result message to history
// Ensure the message has the required structure with id
if (toolResultMessage && toolResultMessage.message && toolResultMessage.message.id) {
state.history = [...state.history, toolResultMessage];
} else {
console.error("Cannot push tool result message to history: invalid message structure", toolResultMessage);
}
}
});

// Add handlers for clearLastEmptyResponseThunk
builder.addCase(clearLastEmptyResponse.fulfilled, (state, action) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no action can remove

// All actions are handled directly by the thunk through dispatching
// other actions, so we don't need additional handling here
});
}
});

function addPassthroughCases(
Expand Down Expand Up @@ -707,7 +728,8 @@ export const {
setActive,
submitEditorAndInitAtIndex,
updateHistoryItemAtIndex,
clearLastEmptyResponse,
removeLastHistoryItem,
removeHistoryItemById,
setMainEditorContentTrigger,
deleteMessage,
setIsGatheringContext,
Expand Down
18 changes: 18 additions & 0 deletions gui/src/redux/thunks/cancelButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import {
abortStream,
setInactive,
} from "../slices/sessionSlice";
import { ThunkApiType } from "../store";
import { streamAssistantMessage } from "./streamAssistantMessage";
import { clearLastEmptyResponse } from "./clearLastEmptyResponse";

export const cancelButton = createAsyncThunk<void, undefined, ThunkApiType>(
"chat/cancelButton",
async (messages, { dispatch, extra, getState }) => {
await dispatch(setInactive());
await dispatch(abortStream());
await dispatch(clearLastEmptyResponse());
await dispatch(streamAssistantMessage({ content: "The request has been cancelled." }));
Copy link
Collaborator

@RomneyDa RomneyDa May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In chat mode this should just terminate, shouldn't stream an assistant response

}
);
10 changes: 3 additions & 7 deletions gui/src/redux/thunks/cancelCurrentToolCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,22 @@ export const cancelCurrentToolCall = createAsyncThunk<
return;
}

if (toolCallState.status !== "generated") {
return;
}

const { toolCallId } = toolCallState;
dispatch(
await dispatch(
cancelToolCall({
toolCallId,
}),
);

dispatch(
await dispatch(
updateToolCallOutput({
toolCallId,
contextItems: [
{
name: "Tool Cancelled",
description: "Tool Cancelled",
content:
"The tool call was cancelled by the user. Please try something else or request further instructions.",
"The tool call was cancelled by the user. Ask for further instructions.",
hidden: true,
},
],
Expand Down
2 changes: 1 addition & 1 deletion gui/src/redux/thunks/cancelStream.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import {
abortStream,
clearLastEmptyResponse,
setInactive,
} from "../slices/sessionSlice";
import { ThunkApiType } from "../store";
import { clearLastEmptyResponse } from "./clearLastEmptyResponse";

export const cancelStream = createAsyncThunk<void, undefined, ThunkApiType>(
"chat/cancelStream",
Expand Down
Loading
Loading