Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Commit a06eed8

Browse files
sugyanclaude
andauthored
feat: implement session-scoped permission mode state management (#232)
- Add usePermissionMode hook for session-scoped PermissionMode state - Integrate permission mode updates with plan approval actions - Simplify sendMessage to use single permission mode source - Add comprehensive tests for permission mode functionality Resolves #135 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9e16835 commit a06eed8

4 files changed

Lines changed: 145 additions & 9 deletions

File tree

frontend/src/components/ChatPage.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useTheme } from "../hooks/useTheme";
66
import { useClaudeStreaming } from "../hooks/useClaudeStreaming";
77
import { useChatState } from "../hooks/chat/useChatState";
88
import { usePermissions } from "../hooks/chat/usePermissions";
9+
import { usePermissionMode } from "../hooks/chat/usePermissionMode";
910
import { useAbortController } from "../hooks/chat/useAbortController";
1011
import { useAutoHistoryLoader } from "../hooks/useHistoryLoader";
1112
import { ThemeToggle } from "./chat/ThemeToggle";
@@ -46,6 +47,9 @@ export function ChatPage() {
4647
const { processStreamLine } = useClaudeStreaming();
4748
const { abortRequest, createAbortHandler } = useAbortController();
4849

50+
// Permission mode state management
51+
const { permissionMode, setPermissionMode } = usePermissionMode();
52+
4953
// Get encoded name for current working directory
5054
const getEncodedName = useCallback(() => {
5155
if (!workingDirectory || !projects.length) {
@@ -113,7 +117,10 @@ export function ChatPage() {
113117
planModeRequest,
114118
showPlanModeRequest,
115119
closePlanModeRequest,
116-
} = usePermissions();
120+
updatePermissionMode,
121+
} = usePermissions({
122+
onPermissionModeChange: setPermissionMode,
123+
});
117124

118125
const handlePermissionError = useCallback(
119126
(toolName: string, patterns: string[], toolUseId: string) => {
@@ -133,7 +140,6 @@ export function ChatPage() {
133140
messageContent?: string,
134141
tools?: string[],
135142
hideUserMessage = false,
136-
permissionMode?: "default" | "plan" | "acceptEdits",
137143
) => {
138144
const content = messageContent || input.trim();
139145
if (!content || isLoading) return;
@@ -164,7 +170,7 @@ export function ChatPage() {
164170
...(currentSessionId ? { sessionId: currentSessionId } : {}),
165171
allowedTools: tools || allowedTools,
166172
...(workingDirectory ? { workingDirectory } : {}),
167-
...(permissionMode ? { permissionMode } : {}),
173+
permissionMode,
168174
} as ChatRequest),
169175
});
170176

@@ -233,6 +239,7 @@ export function ChatPage() {
233239
hasShownInitMessage,
234240
currentAssistantMessage,
235241
workingDirectory,
242+
permissionMode,
236243
generateRequestId,
237244
clearInput,
238245
startRequest,
@@ -306,22 +313,37 @@ export function ChatPage() {
306313

307314
// Plan mode request handlers
308315
const handlePlanAcceptWithEdits = useCallback(() => {
316+
updatePermissionMode("acceptEdits");
309317
closePlanModeRequest();
310318
if (currentSessionId) {
311-
sendMessage("accept", allowedTools, true, "acceptEdits");
319+
sendMessage("accept", allowedTools, true);
312320
}
313-
}, [closePlanModeRequest, currentSessionId, sendMessage, allowedTools]);
321+
}, [
322+
updatePermissionMode,
323+
closePlanModeRequest,
324+
currentSessionId,
325+
sendMessage,
326+
allowedTools,
327+
]);
314328

315329
const handlePlanAcceptDefault = useCallback(() => {
330+
updatePermissionMode("default");
316331
closePlanModeRequest();
317332
if (currentSessionId) {
318-
sendMessage("accept", allowedTools, true, "default");
333+
sendMessage("accept", allowedTools, true);
319334
}
320-
}, [closePlanModeRequest, currentSessionId, sendMessage, allowedTools]);
335+
}, [
336+
updatePermissionMode,
337+
closePlanModeRequest,
338+
currentSessionId,
339+
sendMessage,
340+
allowedTools,
341+
]);
321342

322343
const handlePlanKeepPlanning = useCallback(() => {
344+
updatePermissionMode("plan");
323345
closePlanModeRequest();
324-
}, [closePlanModeRequest]);
346+
}, [updatePermissionMode, closePlanModeRequest]);
325347

