mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: create new session with yolo mode (#13)
* feat: create new session with yolo mode Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * fix: harden sequence typecheck * feat: add unified --yolo flag across all agents --------- Signed-off-by: Ruihang Xia <waynestxia@gmail.com> Co-authored-by: weishu <twsxtd@gmail.com>
This commit is contained in:
@@ -70,13 +70,21 @@ export class ApiMachineClient {
|
||||
|
||||
setRPCHandlers({ spawnSession, stopSession, requestShutdown }: MachineRpcHandlers): void {
|
||||
this.rpcHandlerManager.registerHandler('spawn-happy-session', async (params: any) => {
|
||||
const { directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token } = params || {}
|
||||
const { directory, sessionId, machineId, approvedNewDirectoryCreation, agent, yolo, token } = params || {}
|
||||
|
||||
if (!directory) {
|
||||
throw new Error('Directory is required')
|
||||
}
|
||||
|
||||
const result = await spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token })
|
||||
const result = await spawnSession({
|
||||
directory,
|
||||
sessionId,
|
||||
machineId,
|
||||
approvedNewDirectoryCreation,
|
||||
agent,
|
||||
yolo,
|
||||
token
|
||||
})
|
||||
|
||||
switch (result.type) {
|
||||
case 'success':
|
||||
|
||||
@@ -22,6 +22,11 @@ describe('parseCodexCliOverrides', () => {
|
||||
approvalPolicy: 'on-request'
|
||||
});
|
||||
|
||||
expect(parseCodexCliOverrides(['--yolo'])).toEqual({
|
||||
sandbox: 'danger-full-access',
|
||||
approvalPolicy: 'never'
|
||||
});
|
||||
|
||||
expect(parseCodexCliOverrides(['--dangerously-bypass-approvals-and-sandbox'])).toEqual({
|
||||
sandbox: 'danger-full-access',
|
||||
approvalPolicy: 'never'
|
||||
|
||||
@@ -34,6 +34,12 @@ export function parseCodexCliOverrides(args?: string[]): CodexCliOverrides {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--yolo') {
|
||||
overrides.approvalPolicy = 'never';
|
||||
overrides.sandbox = 'danger-full-access';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--dangerously-bypass-approvals-and-sandbox') {
|
||||
overrides.approvalPolicy = 'never';
|
||||
overrides.sandbox = 'danger-full-access';
|
||||
|
||||
@@ -186,6 +186,8 @@ export async function startDaemon(): Promise<void> {
|
||||
logger.debugLargeJson('[DAEMON RUN] Spawning session', options);
|
||||
|
||||
const { directory, sessionId, machineId, approvedNewDirectoryCreation = true } = options;
|
||||
const agent = options.agent ?? 'claude';
|
||||
const yolo = options.yolo === true;
|
||||
let directoryCreated = false;
|
||||
|
||||
try {
|
||||
@@ -256,9 +258,9 @@ export async function startDaemon(): Promise<void> {
|
||||
}
|
||||
|
||||
// Construct arguments for the CLI
|
||||
const agentCommand = options.agent === 'codex'
|
||||
const agentCommand = agent === 'codex'
|
||||
? 'codex'
|
||||
: options.agent === 'gemini'
|
||||
: agent === 'gemini'
|
||||
? 'gemini'
|
||||
: 'claude';
|
||||
const args = [
|
||||
@@ -266,6 +268,9 @@ export async function startDaemon(): Promise<void> {
|
||||
'--hapi-starting-mode', 'remote',
|
||||
'--started-by', 'daemon'
|
||||
];
|
||||
if (yolo) {
|
||||
args.push('--yolo');
|
||||
}
|
||||
|
||||
// TODO: In future, sessionId could be used with --resume to continue existing sessions
|
||||
// For now, we ignore it - each spawn creates a new session
|
||||
|
||||
+16
-3
@@ -147,16 +147,29 @@ import { getCliArgs } from './utils/cliArgs'
|
||||
} else if (subcommand === 'gemini') {
|
||||
// Handle gemini command
|
||||
try {
|
||||
await import('./agent/runners/gemini');
|
||||
const { runAgentSession } = await import('./agent/runners/runAgentSession');
|
||||
|
||||
let startedBy: 'daemon' | 'terminal' | undefined = undefined;
|
||||
let yolo = false;
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
if (args[i] === '--started-by') {
|
||||
startedBy = args[++i] as 'daemon' | 'terminal';
|
||||
} else if (args[i] === '--yolo') {
|
||||
yolo = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (yolo) {
|
||||
const existingArgs = process.env.HAPPY_GEMINI_ARGS ?? process.env.GEMINI_ACP_ARGS ?? '';
|
||||
if (!existingArgs.includes('--yolo')) {
|
||||
const nextArgs = existingArgs.trim().length > 0
|
||||
? `${existingArgs} --yolo`
|
||||
: '--yolo';
|
||||
process.env.HAPPY_GEMINI_ARGS = nextArgs;
|
||||
}
|
||||
}
|
||||
|
||||
await import('./agent/runners/gemini');
|
||||
const { runAgentSession } = await import('./agent/runners/runAgentSession');
|
||||
|
||||
await initializeToken();
|
||||
await authAndSetupMachineIfNeeded();
|
||||
await runAgentSession({ agentType: 'gemini', startedBy });
|
||||
|
||||
@@ -122,6 +122,7 @@ export interface SpawnSessionOptions {
|
||||
sessionId?: string;
|
||||
approvedNewDirectoryCreation?: boolean;
|
||||
agent?: 'claude' | 'codex' | 'gemini';
|
||||
yolo?: boolean;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,13 @@ export function useSwitchControls(opts: {
|
||||
}, confirmationTimeoutMs);
|
||||
}, [confirmationTimeoutMs, resetConfirmation]);
|
||||
|
||||
const readKeyString = useCallback((keyLike: unknown, prop: 'name' | 'sequence'): string | undefined => {
|
||||
if (!keyLike || typeof keyLike !== 'object') return undefined;
|
||||
if (!(prop in keyLike)) return undefined;
|
||||
const value = (keyLike as Record<string, unknown>)[prop];
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (confirmationTimeoutRef.current) {
|
||||
@@ -78,14 +85,18 @@ export function useSwitchControls(opts: {
|
||||
return;
|
||||
}
|
||||
|
||||
const sequence = typeof key.sequence === 'string' ? key.sequence : input;
|
||||
const isKeyRelease = typeof sequence === 'string' && /^\u001b\[[0-9;]*:3u$/.test(sequence);
|
||||
const csiUMatch = typeof sequence === 'string'
|
||||
? sequence.match(/^\u001b\[(\d+)(?:;(\d+))?u$/)
|
||||
const keySequence = readKeyString(key, 'sequence');
|
||||
const keyName = readKeyString(key, 'name');
|
||||
const sequence = keySequence ?? input;
|
||||
const sequenceString = typeof sequence === 'string' ? sequence : '';
|
||||
const isKeyRelease = sequenceString.length > 0
|
||||
&& /^\u001b\[[0-9;]*:3u$/.test(sequenceString);
|
||||
const csiUMatch = sequenceString.length > 0
|
||||
? sequenceString.match(/^\u001b\[(\d+)(?:;(\d+))?u$/)
|
||||
: null;
|
||||
const csiUCodepoint = csiUMatch ? Number(csiUMatch[1]) : null;
|
||||
const isCsiUSpace = csiUCodepoint === 32;
|
||||
const isSpace = Boolean(onSwitch) && !isKeyRelease && (input === ' ' || key.name === 'space' || isCsiUSpace);
|
||||
const isSpace = Boolean(onSwitch) && !isKeyRelease && (input === ' ' || keyName === 'space' || isCsiUSpace);
|
||||
const hasPrintableInput = typeof input === 'string' && input.length > 0;
|
||||
|
||||
if (isSpace) {
|
||||
|
||||
@@ -652,10 +652,15 @@ export class SyncEngine {
|
||||
async spawnSession(
|
||||
machineId: string,
|
||||
directory: string,
|
||||
agent: 'claude' | 'codex' | 'gemini' = 'claude'
|
||||
agent: 'claude' | 'codex' | 'gemini' = 'claude',
|
||||
yolo?: boolean
|
||||
): Promise<{ type: 'success'; sessionId: string } | { type: 'error'; message: string }> {
|
||||
try {
|
||||
const result = await this.machineRpc(machineId, 'spawn-happy-session', { type: 'spawn-in-directory', directory, agent })
|
||||
const result = await this.machineRpc(
|
||||
machineId,
|
||||
'spawn-happy-session',
|
||||
{ type: 'spawn-in-directory', directory, agent, yolo }
|
||||
)
|
||||
if (result && typeof result === 'object') {
|
||||
const obj = result as Record<string, unknown>
|
||||
if (obj.type === 'success' && typeof obj.sessionId === 'string') {
|
||||
|
||||
@@ -5,7 +5,8 @@ import type { WebAppEnv } from '../middleware/auth'
|
||||
|
||||
const spawnBodySchema = z.object({
|
||||
directory: z.string().min(1),
|
||||
agent: z.enum(['claude', 'codex', 'gemini']).optional()
|
||||
agent: z.enum(['claude', 'codex', 'gemini']).optional(),
|
||||
yolo: z.boolean().optional()
|
||||
})
|
||||
|
||||
export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Hono<WebAppEnv> {
|
||||
@@ -39,7 +40,12 @@ export function createMachinesRoutes(getSyncEngine: () => SyncEngine | null): Ho
|
||||
return c.json({ error: 'Invalid body' }, 400)
|
||||
}
|
||||
|
||||
const result = await engine.spawnSession(machineId, parsed.data.directory, parsed.data.agent)
|
||||
const result = await engine.spawnSession(
|
||||
machineId,
|
||||
parsed.data.directory,
|
||||
parsed.data.agent,
|
||||
parsed.data.yolo
|
||||
)
|
||||
return c.json(result)
|
||||
})
|
||||
|
||||
|
||||
@@ -227,10 +227,15 @@ export class ApiClient {
|
||||
return await this.request<MachinesResponse>('/api/machines')
|
||||
}
|
||||
|
||||
async spawnSession(machineId: string, directory: string, agent?: 'claude' | 'codex' | 'gemini'): Promise<SpawnResponse> {
|
||||
async spawnSession(
|
||||
machineId: string,
|
||||
directory: string,
|
||||
agent?: 'claude' | 'codex' | 'gemini',
|
||||
yolo?: boolean
|
||||
): Promise<SpawnResponse> {
|
||||
return await this.request<SpawnResponse>(`/api/machines/${encodeURIComponent(machineId)}/spawn`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ directory, agent })
|
||||
body: JSON.stringify({ directory, agent, yolo })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export function NewSession(props: {
|
||||
const [machineId, setMachineId] = useState<string | null>(null)
|
||||
const [directory, setDirectory] = useState('')
|
||||
const [agent, setAgent] = useState<AgentType>('claude')
|
||||
const [yoloMode, setYoloMode] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Initialize with last used machine or first available
|
||||
@@ -83,6 +84,7 @@ export function NewSession(props: {
|
||||
machineId,
|
||||
directory: directory.trim(),
|
||||
agent,
|
||||
yolo: yoloMode
|
||||
})
|
||||
|
||||
if (result.type === 'success') {
|
||||
@@ -194,6 +196,34 @@ export function NewSession(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* YOLO Mode */}
|
||||
<div className="flex flex-col gap-1.5 px-3 py-3">
|
||||
<label className="text-xs font-medium text-[var(--app-hint)]">
|
||||
YOLO mode
|
||||
</label>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-[var(--app-fg)]">
|
||||
Bypass approvals and sandbox
|
||||
</span>
|
||||
<span className="text-xs text-[var(--app-hint)]">
|
||||
Uses dangerous agent flags when spawning.
|
||||
</span>
|
||||
</div>
|
||||
<label className="relative inline-flex h-5 w-9 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={yoloMode}
|
||||
onChange={(e) => setYoloMode(e.target.checked)}
|
||||
disabled={isFormDisabled}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<span className="absolute inset-0 rounded-full bg-[var(--app-border)] transition-colors peer-checked:bg-[var(--app-link)] peer-disabled:opacity-50" />
|
||||
<span className="absolute left-0.5 h-4 w-4 rounded-full bg-[var(--app-bg)] transition-transform peer-checked:translate-x-4 peer-disabled:opacity-50" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{(error ?? spawnError) ? (
|
||||
<div className="px-3 py-2 text-sm text-red-600">
|
||||
|
||||
@@ -7,6 +7,7 @@ type SpawnInput = {
|
||||
machineId: string
|
||||
directory: string
|
||||
agent?: 'claude' | 'codex' | 'gemini'
|
||||
yolo?: boolean
|
||||
}
|
||||
|
||||
export function useSpawnSession(api: ApiClient | null): {
|
||||
@@ -21,7 +22,7 @@ export function useSpawnSession(api: ApiClient | null): {
|
||||
if (!api) {
|
||||
throw new Error('API unavailable')
|
||||
}
|
||||
return await api.spawnSession(input.machineId, input.directory, input.agent)
|
||||
return await api.spawnSession(input.machineId, input.directory, input.agent, input.yolo)
|
||||
},
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.sessions })
|
||||
|
||||
Reference in New Issue
Block a user