diff --git a/web/src/features/chat/components/session-files-panel.tsx b/web/src/features/chat/components/session-files-panel.tsx
index 1464f2ee5..83c864142 100644
--- a/web/src/features/chat/components/session-files-panel.tsx
+++ b/web/src/features/chat/components/session-files-panel.tsx
@@ -251,7 +251,7 @@ export function SessionFilesPanel({
) : null}
- {!isLoading && !error && entries.length === 0 ? (
+ {!(isLoading || error) && entries.length === 0 ? (
No files in this directory.
diff --git a/web/src/features/chat/global-config-controls.tsx b/web/src/features/chat/global-config-controls.tsx
index 4103725ee..8713723cd 100644
--- a/web/src/features/chat/global-config-controls.tsx
+++ b/web/src/features/chat/global-config-controls.tsx
@@ -46,12 +46,16 @@ export type GlobalConfigControlsProps = {
className?: string;
planMode?: boolean;
onPlanModeChange?: (enabled: boolean) => void;
+ yoloMode?: boolean;
+ onYoloModeChange?: (enabled: boolean) => void;
};
export function GlobalConfigControls({
className,
planMode = false,
onPlanModeChange,
+ yoloMode = false,
+ onYoloModeChange,
}: GlobalConfigControlsProps): ReactElement {
const { config, isLoading, isUpdating, error, refresh, update } =
useGlobalConfig();
@@ -304,6 +308,31 @@ export function GlobalConfigControls({
>
)}
+ {onYoloModeChange && (
+ <>
+
+
+
+
+
+ YOLO
+
+
+
+
+
+ {yoloMode
+ ? "YOLO mode is active. All actions will be auto-approved without confirmation."
+ : "Enable YOLO mode to auto-approve all actions without confirmation."}
+
+
+ >
+ )}
+
{(lastBusySkip && lastBusySkip.length > 0) || error ? (
) : null}
diff --git a/web/src/hooks/useSessionStream.ts b/web/src/hooks/useSessionStream.ts
index 306f9d6bb..7a324c301 100644
--- a/web/src/hooks/useSessionStream.ts
+++ b/web/src/hooks/useSessionStream.ts
@@ -138,6 +138,7 @@ import { createMessageId, getApiBaseUrl } from "./utils";
import { kimiCliVersion } from "@/lib/version";
import { handleToolResult, useToolEventsStore, type TodoItem } from "@/features/tool/store";
import { v4 as uuidV4 } from "uuid";
+import { useYoloMode } from "@/hooks/useYoloMode";
// Regex patterns moved to top level for performance
const DATA_URL_MEDIA_TYPE_REGEX = /^data:([^;,]+)[;,]/;
@@ -242,6 +243,10 @@ type UseSessionStreamReturn = {
planMode: boolean;
/** Set plan mode via silent RPC (no context message) */
sendSetPlanMode: (enabled: boolean) => void;
+ /** Whether YOLO (auto-approve) mode is active */
+ yoloMode: boolean;
+ /** Set YOLO mode via silent RPC (no context message) */
+ sendSetYoloMode: (enabled: boolean) => void;
/** Available slash commands from the server */
slashCommands: SlashCommandDef[];
};
@@ -286,6 +291,7 @@ export function useSessionStream(
const [contextUsage, setContextUsage] = useState(0);
const [tokenUsage, setTokenUsage] = useState
(null);
const [planMode, setPlanMode] = useState(false);
+ const [yoloMode, setYoloMode] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState(null);
@@ -293,6 +299,18 @@ export function useSessionStream(
const [isReplayingHistory, setIsReplayingHistory] = useState(true);
const [slashCommands, setSlashCommands] = useState([]);
+ // Fetch initial YOLO mode state from the server API
+ const { yoloStatus } = useYoloMode(sessionId);
+
+ // Update yoloMode state when yoloStatus changes (initial load or refresh)
+ // Only apply if we haven't received a live update from WebSocket to avoid
+ // stale HTTP responses overwriting newer real-time state
+ useEffect(() => {
+ if (yoloStatus !== null && !hasReceivedLiveYoloUpdateRef.current) {
+ setYoloMode(yoloStatus.enabled);
+ }
+ }, [yoloStatus]);
+
// Refs
/**
* The single source of truth for "which WebSocket is allowed to mutate React state".
@@ -315,7 +333,7 @@ export function useSessionStream(
const connectRef = useRef<() => void>(() => undefined);
const disconnectRef = useRef<() => void>(() => undefined);
const reconnectRef = useRef<() => void>(() => undefined);
- const resetStateRef = useRef<(preserveSlashCommands?: boolean) => void>(() => undefined);
+ const resetStateRef = useRef<(preserveSlashCommands?: boolean, preserveYoloMode?: boolean) => void>(() => undefined);
const historyCompleteTimeoutRef = useRef(null);
const isReplayingRef = useRef(true); // Track if we're still replaying history
const pendingMessageRef = useRef(null); // Message to send after connection
@@ -325,6 +343,8 @@ export function useSessionStream(
const lastWsMessageTimeRef = useRef(0); // Last time a WS message was received
const watchdogIntervalRef = useRef(null); // Stale connection watchdog
const statusRef = useRef("ready"); // Synced copy of status for watchdog
+ const previousSessionIdRef = useRef(null); // Track previous session for reconnect detection
+ const hasReceivedLiveYoloUpdateRef = useRef(false); // Track if we've received live yolo_mode from WebSocket
// First turn tracking for auto-rename (simplified: backend reads from wire.jsonl)
const hasTurnStartedRef = useRef(false); // Whether at least one turn has started
@@ -842,7 +862,7 @@ export function useSessionStream(
}, []);
// Reset all state
- const resetState = useCallback((preserveSlashCommands = false) => {
+ const resetState = useCallback((preserveSlashCommands = false, preserveYoloMode = false) => {
resetStepState();
currentToolCallsRef.current?.clear();
currentToolCallIdRef.current = null;
@@ -853,6 +873,10 @@ export function useSessionStream(
setContextUsage(0);
setTokenUsage(null);
setPlanMode(false);
+ if (!preserveYoloMode) {
+ setYoloMode(false);
+ hasReceivedLiveYoloUpdateRef.current = false;
+ }
setError(null);
setSessionStatus(null);
lastStatusSeqRef.current = null;
@@ -1735,6 +1759,12 @@ export function useSessionStream(
setPlanMode(nextPlanMode);
}
+ const nextYoloMode = event.payload.yolo_mode;
+ if (typeof nextYoloMode === "boolean") {
+ hasReceivedLiveYoloUpdateRef.current = true;
+ setYoloMode(nextYoloMode);
+ }
+
// If we have a message_id, create a special message to display it
const messageId = event.payload.message_id;
if (messageId) {
@@ -2397,8 +2427,12 @@ export function useSessionStream(
watchdogIntervalRef.current = null;
}
+ // Check if we're reconnecting to the same session (preserve yoloMode) or switching sessions (reset yoloMode)
+ const isReconnectingToSameSession = previousSessionIdRef.current === sessionId;
+ previousSessionIdRef.current = sessionId;
+
awaitingIdleRef.current = false;
- resetState(true); // preserve slashCommands on reconnect
+ resetState(true, isReconnectingToSameSession); // preserve slashCommands on reconnect, preserve yoloMode on same-session reconnect
setMessages([]);
setStatus("submitted");
setAwaitingFirstResponse(Boolean(pendingMessageRef.current));
@@ -2815,6 +2849,20 @@ export function useSessionStream(
wsRef.current.send(JSON.stringify(message));
}, []);
+ // Set YOLO mode via silent RPC (no context message)
+ const sendSetYoloMode = useCallback((enabled: boolean) => {
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
+ return;
+ }
+ const message: JsonRpcRequest = {
+ jsonrpc: "2.0",
+ method: "set_yolo_mode",
+ id: uuidV4(),
+ params: { enabled },
+ };
+ wsRef.current.send(JSON.stringify(message));
+ }, []);
+
// Auto-connect when sessionId changes
useLayoutEffect(() => {
/**
@@ -2899,6 +2947,8 @@ export function useSessionStream(
error,
planMode,
sendSetPlanMode,
+ yoloMode,
+ sendSetYoloMode,
slashCommands,
};
}
diff --git a/web/src/hooks/useYoloMode.ts b/web/src/hooks/useYoloMode.ts
new file mode 100644
index 000000000..456c095d9
--- /dev/null
+++ b/web/src/hooks/useYoloMode.ts
@@ -0,0 +1,166 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { getApiBaseUrl } from "@/hooks/utils";
+import { getAuthHeader } from "@/lib/auth";
+import type { YoloStatus } from "@/lib/api/models";
+
+export type UseYoloModeReturn = {
+ yoloStatus: YoloStatus | null;
+ isLoading: boolean;
+ isUpdating: boolean;
+ error: string | null;
+ refresh: () => Promise;
+ setYoloMode: (enabled: boolean) => Promise;
+};
+
+export function useYoloMode(sessionId: string | null): UseYoloModeReturn {
+ const [yoloStatus, setYoloStatus] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isUpdating, setIsUpdating] = useState(false);
+ const [error, setError] = useState(null);
+
+ const isInitializedRef = useRef(false);
+ const sessionIdRef = useRef(sessionId);
+ const abortControllerRef = useRef(null);
+
+ const refresh = useCallback(async () => {
+ if (!sessionId) {
+ setYoloStatus(null);
+ return;
+ }
+
+ // Capture sessionId at request start to detect stale responses
+ const requestSessionId = sessionId;
+
+ // Cancel any in-flight request
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+
+ // Create new AbortController for this request
+ const controller = new AbortController();
+ abortControllerRef.current = controller;
+
+ setIsLoading(true);
+ setError(null);
+ try {
+ const response = await fetch(
+ `${getApiBaseUrl()}/api/sessions/${sessionId}/yolo`,
+ {
+ headers: {
+ ...getAuthHeader(),
+ },
+ signal: controller.signal,
+ },
+ );
+
+ if (!response.ok) {
+ const data = await response.json().catch(() => ({}));
+ throw new Error(
+ data.detail || `Failed to load YOLO status: ${response.status}`,
+ );
+ }
+
+ const data = await response.json();
+
+ // Guard against stale responses: only apply if session hasn't changed
+ if (
+ requestSessionId !== sessionIdRef.current ||
+ abortControllerRef.current !== controller
+ ) {
+ return;
+ }
+
+ setYoloStatus({
+ enabled: data.enabled,
+ autoApproveActions: data.auto_approve_actions,
+ });
+ } catch (err) {
+ // Skip state updates if the request was aborted
+ if (err instanceof Error && err.name === "AbortError") {
+ return;
+ }
+ const message =
+ err instanceof Error ? err.message : "Failed to load YOLO status";
+ setError(message);
+ console.error("[useYoloMode] Failed to load YOLO status:", err);
+ } finally {
+ // Only clear loading state if this request wasn't aborted
+ if (abortControllerRef.current === controller) {
+ setIsLoading(false);
+ }
+ }
+ }, [sessionId]);
+
+ const setYoloMode = useCallback(
+ async (enabled: boolean) => {
+ if (!sessionId) {
+ throw new Error("No session selected");
+ }
+
+ setIsUpdating(true);
+ setError(null);
+ try {
+ const response = await fetch(
+ `${getApiBaseUrl()}/api/sessions/${sessionId}/yolo`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...getAuthHeader(),
+ },
+ body: JSON.stringify({ enabled }),
+ },
+ );
+
+ if (!response.ok) {
+ const data = await response.json().catch(() => ({}));
+ throw new Error(
+ data.detail || `Failed to update YOLO mode: ${response.status}`,
+ );
+ }
+
+ const data = await response.json();
+ setYoloStatus({
+ enabled: data.enabled,
+ autoApproveActions: data.auto_approve_actions,
+ });
+ } catch (err) {
+ const message =
+ err instanceof Error ? err.message : "Failed to update YOLO mode";
+ setError(message);
+ console.error("[useYoloMode] Failed to update YOLO mode:", err);
+ throw err;
+ } finally {
+ setIsUpdating(false);
+ }
+ },
+ [sessionId],
+ );
+
+ // Load initial data
+ useEffect(() => {
+ if (isInitializedRef.current && sessionIdRef.current === sessionId) {
+ return;
+ }
+ sessionIdRef.current = sessionId;
+ isInitializedRef.current = true;
+ refresh();
+
+ // Cleanup: abort any in-flight request when sessionId changes or component unmounts
+ return () => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ abortControllerRef.current = null;
+ }
+ };
+ }, [sessionId, refresh]);
+
+ return {
+ yoloStatus,
+ isLoading,
+ isUpdating,
+ error,
+ refresh,
+ setYoloMode,
+ };
+}
diff --git a/web/src/hooks/wireTypes.ts b/web/src/hooks/wireTypes.ts
index f35b378a4..ee88be9ad 100644
--- a/web/src/hooks/wireTypes.ts
+++ b/web/src/hooks/wireTypes.ts
@@ -131,6 +131,7 @@ export type StatusUpdateEvent = {
token_usage?: TokenUsage | null;
message_id?: string;
plan_mode?: boolean | null;
+ yolo_mode?: boolean | null;
};
};
diff --git a/web/src/lib/api/models/UpdateYoloRequest.ts b/web/src/lib/api/models/UpdateYoloRequest.ts
new file mode 100644
index 000000000..a1aab9cdf
--- /dev/null
+++ b/web/src/lib/api/models/UpdateYoloRequest.ts
@@ -0,0 +1,65 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Kimi Code CLI Web Interface
+ * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
+ *
+ * The version of the OpenAPI document: 0.1.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { exists, mapValues } from "../runtime";
+/**
+ * Update YOLO mode request.
+ * @export
+ * @interface UpdateYoloRequest
+ */
+export interface UpdateYoloRequest {
+ /**
+ * Enable or disable YOLO mode
+ * @type {boolean}
+ * @memberof UpdateYoloRequest
+ */
+ enabled: boolean;
+}
+
+/**
+ * Check if a given object implements the UpdateYoloRequest interface.
+ */
+export function instanceOfUpdateYoloRequest(
+ value: object,
+): value is UpdateYoloRequest {
+ if (!("enabled" in value) || value["enabled"] === undefined) return false;
+ return true;
+}
+
+export function UpdateYoloRequestFromJSON(json: any): UpdateYoloRequest {
+ return UpdateYoloRequestFromJSONTyped(json, false);
+}
+
+export function UpdateYoloRequestFromJSONTyped(
+ json: any,
+ ignoreDiscriminator: boolean,
+): UpdateYoloRequest {
+ if (json == null) {
+ return json;
+ }
+ return {
+ enabled: json["enabled"],
+ };
+}
+
+export function UpdateYoloRequestToJSON(
+ value?: UpdateYoloRequest | null,
+): any {
+ if (value == null) {
+ return value;
+ }
+ return {
+ enabled: value["enabled"],
+ };
+}
diff --git a/web/src/lib/api/models/YoloStatus.ts b/web/src/lib/api/models/YoloStatus.ts
new file mode 100644
index 000000000..cf9f8d399
--- /dev/null
+++ b/web/src/lib/api/models/YoloStatus.ts
@@ -0,0 +1,74 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Kimi Code CLI Web Interface
+ * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
+ *
+ * The version of the OpenAPI document: 0.1.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { exists, mapValues } from "../runtime";
+/**
+ * YOLO (auto-approve) mode status.
+ * @export
+ * @interface YoloStatus
+ */
+export interface YoloStatus {
+ /**
+ * Whether YOLO mode is enabled
+ * @type {boolean}
+ * @memberof YoloStatus
+ */
+ enabled: boolean;
+ /**
+ * List of auto-approved action types
+ * @type {Array}
+ * @memberof YoloStatus
+ */
+ autoApproveActions?: Array;
+}
+
+/**
+ * Check if a given object implements the YoloStatus interface.
+ */
+export function instanceOfYoloStatus(
+ value: object,
+): value is YoloStatus {
+ if (!("enabled" in value) || value["enabled"] === undefined) return false;
+ return true;
+}
+
+export function YoloStatusFromJSON(json: any): YoloStatus {
+ return YoloStatusFromJSONTyped(json, false);
+}
+
+export function YoloStatusFromJSONTyped(
+ json: any,
+ ignoreDiscriminator: boolean,
+): YoloStatus {
+ if (json == null) {
+ return json;
+ }
+ return {
+ enabled: json["enabled"],
+ autoApproveActions:
+ json["auto_approve_actions"] == null
+ ? undefined
+ : json["auto_approve_actions"],
+ };
+}
+
+export function YoloStatusToJSON(value?: YoloStatus | null): any {
+ if (value == null) {
+ return value;
+ }
+ return {
+ enabled: value["enabled"],
+ auto_approve_actions: value["autoApproveActions"],
+ };
+}
diff --git a/web/src/lib/api/models/index.ts b/web/src/lib/api/models/index.ts
index 80183dae6..be0e21ca4 100644
--- a/web/src/lib/api/models/index.ts
+++ b/web/src/lib/api/models/index.ts
@@ -20,6 +20,8 @@ export * from './UpdateConfigTomlResponse';
export * from './UpdateGlobalConfigRequest';
export * from './UpdateGlobalConfigResponse';
export * from './UpdateSessionRequest';
+export * from './UpdateYoloRequest';
export * from './UploadSessionFileResponse';
export * from './ValidationError';
export * from './ValidationErrorLocInner';
+export * from './YoloStatus';