326348
// Create permission data for inline permission interface
327349
const permissionData = permissionRequest
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, it, expect } from "vitest";
2+
import { renderHook, act } from "@testing-library/react";
3+
import { usePermissionMode } from "./usePermissionMode";
4+
5+
describe("usePermissionMode", () => {
6+
it("should initialize with default permission mode", () => {
7+
const { result } = renderHook(() => usePermissionMode());
8+
9+
expect(result.current.permissionMode).toBe("default");
10+
expect(result.current.isDefaultMode).toBe(true);
11+
expect(result.current.isPlanMode).toBe(false);
12+
expect(result.current.isAcceptEditsMode).toBe(false);
13+
});
14+
15+
it("should update permission mode correctly", () => {
16+
const { result } = renderHook(() => usePermissionMode());
17+
18+
act(() => {
19+
result.current.setPermissionMode("plan");
20+
});
21+
22+
expect(result.current.permissionMode).toBe("plan");
23+
expect(result.current.isPlanMode).toBe(true);
24+
expect(result.current.isDefaultMode).toBe(false);
25+
expect(result.current.isAcceptEditsMode).toBe(false);
26+
});
27+
28+
it("should handle acceptEdits mode correctly", () => {
29+
const { result } = renderHook(() => usePermissionMode());
30+
31+
act(() => {
32+
result.current.setPermissionMode("acceptEdits");
33+
});
34+
35+
expect(result.current.permissionMode).toBe("acceptEdits");
36+
expect(result.current.isAcceptEditsMode).toBe(true);
37+
expect(result.current.isDefaultMode).toBe(false);
38+
expect(result.current.isPlanMode).toBe(false);
39+
});
40+
41+
it("should persist state across re-renders", () => {
42+
const { result, rerender } = renderHook(() => usePermissionMode());
43+
44+
act(() => {
45+
result.current.setPermissionMode("plan");
46+
});
47+
48+
rerender();
49+
50+
expect(result.current.permissionMode).toBe("plan");
51+
expect(result.current.isPlanMode).toBe(true);
52+
});
53+
54+
it("should reset to default on new hook instance", () => {
55+
const { result: result1 } = renderHook(() => usePermissionMode());
56+
57+
act(() => {
58+
result1.current.setPermissionMode("acceptEdits");
59+
});
60+
61+
// Create a new hook instance (simulating page reload)
62+
const { result: result2 } = renderHook(() => usePermissionMode());
63+
64+
expect(result2.current.permissionMode).toBe("default");
65+
expect(result2.current.isDefaultMode).toBe(true);
66+
});
67+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useState, useCallback } from "react";
2+
import type { PermissionMode } from "../../types";
3+
4+
export interface UsePermissionModeResult {
5+
permissionMode: PermissionMode;
6+
setPermissionMode: (mode: PermissionMode) => void;
7+
isPlanMode: boolean;
8+
isDefaultMode: boolean;
9+
isAcceptEditsMode: boolean;
10+
}
11+
12+
/**
13+
* Hook for managing PermissionMode state within a browser session.
14+
* State is preserved across component re-renders but resets on page reload.
15+
* No localStorage persistence - simple React state management.
16+
*/
17+
export function usePermissionMode(): UsePermissionModeResult {
18+
const [permissionMode, setPermissionModeState] =
19+
useState<PermissionMode>("default");
20+
21+
const setPermissionMode = useCallback((mode: PermissionMode) => {
22+
setPermissionModeState(mode);
23+
}, []);
24+
25+
return {
26+
permissionMode,
27+
setPermissionMode,
28+
isPlanMode: permissionMode === "plan",
29+
isDefaultMode: permissionMode === "default",
30+
isAcceptEditsMode: permissionMode === "acceptEdits",
31+
};
32+
}

frontend/src/hooks/chat/usePermissions.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState, useCallback } from "react";
2+
import type { PermissionMode } from "../../types";
23

34
interface PermissionRequest {
45
isOpen: boolean;
@@ -12,7 +13,12 @@ interface PlanModeRequest {
1213
planContent: string;
1314
}
1415

15-
export function usePermissions() {
16+
interface UsePermissionsOptions {
17+
onPermissionModeChange?: (mode: PermissionMode) => void;
18+
}
19+
20+
export function usePermissions(options: UsePermissionsOptions = {}) {
21+
const { onPermissionModeChange } = options;
1622
const [allowedTools, setAllowedTools] = useState<string[]>([]);
1723
const [permissionRequest, setPermissionRequest] =
1824
useState<PermissionRequest | null>(null);
@@ -77,6 +83,14 @@ export function usePermissions() {
7783
setAllowedTools([]);
7884
}, []);
7985

86+
// Helper function to update permission mode based on user action
87+
const updatePermissionMode = useCallback(
88+
(mode: PermissionMode) => {
89+
onPermissionModeChange?.(mode);
90+
},
91+
[onPermissionModeChange],
92+
);
93+
8094
return {
8195
allowedTools,
8296
permissionRequest,
@@ -90,5 +104,6 @@ export function usePermissions() {
90104
planModeRequest,
91105
showPlanModeRequest,
92106
closePlanModeRequest,
107+
updatePermissionMode,
93108
};
94109
}

0 commit comments

Comments
 (0)