import { useState, useEffect, useCallback } from 'react';
import { readFile, readdir, writeFile } from 'fs/promises';
import { existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import type { Session, StepStatus } from '../types.js';
const SESSIONS_PATH = join(homedir(), '.user-steps', 'sessions');
export interface UseSessionReturn {
session: Session | null;
loading: boolean;
error: Error | null;
updateStep: (stepId: string, status: StepStatus, notes?: string) => Promise<void>;
cancelSession: () => Promise<void>;
refresh: () => Promise<void>;
}
export function useSession(sessionId?: string): UseSessionReturn {
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const loadSession = useCallback(async () => {
try {
let loadedSession: Session | null = null;
if (sessionId) {
// Load specific session
const path = join(SESSIONS_PATH, `${sessionId}.json`);
if (existsSync(path)) {
const content = await readFile(path, 'utf-8');
loadedSession = JSON.parse(content);
}
} else {
// Find latest active session
if (existsSync(SESSIONS_PATH)) {
const files = await readdir(SESSIONS_PATH);
const jsonFiles = files.filter(f => f.endsWith('.json'));
let latestTime = 0;
for (const file of jsonFiles) {
const path = join(SESSIONS_PATH, file);
const content = await readFile(path, 'utf-8');
const sess = JSON.parse(content) as Session;
if (sess.status === 'active') {
const time = new Date(sess.createdAt).getTime();
if (time > latestTime) {
latestTime = time;
loadedSession = sess;
}
}
}
}
}
setSession(loadedSession);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error(String(err)));
} finally {
setLoading(false);
}
}, [sessionId]);
const saveSession = useCallback(async (updatedSession: Session) => {
const path = join(SESSIONS_PATH, `${updatedSession.id}.json`);
await writeFile(path, JSON.stringify(updatedSession, null, 2), 'utf-8');
}, []);
const autoSkipDependentSteps = useCallback((steps: Session['steps'], skippedStepId: string): Session['steps'] => {
const newSteps = [...steps];
// Find all steps that depend on the skipped step
const dependentSteps = newSteps.filter(s =>
s.dependsOn?.includes(skippedStepId)
);
// Recursively skip dependent steps
for (const depStep of dependentSteps) {
if (depStep.status === 'pending') {
depStep.status = 'skipped';
depStep.completedAt = new Date().toISOString();
// Recursively skip steps that depend on this one
return autoSkipDependentSteps(newSteps, depStep.id);
}
}
return newSteps;
}, []);
const updateStep = useCallback(async (stepId: string, status: StepStatus, notes?: string) => {
if (!session) return;
let updatedSteps = session.steps.map(step => {
if (step.id === stepId) {
return {
...step,
status,
completedAt: status === 'completed' || status === 'skipped'
? new Date().toISOString()
: undefined,
notes,
};
}
return step;
});
// If a step is skipped, auto-skip all dependent steps
if (status === 'skipped') {
updatedSteps = autoSkipDependentSteps(updatedSteps, stepId);
}
// Check if all required steps are complete
const allRequiredComplete = updatedSteps
.filter(s => s.required)
.every(s => s.status === 'completed');
const allComplete = updatedSteps.every(
s => s.status === 'completed' || s.status === 'skipped'
);
let newStatus = session.status;
let completedAt = session.completedAt;
if (allRequiredComplete && (allComplete || session.allowPartialCompletion)) {
newStatus = 'completed';
completedAt = new Date().toISOString();
}
const updatedSession: Session = {
...session,
steps: updatedSteps,
status: newStatus,
completedAt,
};
await saveSession(updatedSession);
setSession(updatedSession);
}, [session, saveSession, autoSkipDependentSteps]);
const cancelSession = useCallback(async () => {
if (!session) return;
const updatedSession: Session = {
...session,
status: 'cancelled',
completedAt: new Date().toISOString(),
};
await saveSession(updatedSession);
setSession(updatedSession);
}, [session, saveSession]);
// Initial load
useEffect(() => {
loadSession();
}, [loadSession]);
// Poll for external updates (in case user opens multiple terminals)
useEffect(() => {
if (!session || session.status !== 'active') return;
const interval = setInterval(loadSession, 1000);
return () => clearInterval(interval);
}, [session?.id, session?.status, loadSession]);
return {
session,
loading,
error,
updateStep,
cancelSession,
refresh: loadSession,
};
}