Skip to content
Open
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
95 changes: 87 additions & 8 deletions apps/agent/src/app/(protected)/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { View, Platform, KeyboardAvoidingView, ScrollView } from "react-native";
import { Image } from "expo-image";
import * as Clipboard from "expo-clipboard";
Expand Down Expand Up @@ -52,6 +52,7 @@ export default function ChatPage() {

const pendingToolCalls = useRef<Set<string>>(new Set()); // Track tool calls that need responses before calling LLM
const toolKAContents = useRef<Map<string, any[]>>(new Map()); // Track KAs across tool calls in a single request
const dispatchedHiddenCalls = useRef<Set<string>>(new Set()); // Track auto-dispatched hidden tool calls

const chatMessagesRef = useRef<ScrollView>(null);

Expand Down Expand Up @@ -92,6 +93,29 @@ export default function ChatPage() {
});
}

// Auto-execute tool calls when panels are hidden (auto-approve on + show panels off).
// Deps intentionally exclude tools/callTool — dispatchedHiddenCalls ref prevents double-dispatch.
useEffect(() => {
for (const m of messages) {
if (m.role !== "assistant" || !m.tool_calls) continue;
for (const tc of m.tool_calls) {
const tcId = tc.id || "";
if (!tcId) continue;
if (dispatchedHiddenCalls.current.has(tcId)) continue;
if (tools.getCallInfo(tcId)) continue;

const isAutoApproved =
settings.autoApproveMcpTools || tools.isAllowedForSession(tc.name);
if (!isAutoApproved || settings.showMcpToolExecutionPanels) continue;

dispatchedHiddenCalls.current.add(tcId);
tools.allowForSession(tc.name);
callTool({ ...tc, id: tcId });
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messages, settings.autoApproveMcpTools, settings.showMcpToolExecutionPanels]);

function addToolResultAndCheckCompletion(toolResult: ChatMessage) {
const kaContents: any[] = [];
const otherContents: any[] = [];
Expand Down Expand Up @@ -277,6 +301,25 @@ export default function ChatPage() {
[mcp, showAlert],
);

// True when hidden tool calls are in-flight (not yet dispatched or still loading)
const hasHiddenPendingTools =
!settings.showMcpToolExecutionPanels &&
messages.some(
(m) =>
m.role === "assistant" &&
m.tool_calls?.some((tc) => {
const tcId = tc.id || "";
if (!tcId) return false;
const info = tools.getCallInfo(tcId);
const isAutoApproved =
settings.autoApproveMcpTools ||
tools.isAllowedForSession(tc.name);
return isAutoApproved && (!info || info.status === "loading");
}),
);

const isBusy = isGenerating || hasHiddenPendingTools;

const isLandingScreen = !messages.length && !isNativeMobile;
console.debug("Messages:", messages);
console.debug("Tools (enabled):", tools.enabled);
Expand Down Expand Up @@ -349,8 +392,33 @@ export default function ChatPage() {
}
}

// Skip assistant messages that have only hidden tool calls and no visible content
const hasToolCalls = !!m.tool_calls?.length;
const allToolCallsHidden =
hasToolCalls &&
m.tool_calls!.every((tc) => {
const isAutoApproved =
settings.autoApproveMcpTools ||
tools.isAllowedForSession(tc.name);
return (
isAutoApproved && !settings.showMcpToolExecutionPanels
);
});

const hasVisibleText = text.some((t) => t.trim());

if (
allToolCallsHidden &&
!hasVisibleText &&
kas.length === 0 &&
files.length === 0 &&
images.length === 0
) {
return null;
}

const isLastMessage = i === messages.length - 1;
const isIdle = !isGenerating && !m.tool_calls?.length;
const isIdle = !isBusy && !m.tool_calls?.length;

return (
<Chat.Message
Expand Down Expand Up @@ -391,16 +459,26 @@ export default function ChatPage() {
id: tcId,
info: tools.getCallInfo(tcId),
};

const isAutoApproved =
settings.autoApproveMcpTools ||
tools.isAllowedForSession(tc.name);

// Hide panel when auto-approved and panels are off
if (
isAutoApproved &&
!settings.showMcpToolExecutionPanels
) {
return null;
}

const toolInfo = mcp.getToolInfo(tc.name);

const title = toolInfo
? `${toolInfo.name} - ${mcp.name} (MCP Server)`
: tc.name;
const description = toolInfo?.description;
const autoconfirm =
(settings.autoApproveMcpTools ||
tools.isAllowedForSession(tc.name)) &&
!tc.info;
const autoconfirm = isAutoApproved && !tc.info;

return (
<Chat.Message.ToolCall
Expand Down Expand Up @@ -432,13 +510,14 @@ export default function ChatPage() {
tools.reset();
pendingToolCalls.current.clear();
toolKAContents.current.clear();
dispatchedHiddenCalls.current.clear();
}}
/>
)}
</Chat.Message>
);
})}
{isGenerating && <Chat.Thinking />}
{isBusy && <Chat.Thinking />}
</Chat.Messages>
</Container>

