Files
hapi/cli/src/claude/session.ts
T
bd13fac7ae fix(claude): apply mid-turn permission mode changes to canCallTool (#764)
`PermissionHandler` stored its own `permissionMode` field and only updated
it inside `handleModeChange`, which is called when a new batch is pulled
from the queue. The `SetSessionConfig` RPC (web dropdown changes) updates
`runClaude.ts`'s `currentPermissionMode` and the session keepalive
metadata, but never reaches the handler — so switching to Yolo mid-turn
left `canCallTool` checking the stale mode and still prompting for
approval. Closes #735.

Drop the stored field and read live from `session.getPermissionMode()`,
mirroring how the OpenCode permission handler already works. Override
`Session.getPermissionMode()` in `claude/session.ts` to return the
Claude-narrow `PermissionMode`, sound because the matching
`setPermissionMode` setter only accepts that subset.

via [HAPI](https://hapi.run)

Co-authored-by: HAPI <noreply@hapi.run>
2026-06-01 18:50:54 +08:00

143 lines
5.2 KiB
TypeScript

import { ApiClient, ApiSessionClient } from '@/lib';
import { MessageQueue2 } from '@/utils/MessageQueue2';
import { logger } from '@/ui/logger';
import { AgentSessionBase } from '@/agent/sessionBase';
import type { SessionEffort, SessionModel } from '@/api/types';
import type { EnhancedMode } from './loop';
import type { PermissionMode } from './loop';
import type { LocalLaunchExitReason } from '@/agent/localLaunchPolicy';
type LocalLaunchFailure = {
message: string;
exitReason: LocalLaunchExitReason;
};
export class Session extends AgentSessionBase<EnhancedMode> {
readonly claudeEnvVars?: Record<string, string>;
claudeArgs?: string[];
readonly mcpServers: Record<string, any>;
readonly allowedTools?: string[];
readonly hookSettingsPath: string;
readonly startedBy: 'runner' | 'terminal';
readonly startingMode: 'local' | 'remote';
localLaunchFailure: LocalLaunchFailure | null = null;
constructor(opts: {
api: ApiClient;
client: ApiSessionClient;
path: string;
logPath: string;
sessionId: string | null;
claudeEnvVars?: Record<string, string>;
claudeArgs?: string[];
mcpServers: Record<string, any>;
messageQueue: MessageQueue2<EnhancedMode>;
onModeChange: (mode: 'local' | 'remote') => void;
allowedTools?: string[];
mode?: 'local' | 'remote';
startedBy: 'runner' | 'terminal';
startingMode: 'local' | 'remote';
hookSettingsPath: string;
permissionMode?: PermissionMode;
model?: SessionModel;
effort?: SessionEffort;
}) {
super({
api: opts.api,
client: opts.client,
path: opts.path,
logPath: opts.logPath,
sessionId: opts.sessionId,
messageQueue: opts.messageQueue,
onModeChange: opts.onModeChange,
mode: opts.mode,
sessionLabel: 'Session',
sessionIdLabel: 'Claude Code',
applySessionIdToMetadata: (metadata, sessionId) => ({
...metadata,
claudeSessionId: sessionId
}),
permissionMode: opts.permissionMode,
model: opts.model,
effort: opts.effort
});
this.claudeEnvVars = opts.claudeEnvVars;
this.claudeArgs = opts.claudeArgs;
this.mcpServers = opts.mcpServers;
this.allowedTools = opts.allowedTools;
this.hookSettingsPath = opts.hookSettingsPath;
this.startedBy = opts.startedBy;
this.startingMode = opts.startingMode;
this.permissionMode = opts.permissionMode;
this.model = opts.model;
this.effort = opts.effort;
}
setPermissionMode = (mode: PermissionMode): void => {
this.permissionMode = mode;
};
// Override base getPermissionMode to return the Claude-narrow type. Safe
// because the only writer (setPermissionMode above) accepts only Claude
// PermissionMode, so the field cannot hold a foreign flavor value.
getPermissionMode(): PermissionMode | undefined {
return this.permissionMode as PermissionMode | undefined;
}
setModel = (model: SessionModel): void => {
this.model = model;
};
setEffort = (effort: SessionEffort): void => {
this.effort = effort;
};
recordLocalLaunchFailure = (message: string, exitReason: LocalLaunchExitReason): void => {
this.localLaunchFailure = { message, exitReason };
};
/**
* Clear the current session ID (used by /clear command)
*/
clearSessionId = (): void => {
this.sessionId = null;
logger.debug('[Session] Session ID cleared');
};
/**
* Consume one-time Claude flags from claudeArgs after Claude spawn
* Currently handles: --resume (with or without session ID)
*/
consumeOneTimeFlags = (): void => {
if (!this.claudeArgs) return;
const filteredArgs: string[] = [];
for (let i = 0; i < this.claudeArgs.length; i++) {
if (this.claudeArgs[i] === '--resume') {
// Check if next arg looks like a UUID (contains dashes and alphanumeric)
if (i + 1 < this.claudeArgs.length) {
const nextArg = this.claudeArgs[i + 1];
// Simple UUID pattern check - contains dashes and is not another flag
if (!nextArg.startsWith('-') && nextArg.includes('-')) {
// Skip both --resume and the UUID
i++; // Skip the UUID
logger.debug(`[Session] Consumed --resume flag with session ID: ${nextArg}`);
} else {
// Just --resume without UUID
logger.debug('[Session] Consumed --resume flag (no session ID)');
}
} else {
// --resume at the end of args
logger.debug('[Session] Consumed --resume flag (no session ID)');
}
} else {
filteredArgs.push(this.claudeArgs[i]);
}
}
this.claudeArgs = filteredArgs.length > 0 ? filteredArgs : undefined;
logger.debug(`[Session] Consumed one-time flags, remaining args:`, this.claudeArgs);
};
}