Skip to content
Merged
6 changes: 3 additions & 3 deletions core/tools/definitions/createNewFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn";
export const createNewFileTool: Tool = {
type: "function",
displayTitle: "Create New File",
wouldLikeTo: "create a new file at {{{ filepath }}}",
isCurrently: "creating a new file at {{{ filepath }}}",
hasAlready: "created a new file at {{{ filepath }}}",
wouldLikeTo: "create {{{ filepath }}}",
isCurrently: "creating {{{ filepath }}}",
hasAlready: "created {{{ filepath }}}",
group: BUILT_IN_GROUP_NAME,
readonly: false,
isInstant: true,
Expand Down
2 changes: 1 addition & 1 deletion core/tools/definitions/fetchUrlContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const fetchUrlContentTool: Tool = {
displayTitle: "Read URL",
wouldLikeTo: "fetch {{{ url }}}",
isCurrently: "fetching {{{ url }}}",
hasAlready: "viewed {{{ url }}}",
hasAlready: "fetched {{{ url }}}",
readonly: true,
isInstant: true,
group: BUILT_IN_GROUP_NAME,
Expand Down
6 changes: 3 additions & 3 deletions core/tools/definitions/globSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn";
export const globSearchTool: Tool = {
type: "function",
displayTitle: "Glob File Search",
wouldLikeTo: 'find file matches for "{{{ pattern }}}"',
isCurrently: 'finding file matches for "{{{ pattern }}}"',
hasAlready: 'retrieved file matches for "{{{ pattern }}}"',
wouldLikeTo: 'search for files like "{{{ pattern }}}"',
isCurrently: 'searching for files like "{{{ pattern }}}"',
hasAlready: 'searched for files like "{{{ pattern }}}"',
readonly: true,
isInstant: true,
group: BUILT_IN_GROUP_NAME,
Expand Down
6 changes: 3 additions & 3 deletions core/tools/definitions/grepSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn";
export const grepSearchTool: Tool = {
type: "function",
displayTitle: "Grep Search",
wouldLikeTo: 'search for "{{{ query }}}" in the repository',
isCurrently: 'getting search results for "{{{ query }}}"',
hasAlready: 'retrieved search results for "{{{ query }}}"',
wouldLikeTo: 'search for "{{{ query }}}"',
isCurrently: 'searching for "{{{ query }}}"',
hasAlready: 'searched for "{{{ query }}}"',
readonly: true,
isInstant: true,
group: BUILT_IN_GROUP_NAME,
Expand Down
6 changes: 3 additions & 3 deletions core/tools/definitions/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn";
export const lsTool: Tool = {
type: "function",
displayTitle: "ls",
wouldLikeTo: "list files and folders in {{{ dirPath }}}",
isCurrently: "listing files and folders in {{{ dirPath }}}",
hasAlready: "listed files and folders in {{{ dirPath }}}",
wouldLikeTo: "list files in {{{ dirPath }}}",
isCurrently: "listing files in {{{ dirPath }}}",
hasAlready: "listed files in {{{ dirPath }}}",
readonly: true,
isInstant: true,
group: BUILT_IN_GROUP_NAME,
Expand Down
2 changes: 1 addition & 1 deletion core/tools/definitions/readCurrentlyOpenFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const readCurrentlyOpenFileTool: Tool = {
displayTitle: "Read Currently Open File",
wouldLikeTo: "read the current file",
isCurrently: "reading the current file",
hasAlready: "viewed the current file",
hasAlready: "read the current file",
readonly: true,
isInstant: true,
group: BUILT_IN_GROUP_NAME,
Expand Down
2 changes: 1 addition & 1 deletion core/tools/definitions/readFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const readFileTool: Tool = {
displayTitle: "Read File",
wouldLikeTo: "read {{{ filepath }}}",
isCurrently: "reading {{{ filepath }}}",
hasAlready: "viewed {{{ filepath }}}",
hasAlready: "read {{{ filepath }}}",
readonly: true,
isInstant: true,
group: BUILT_IN_GROUP_NAME,
Expand Down
2 changes: 1 addition & 1 deletion core/tools/definitions/readFileRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const readFileRangeTool: Tool = {
isCurrently:
"reading lines {{{ startLine }}}-{{{ endLine }}} of {{{ filepath }}}",
hasAlready:
"viewed lines {{{ startLine }}}-{{{ endLine }}} of {{{ filepath }}}",
"read lines {{{ startLine }}}-{{{ endLine }}} of {{{ filepath }}}",
readonly: true,
isInstant: true,
group: BUILT_IN_GROUP_NAME,
Expand Down
6 changes: 3 additions & 3 deletions core/tools/definitions/singleFindAndReplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export interface SingleFindAndReplaceArgs {
export const singleFindAndReplaceTool: Tool = {
type: "function",
displayTitle: "Find and Replace",
wouldLikeTo: "find and replace in {{{ filepath }}}",
isCurrently: "finding and replacing in {{{ filepath }}}",
hasAlready: "found and replaced in {{{ filepath }}}",
wouldLikeTo: "edit {{{ filepath }}}",
isCurrently: "editing {{{ filepath }}}",
hasAlready: "edited {{{ filepath }}}",
group: BUILT_IN_GROUP_NAME,
readonly: false,
isInstant: false,
Expand Down
9 changes: 5 additions & 4 deletions gui/src/pages/gui/ToolCallDiv/FindAndReplace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,11 @@ describe("FindAndReplaceDisplay", () => {

render(<FindAndReplaceDisplay {...defaultProps} />);

const toggleButton = screen.getByTestId("toggle-find-and-replace-diff");
fireEvent.click(toggleButton);

expect(screen.getByText("Error generating diff")).toBeInTheDocument();
// When diff generation errors, component shows a friendly message
// without rendering the expand/collapse container
expect(
screen.getByText("The searched string was not found in the file"),
).toBeInTheDocument();
});

it("should show 'No changes to display' when diff is empty", () => {
Expand Down
86 changes: 63 additions & 23 deletions gui/src/pages/gui/ToolCallDiv/FindAndReplace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const MAX_SAME_LINES = 2;

function EllipsisLine() {
return (
<div className="text-description-muted px-3 py-1 text-center font-mono">
<div className="text-description-muted px-3 py-1 text-left font-mono">
</div>
);
Expand Down Expand Up @@ -68,6 +68,19 @@ function DiffLines({
);
}

function DiffStats({ added, removed }: { added: number; removed: number }) {
if (added === 0 && removed === 0) {
return null;
}

return (
<div className="flex items-center gap-1 font-mono text-xs">
{added > 0 && <span className="text-success">+{added}</span>}
{removed > 0 && <span className="text-error">-{removed}</span>}
</div>
);
}

export function FindAndReplaceDisplay({
fileUri,
relativeFilePath,
Expand Down Expand Up @@ -141,6 +154,30 @@ export function FindAndReplaceDisplay({
}
}, [currentFileContent, edits]);

const diffStats = useMemo(() => {
if (!diffResult?.diff) {
return { added: 0, removed: 0 };
}

let added = 0;
let removed = 0;

diffResult.diff.forEach((part) => {
const lines = part.value.split("\n");
// Exclude empty line at the end (similar to DiffLines component logic)
const lineCount =
lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;

if (part.added) {
added += lineCount;
} else if (part.removed) {
removed += lineCount;
}
});

return { added, removed };
}, [diffResult?.diff]);

const statusIcon = useMemo(() => {
const status = toolCallState?.status;
if (status) {
Expand Down Expand Up @@ -174,24 +211,27 @@ export function FindAndReplaceDisplay({
setIsExpanded(!showContent);
}}
>
<div className="flex max-w-[50%] flex-row items-center text-xs">
{statusIcon}
<ChevronDownIcon
data-testid="toggle-find-and-replace-diff"
className={`text-lightgray h-3.5 w-3.5 flex-shrink-0 cursor-pointer select-none transition-all hover:brightness-125 ${
showContent ? "rotate-0" : "-rotate-90"
}`}
/>
<FileInfo
filepath={displayName || "..."}
onClick={(e) => {
if (!fileUri) {
return;
}
e.stopPropagation();
ideMessenger.post("openFile", { path: fileUri });
}}
/>
<div className="flex min-w-0 flex-1 flex-row items-center gap-2 text-xs">
<div className="flex min-w-0 flex-row items-center">
{statusIcon}
<ChevronDownIcon
data-testid="toggle-find-and-replace-diff"
className={`text-lightgray h-3.5 w-3.5 flex-shrink-0 cursor-pointer select-none transition-all hover:brightness-125 ${
showContent ? "rotate-0" : "-rotate-90"
}`}
/>
<FileInfo
filepath={displayName || "..."}
onClick={(e) => {
if (!fileUri) {
return;
}
e.stopPropagation();
ideMessenger.post("openFile", { path: fileUri });
}}
/>
</div>
<DiffStats added={diffStats.added} removed={diffStats.removed} />
</div>

{applyState && (
Expand All @@ -218,10 +258,10 @@ export function FindAndReplaceDisplay({
);

if (diffResult?.error) {
return renderContainer(
<div className="text-error p-3 text-sm">
<strong>Error generating diff</strong>
</div>,
return (
<div className="text-description mt-2 px-3">
The searched string was not found in the file
</div>
);
}

Expand Down
19 changes: 0 additions & 19 deletions gui/src/pages/gui/ToolCallDiv/SimpleToolCallUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import {
} from "../../../components/mainInput/belowMainInput/ContextItemsPeek";
import { IdeMessengerContext } from "../../../context/IdeMessenger";
import { ToggleWithIcon } from "./ToggleWithIcon";
import { ArgsItems, ArgsToggleIcon } from "./ToolCallArgs";
import { ToolCallStatusMessage } from "./ToolCallStatusMessage";
import { ToolTruncateHistoryIcon } from "./ToolTruncateHistoryIcon";
import { toolCallStateToContextItems } from "./utils";

interface SimpleToolCallUIProps {
Expand All @@ -31,11 +29,6 @@ export function SimpleToolCallUI({
}, [toolCallState]);

const [open, setOpen] = useState(false);
const [showingArgs, setShowingArgs] = useState(false);

const args: [string, any][] = useMemo(() => {
return Object.entries(toolCallState.parsedArgs);
}, [toolCallState.parsedArgs]);

const isToggleable = shownContextItems.length > 1;
const isSingleItem = shownContextItems.length === 1;
Expand Down Expand Up @@ -81,19 +74,7 @@ export function SimpleToolCallUI({
/>
<ToolCallStatusMessage tool={tool} toolCallState={toolCallState} />
</div>
<div className="flex flex-row items-center gap-1.5">
{!!toolCallState.output?.length && (
<ToolTruncateHistoryIcon historyIndex={historyIndex} />
)}
{args.length > 0 ? (
<ArgsToggleIcon
isShowing={showingArgs}
setIsShowing={setShowingArgs}
/>
) : null}
</div>
</div>
<ArgsItems args={args} isShowing={showingArgs} />

{isToggleable && (
<div
Expand Down
23 changes: 0 additions & 23 deletions gui/src/pages/gui/ToolCallDiv/ToolCallDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { Tool, ToolCallState } from "core";
import { useMemo, useState } from "react";
import { ArgsItems, ArgsToggleIcon } from "./ToolCallArgs";
import { ToolCallStatusMessage } from "./ToolCallStatusMessage";
import { ToolTruncateHistoryIcon } from "./ToolTruncateHistoryIcon";

interface ToolCallDisplayProps {
children: React.ReactNode;
Expand All @@ -19,12 +16,6 @@ export function ToolCallDisplay({
icon,
historyIndex,
}: ToolCallDisplayProps) {
const [argsExpanded, setArgsExpanded] = useState(false);

const args: [string, any][] = useMemo(() => {
return Object.entries(toolCallState.parsedArgs);
}, [toolCallState.parsedArgs]);

return (
<div className="flex flex-col justify-center px-4">
<div className="mb-2 flex flex-col">
Expand All @@ -38,21 +29,7 @@ export function ToolCallDisplay({
)}
<ToolCallStatusMessage tool={tool} toolCallState={toolCallState} />
</div>
<div className="flex flex-row items-center gap-1.5">
{!!toolCallState.output && (
<ToolTruncateHistoryIcon historyIndex={historyIndex} />
)}
{!!args.length ? (
<ArgsToggleIcon
isShowing={argsExpanded}
setIsShowing={setArgsExpanded}
/>
) : null}
</div>
</div>
{argsExpanded && !!args.length && (
<ArgsItems args={args} isShowing={argsExpanded} />
)}
</div>
<div>{children}</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion gui/src/pages/gui/ToolCallDiv/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function getStatusIntro(

switch (status) {
case "generating":
return "is generating output to";
return "will";
case "generated":
return "wants to";
case "calling":
Expand Down
2 changes: 2 additions & 0 deletions gui/src/pages/gui/chat-tests/EditToolScenarios.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ test(
store.dispatch(setInactive());

ideMessenger.responses["getWorkspaceDirs"] = [EDIT_WORKSPACE_DIR];
// Provide empty open files to avoid MockIdeMessenger throwing on getOpenFiles
ideMessenger.responses["getOpenFiles"] = [] as any;
ideMessenger.responses["tools/evaluatePolicy"] = {
policy: "allowedWithPermission",
};
Expand Down
8 changes: 5 additions & 3 deletions gui/src/redux/thunks/callToolById.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import {
setToolCallCalling,
updateToolCallOutput,
} from "../slices/sessionSlice";
import { DEFAULT_TOOL_SETTING } from "../slices/uiSlice";
import { ThunkApiType } from "../store";
import { findToolCallById, logToolUsage } from "../util";
import { streamResponseAfterToolCall } from "./streamResponseAfterToolCall";
import { DEFAULT_TOOL_SETTING } from "../slices/uiSlice";

export const callToolById = createAsyncThunk<
void,
Expand Down Expand Up @@ -43,14 +43,15 @@ export const callToolById = createAsyncThunk<
DEFAULT_TOOL_SETTING;
const isAutoApproved = toolPolicy === "allowedWithoutPermission";

const selectedChatModel = selectSelectedChatModel(state);

posthog.capture("gui_tool_call_decision", {
model: selectedChatModel,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Sep 11, 2025

Choose a reason for hiding this comment

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

Telemetry event may include an undefined model because the null-check happens after this capture; consider providing a fallback value or moving the check earlier.

Prompt for AI agents
Address the following comment on gui/src/redux/thunks/callToolById.ts at line 49:

<comment>Telemetry event may include an undefined model because the null-check happens after this capture; consider providing a fallback value or moving the check earlier.</comment>

<file context>
@@ -43,14 +43,15 @@ export const callToolById = createAsyncThunk&lt;
+  const selectedChatModel = selectSelectedChatModel(state);
+
   posthog.capture(&quot;gui_tool_call_decision&quot;, {
+    model: selectedChatModel,
     decision: isAutoApproved ? &quot;auto_accept&quot; : &quot;accept&quot;,
     toolName: toolCallState.toolCall.function.name,
</file context>
Suggested change
model: selectedChatModel,
model: selectedChatModel ?? "unknown",
Fix with Cubic

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@continuedev does this make sense?

Choose a reason for hiding this comment

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

I've started a remote session to help with your request:

@continuedev does this make sense?

decision: isAutoApproved ? "auto_accept" : "accept",
toolName: toolCallState.toolCall.function.name,
toolCallId: toolCallId,
});

const selectedChatModel = selectSelectedChatModel(state);

if (!selectedChatModel) {
throw new Error("No model selected");
}
Expand Down Expand Up @@ -129,6 +130,7 @@ export const callToolById = createAsyncThunk<
// Capture telemetry for tool call execution outcome with duration
const duration_ms = Date.now() - startTime;
posthog.capture("gui_tool_call_outcome", {
model: selectedChatModel,
succeeded: errorMessage === undefined,
toolName: toolCallState.toolCall.function.name,
errorMessage: errorMessage,
Expand Down
Loading
Loading