Expand Down Expand Up @@ -541,7 +620,7 @@ export default function ChatPage() {
onToolServerTick={(_, enabled) => {
tools.toggleAll(enabled);
}}
disabled={isGenerating}
disabled={isBusy}
style={[{ maxWidth: 800 }, isWeb && { pointerEvents: "auto" }]}
/>
</Container>
Expand Down
8 changes: 5 additions & 3 deletions apps/agent/src/app/(protected)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,10 @@ const sections = [
const { showDialog } = useDialog();

const update = useCallback(
async (value: boolean) => {
async (autoApprove: boolean, showPanels: boolean) => {
try {
await settings.set("autoApproveMcpTools", value);
await settings.set("autoApproveMcpTools", autoApprove);
await settings.set("showMcpToolExecutionPanels", showPanels);
await settings.reload();
showDialog({
type: "success",
Expand All @@ -199,7 +200,8 @@ const sections = [

return (
<McpAutoapproveForm
currentValue={settings.autoApproveMcpTools}
currentAutoApprove={settings.autoApproveMcpTools}
currentShowPanels={settings.showMcpToolExecutionPanels}
onSubmit={update}
/>
);
Expand Down
3 changes: 3 additions & 0 deletions apps/agent/src/components/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ export default function Checkbox(
onValueChange?: (value: boolean) => void;
style?: StyleProp<ViewStyle>;
testID?: string;
disabled?: boolean;
}>,
) {
const colors = useColors();

return (
<Pressable
disabled={props.disabled}
style={[
{
padding: 6,
Expand All @@ -25,6 +27,7 @@ export default function Checkbox(
gap: 4,
},
props.style,
props.disabled && { opacity: 0.4 },
]}
onPress={() => props.onValueChange?.(!props.value)}
testID={props.testID}
Expand Down
55 changes: 47 additions & 8 deletions apps/agent/src/components/forms/McpAutoaproveForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,34 @@ import Checkbox from "@/components/Checkbox";
import Button from "@/components/Button";

export default function McpAutoapproveForm({
currentValue,
currentAutoApprove,
currentShowPanels,
onSubmit,
}: {
currentValue: boolean;
onSubmit: (value: boolean) => Promise<void>;
currentAutoApprove: boolean;
currentShowPanels: boolean;
onSubmit: (autoApprove: boolean, showPanels: boolean) => Promise<void>;
}) {
const colors = useColors();
const [value, setValue] = useState(currentValue);
const [autoApprove, setAutoApprove] = useState(currentAutoApprove);
const [showPanels, setShowPanels] = useState(currentShowPanels);
const [loading, setLoading] = useState(false);

const isDirty =
autoApprove !== currentAutoApprove || showPanels !== currentShowPanels;

const submit = useCallback(async () => {
setLoading(true);
try {
await onSubmit(value);
await onSubmit(autoApprove, showPanels);
} finally {
setLoading(false);
}
}, [onSubmit, value]);
}, [onSubmit, autoApprove, showPanels]);

return (
<View style={{ flex: 1 }}>
<Checkbox value={value} onValueChange={setValue}>
<Checkbox value={autoApprove} onValueChange={setAutoApprove}>
<Text
style={{
fontFamily: "Manrope_400Regular",
Expand All @@ -51,11 +57,44 @@ export default function McpAutoapproveForm({
Allow DKG Agent to run MCP tools automatically without requiring user
confirmation.
</Text>

<Checkbox
value={showPanels}
onValueChange={setShowPanels}
disabled={!autoApprove}
style={{ marginLeft: 24 }}
>
<Text
style={{
fontFamily: "Manrope_400Regular",
color: colors.text,
fontSize: 16,
lineHeight: 16,
}}
>
Show MCP tool execution panels
</Text>
</Checkbox>
<Text
style={{
fontFamily: "Manrope_400Regular",
color: colors.placeholder,
fontSize: 12,
lineHeight: 18,
marginBottom: 8,
marginLeft: 24,
opacity: !autoApprove ? 0.4 : 1,
}}
>
Display tool name, inputs, and outputs in chat when tools run
automatically.
</Text>

<Button
color="primary"
text="Update"
onPress={submit}
disabled={loading || value === currentValue}
disabled={!isDirty || loading}
/>
</View>
);
Expand Down
4 changes: 3 additions & 1 deletion apps/agent/src/hooks/useSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import {

export type Settings = {
autoApproveMcpTools: boolean;
showMcpToolExecutionPanels: boolean;
};

const defaultSettings: Settings = {
autoApproveMcpTools: false,
autoApproveMcpTools: true,
showMcpToolExecutionPanels: false,
} satisfies Record<string, boolean | string | number | Record<string, unknown>>;

const SettingsContext = createContext<{
Expand Down
Loading