Skip to content

Commit 9cb9640

Browse files
王璨claude
andcommitted
fix: improve permission prompt interaction
Stabilize permission menu navigation, add input-idea flow for revising blocked actions, and persist permission rules through user settings when needed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 873914e commit 9cb9640

9 files changed

Lines changed: 371 additions & 41 deletions

File tree

src/core/config.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function userConfigPath(): string {
1818
return join(dsConfigHome(), "config.json");
1919
}
2020

21-
function userSettingsPath(): string {
21+
export function userSettingsPath(): string {
2222
return join(dsConfigHome(), "settings.json");
2323
}
2424

@@ -54,6 +54,16 @@ function loadScopedSettings(settingsPath: string): Record<string, unknown> {
5454
return loadJsonSafe(settingsPath);
5555
}
5656

57+
export function loadUserSettings(): Record<string, unknown> {
58+
return loadScopedSettings(userSettingsPath());
59+
}
60+
61+
export function saveUserSettings(partial: Record<string, unknown>): void {
62+
const path = userSettingsPath();
63+
const existing = loadUserSettings();
64+
saveJsonSafe(path, { ...existing, ...partial });
65+
}
66+
5767
export function saveUserConfig(partial: Record<string, unknown>): void {
5868
const path = userConfigPath();
5969
const existing = loadUserCommandConfig();
@@ -106,6 +116,8 @@ export function loadConfig(): HarnessConfig {
106116
// API key: env var > user config (never project config for security)
107117
const apiKey = process.env.DEEPSEEK_API_KEY ?? (userConfig.apiKey as string | undefined);
108118

119+
const userPermissionRules = ((userSettings.permissions as any)?.rules as Record<string, unknown>[]) ?? [];
120+
const projectPermissionRules = ((projectSettings.permissions as any)?.rules as Record<string, unknown>[]) ?? [];
109121
const userDeny = ((userSettings.permissions as any)?.deny as string[]) ?? [];
110122
const projectDeny = ((projectSettings.permissions as any)?.deny as string[]) ?? [];
111123
const denyPatterns = [...new Set([...userDeny, ...projectDeny])];
@@ -182,7 +194,7 @@ export function loadConfig(): HarnessConfig {
182194
},
183195
permissions: {
184196
defaultDecision: "ask",
185-
rules: [],
197+
rules: [...userPermissionRules, ...projectPermissionRules] as any,
186198
denyPatterns,
187199
},
188200
skills,

src/core/harness.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class Harness {
4040
this.skillManager = new SkillManager(config.userSkillsDir, config.projectSkillsDir);
4141
this.permissionManager = new PermissionManager(
4242
config.permissions,
43-
(toolName, preview) => this.tui.getPromptPermission()(toolName, preview),
43+
(toolName, preview, args) => this.tui.getPromptPermission()(toolName, preview, args),
4444
() => {},
4545
);
4646
}

src/core/types.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,15 @@ export interface SkillManifest {
125125

126126
// --- UI ---
127127

128-
export type PromptUserFn = (toolName: string, preview: string) => Promise<{
128+
export interface PermissionPromptResult {
129129
decision: "allow" | "deny";
130-
rememberForSession: boolean;
131-
}>;
130+
rememberForSession?: boolean;
131+
persistRule?: PermissionRuleConfig;
132+
denyReason?: string;
133+
}
134+
135+
export type PromptUserFn = (
136+
toolName: string,
137+
preview: string,
138+
args: unknown,
139+
) => Promise<PermissionPromptResult>;

src/permissions/manager.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { PermissionDecision, PermissionRule, PermissionsConfig, PromptUserFn } from "../core/types.js";
1+
import type { PermissionDecision, PermissionRule, PermissionRuleConfig, PermissionsConfig, PromptUserFn } from "../core/types.js";
2+
import { loadUserSettings, saveUserSettings } from "../core/config.js";
23
import { DEFAULT_RULES } from "./rules.js";
34

45
function globToRegex(pattern: string): RegExp {
@@ -70,12 +71,23 @@ export class PermissionManager {
7071
case "ask": {
7172
this.onBeforePrompt?.();
7273
const preview = this.formatPreview(toolName, context.args);
73-
const result = await this.promptUser(toolName, preview);
74+
const result = await this.promptUser(toolName, preview, context.args);
75+
if (result.persistRule) {
76+
this.persistRule(result.persistRule);
77+
this.rules.push({
78+
tool: result.persistRule.tool,
79+
argPattern: result.persistRule.argPattern ? new RegExp(result.persistRule.argPattern) : undefined,
80+
decision: result.persistRule.decision,
81+
reason: result.persistRule.reason,
82+
priority: result.persistRule.priority ?? 5,
83+
});
84+
this.rules.sort((a, b) => b.priority - a.priority);
85+
}
7486
if (result.rememberForSession) {
7587
this.sessionGrants.add(toolName);
7688
}
7789
if (result.decision === "deny") {
78-
return { block: true, reason: "Denied by user" };
90+
return { block: true, reason: result.denyReason ?? "Denied by user" };
7991
}
8092
return undefined;
8193
}
@@ -94,6 +106,20 @@ export class PermissionManager {
94106
return Array.from(this.sessionGrants);
95107
}
96108

109+
private persistRule(rule: PermissionRuleConfig): void {
110+
const settings = loadUserSettings();
111+
const permissions = ((settings.permissions as Record<string, unknown> | undefined) ?? {});
112+
const rules = Array.isArray(permissions.rules) ? [...permissions.rules] : [];
113+
rules.push(rule);
114+
saveUserSettings({
115+
...settings,
116+
permissions: {
117+
...permissions,
118+
rules,
119+
},
120+
});
121+
}
122+
97123
private evaluate(toolName: string, argsStr: string): { decision: PermissionDecision; reason?: string } {
98124
for (const rule of this.rules) {
99125
if (rule.tool !== "*" && rule.tool !== toolName) continue;

src/ui/conversation.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,30 @@ function toolResultPreview(result: unknown): string {
4040
}
4141
}
4242

43-
interface PermOption {
44-
value: "allow" | "always_allow" | "deny";
43+
export type PermOptionValue = "allow" | "always_allow" | "explain" | "deny";
44+
45+
export interface PermOption {
46+
value: PermOptionValue;
4547
label: string;
4648
key: string;
4749
color: (s: string) => string;
4850
}
4951

50-
const PERM_OPTIONS: PermOption[] = [
52+
export const PERM_OPTIONS: PermOption[] = [
5153
{ value: "allow", label: "Allow", key: "enter", color: c.green },
52-
{ value: "deny", label: "Deny", key: "esc", color: c.red },
5354
{ value: "always_allow", label: "Always Allow", key: "a", color: c.cyan },
55+
{ value: "explain", label: "Input Idea", key: "i", color: c.yellow },
56+
{ value: "deny", label: "Deny", key: "esc", color: c.red },
5457
];
5558

59+
export function navigatePermSelection(current: number, direction: -1 | 1): number {
60+
return (current + direction + PERM_OPTIONS.length) % PERM_OPTIONS.length;
61+
}
62+
63+
export function findPermOptionByKey(input: string): PermOption | undefined {
64+
return PERM_OPTIONS.find((option) => option.key.length === 1 && option.key.toLowerCase() === input.toLowerCase());
65+
}
66+
5667
export class ConversationView {
5768
private box: Box;
5869
private textComponent: Text;
@@ -205,8 +216,7 @@ export class ConversationView {
205216
}
206217

207218
permNavigate(direction: -1 | 1): void {
208-
const max = PERM_OPTIONS.length - 1;
209-
this.permSelected = Math.max(0, Math.min(max, this.permSelected + direction));
219+
this.permSelected = navigatePermSelection(this.permSelected, direction);
210220
this.render();
211221
}
212222

@@ -242,7 +252,7 @@ export class ConversationView {
242252
lines.push(`${prefix} ${label} ${hint}`);
243253
}
244254
lines.push(c.dim(" ──────────────────────────────────────────────────"));
245-
lines.push(c.dim(" ↑↓ to navigate Enter to confirm Esc to deny"));
255+
lines.push(c.dim(" ↑↓ to navigate Enter to confirm A/I shortcuts Esc to deny"));
246256
return lines;
247257
}
248258

0 commit comments

Comments
 (0)