mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-07 06:52:28 +00:00
Implement comprehensive worktree session support allowing users to spawn sessions in temporary git worktrees. Includes backend worktree management, full-stack integration, and refined UI for session type selection. Backend: - Add worktree creation/removal utilities with branch management - Track worktree metadata (basePath, branch, name, path) in session metadata - Automatic cleanup of worktrees when sessions fail or exit - Enhanced error handling with stderr tail logging UI improvements: - Redesign session type toggle with improved alignment and spacing - Move worktree description inline with label for cleaner layout - Add branch name input field that appears when worktree mode selected - Auto-focus on worktree input when switching modes - Reduce gap between radio options from gap-3 to gap-1.5 - Update descriptive text and placeholders for clarity Integration: - Thread worktree parameters through API client, RPC handlers, and daemon - Add worktreeEnv utility to read worktree info from environment - Update session spawning to support both simple and worktree modes
214 lines
6.1 KiB
TypeScript
214 lines
6.1 KiB
TypeScript
/**
|
|
* HTTP control server for daemon management
|
|
* Provides endpoints for listing sessions, stopping sessions, and daemon shutdown
|
|
*/
|
|
|
|
import fastify, { FastifyInstance } from 'fastify';
|
|
import { z } from 'zod';
|
|
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from 'fastify-type-provider-zod';
|
|
import { logger } from '@/ui/logger';
|
|
import { Metadata } from '@/api/types';
|
|
import { TrackedSession } from './types';
|
|
import { SpawnSessionOptions, SpawnSessionResult } from '@/modules/common/registerCommonHandlers';
|
|
|
|
export function startDaemonControlServer({
|
|
getChildren,
|
|
stopSession,
|
|
spawnSession,
|
|
requestShutdown,
|
|
onHappySessionWebhook
|
|
}: {
|
|
getChildren: () => TrackedSession[];
|
|
stopSession: (sessionId: string) => boolean;
|
|
spawnSession: (options: SpawnSessionOptions) => Promise<SpawnSessionResult>;
|
|
requestShutdown: () => void;
|
|
onHappySessionWebhook: (sessionId: string, metadata: Metadata) => void;
|
|
}): Promise<{ port: number; stop: () => Promise<void> }> {
|
|
return new Promise((resolve) => {
|
|
const app = fastify({
|
|
logger: false // We use our own logger
|
|
});
|
|
|
|
// Set up Zod type provider
|
|
app.setValidatorCompiler(validatorCompiler);
|
|
app.setSerializerCompiler(serializerCompiler);
|
|
const typed = app.withTypeProvider<ZodTypeProvider>();
|
|
|
|
// Session reports itself after creation
|
|
typed.post('/session-started', {
|
|
schema: {
|
|
body: z.object({
|
|
sessionId: z.string(),
|
|
metadata: z.any() // Metadata type from API
|
|
}),
|
|
response: {
|
|
200: z.object({
|
|
status: z.literal('ok')
|
|
})
|
|
}
|
|
}
|
|
}, async (request) => {
|
|
const { sessionId, metadata } = request.body;
|
|
|
|
logger.debug(`[CONTROL SERVER] Session started: ${sessionId}`);
|
|
onHappySessionWebhook(sessionId, metadata);
|
|
|
|
return { status: 'ok' as const };
|
|
});
|
|
|
|
// List all tracked sessions
|
|
typed.post('/list', {
|
|
schema: {
|
|
response: {
|
|
200: z.object({
|
|
children: z.array(z.object({
|
|
startedBy: z.string(),
|
|
happySessionId: z.string(),
|
|
pid: z.number()
|
|
}))
|
|
})
|
|
}
|
|
}
|
|
}, async () => {
|
|
const children = getChildren();
|
|
logger.debug(`[CONTROL SERVER] Listing ${children.length} sessions`);
|
|
return {
|
|
children: children
|
|
.filter(child => child.happySessionId !== undefined)
|
|
.map(child => ({
|
|
startedBy: child.startedBy,
|
|
happySessionId: child.happySessionId!,
|
|
pid: child.pid
|
|
}))
|
|
}
|
|
});
|
|
|
|
// Stop specific session
|
|
typed.post('/stop-session', {
|
|
schema: {
|
|
body: z.object({
|
|
sessionId: z.string()
|
|
}),
|
|
response: {
|
|
200: z.object({
|
|
success: z.boolean()
|
|
})
|
|
}
|
|
}
|
|
}, async (request) => {
|
|
const { sessionId } = request.body;
|
|
|
|
logger.debug(`[CONTROL SERVER] Stop session request: ${sessionId}`);
|
|
const success = stopSession(sessionId);
|
|
return { success };
|
|
});
|
|
|
|
// Spawn new session
|
|
typed.post('/spawn-session', {
|
|
schema: {
|
|
body: z.object({
|
|
directory: z.string(),
|
|
sessionId: z.string().optional(),
|
|
sessionType: z.enum(['simple', 'worktree']).optional(),
|
|
worktreeName: z.string().optional()
|
|
}),
|
|
response: {
|
|
200: z.object({
|
|
success: z.boolean(),
|
|
sessionId: z.string().optional(),
|
|
approvedNewDirectoryCreation: z.boolean().optional()
|
|
}),
|
|
409: z.object({
|
|
success: z.boolean(),
|
|
requiresUserApproval: z.boolean().optional(),
|
|
actionRequired: z.string().optional(),
|
|
directory: z.string().optional()
|
|
}),
|
|
500: z.object({
|
|
success: z.boolean(),
|
|
error: z.string().optional()
|
|
})
|
|
}
|
|
}
|
|
}, async (request, reply) => {
|
|
const { directory, sessionId, sessionType, worktreeName } = request.body;
|
|
|
|
logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || 'new'}`);
|
|
const result = await spawnSession({ directory, sessionId, sessionType, worktreeName });
|
|
|
|
switch (result.type) {
|
|
case 'success':
|
|
// Check if sessionId exists, if not return error
|
|
if (!result.sessionId) {
|
|
reply.code(500);
|
|
return {
|
|
success: false,
|
|
error: 'Failed to spawn session: no session ID returned'
|
|
};
|
|
}
|
|
return {
|
|
success: true,
|
|
sessionId: result.sessionId,
|
|
approvedNewDirectoryCreation: true
|
|
};
|
|
|
|
case 'requestToApproveDirectoryCreation':
|
|
reply.code(409); // Conflict - user input needed
|
|
return {
|
|
success: false,
|
|
requiresUserApproval: true,
|
|
actionRequired: 'CREATE_DIRECTORY',
|
|
directory: result.directory
|
|
};
|
|
|
|
case 'error':
|
|
reply.code(500);
|
|
return {
|
|
success: false,
|
|
error: result.errorMessage
|
|
};
|
|
}
|
|
});
|
|
|
|
// Stop daemon
|
|
typed.post('/stop', {
|
|
schema: {
|
|
response: {
|
|
200: z.object({
|
|
status: z.string()
|
|
})
|
|
}
|
|
}
|
|
}, async () => {
|
|
logger.debug('[CONTROL SERVER] Stop daemon request received');
|
|
|
|
// Give time for response to arrive
|
|
setTimeout(() => {
|
|
logger.debug('[CONTROL SERVER] Triggering daemon shutdown');
|
|
requestShutdown();
|
|
}, 50);
|
|
|
|
return { status: 'stopping' };
|
|
});
|
|
|
|
app.listen({ port: 0, host: '127.0.0.1' }, (err, address) => {
|
|
if (err) {
|
|
logger.debug('[CONTROL SERVER] Failed to start:', err);
|
|
throw err;
|
|
}
|
|
|
|
const port = parseInt(address.split(':').pop()!);
|
|
logger.debug(`[CONTROL SERVER] Started on port ${port}`);
|
|
|
|
resolve({
|
|
port,
|
|
stop: async () => {
|
|
logger.debug('[CONTROL SERVER] Stopping server');
|
|
await app.close();
|
|
logger.debug('[CONTROL SERVER] Server stopped');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|