feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
178
frontend/src/features/first-run/useFirstRun.ts
Normal file
178
frontend/src/features/first-run/useFirstRun.ts
Normal file
@ -0,0 +1,178 @@
|
||||
/**
|
||||
* `useFirstRun` — view-model for the first-run wizard and profile management
|
||||
* (L5). Owns the wizard state (reference candidates + selection + availability +
|
||||
* custom profiles) and the actions. Consumes the {@link ProfileGateway}
|
||||
* exclusively — never `invoke()` — so the feature is testable with a mock.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import type {
|
||||
AgentProfile,
|
||||
FirstRunState,
|
||||
GatewayError,
|
||||
ProfileAvailability,
|
||||
} from "@/domain";
|
||||
import { useGateways } from "@/app/di";
|
||||
|
||||
/** One row in the wizard: a candidate profile, selected state, availability. */
|
||||
export interface WizardEntry {
|
||||
profile: AgentProfile;
|
||||
selected: boolean;
|
||||
/** `null` until detection ran; then `true`/`false`. */
|
||||
available: boolean | null;
|
||||
}
|
||||
|
||||
/** What the first-run wizard UI needs from this hook. */
|
||||
export interface FirstRunViewModel {
|
||||
/** Whether the wizard should be shown (null while loading). */
|
||||
isFirstRun: boolean | null;
|
||||
/** The candidate rows (reference + added custom profiles). */
|
||||
entries: WizardEntry[];
|
||||
/** Last error message, or null. */
|
||||
error: string | null;
|
||||
/** Whether a request is in flight. */
|
||||
busy: boolean;
|
||||
/** Toggles selection of a candidate by id. */
|
||||
toggle: (id: string) => void;
|
||||
/** Replaces a candidate's profile (edited command/args/injection). */
|
||||
updateProfile: (id: string, profile: AgentProfile) => void;
|
||||
/** Adds a custom profile (selected by default). */
|
||||
addCustom: (profile: AgentProfile) => void;
|
||||
/** Removes a candidate by id (e.g. a custom one). */
|
||||
remove: (id: string) => void;
|
||||
/** Runs detection over all candidates, filling availability. */
|
||||
detect: () => Promise<void>;
|
||||
/** Persists the selected profiles and closes the wizard. */
|
||||
finish: () => Promise<void>;
|
||||
/** Re-checks first-run state (e.g. reopening profile management). */
|
||||
reload: () => Promise<void>;
|
||||
}
|
||||
|
||||
function describe(e: unknown): string {
|
||||
if (e && typeof e === "object" && "message" in e) {
|
||||
return String((e as GatewayError).message);
|
||||
}
|
||||
return String(e);
|
||||
}
|
||||
|
||||
export function useFirstRun(): FirstRunViewModel {
|
||||
const { profile } = useGateways();
|
||||
const [isFirstRun, setIsFirstRun] = useState<boolean | null>(null);
|
||||
const [entries, setEntries] = useState<WizardEntry[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const state: FirstRunState = await profile.firstRunState();
|
||||
setIsFirstRun(state.isFirstRun);
|
||||
const refs = state.referenceProfiles;
|
||||
// Show the rows immediately (nothing pre-checked yet).
|
||||
setEntries(
|
||||
refs.map((p) => ({ profile: p, selected: false, available: null })),
|
||||
);
|
||||
// Auto-detect once on open: only the CLIs actually installed end up
|
||||
// pre-checked. The manual "Detect installed CLIs" button stays available to
|
||||
// re-probe. If detection is unavailable, rows stay unchecked (the user can
|
||||
// still tick them by hand and re-run detection).
|
||||
try {
|
||||
const results = await profile.detectProfiles(refs);
|
||||
const byId = new Map(results.map((r) => [r.profile.id, r.available]));
|
||||
setEntries(
|
||||
refs.map((p) => {
|
||||
const available = byId.get(p.id) ?? null;
|
||||
return { profile: p, selected: available === true, available };
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
/* detection unavailable: leave rows unchecked */
|
||||
}
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
setIsFirstRun(false);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [profile]);
|
||||
|
||||
useEffect(() => {
|
||||
void reload();
|
||||
}, [reload]);
|
||||
|
||||
const toggle = useCallback((id: string) => {
|
||||
setEntries((prev) =>
|
||||
prev.map((e) =>
|
||||
e.profile.id === id ? { ...e, selected: !e.selected } : e,
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const updateProfile = useCallback((id: string, next: AgentProfile) => {
|
||||
setEntries((prev) =>
|
||||
prev.map((e) => (e.profile.id === id ? { ...e, profile: next } : e)),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const addCustom = useCallback((p: AgentProfile) => {
|
||||
setEntries((prev) => [
|
||||
...prev,
|
||||
{ profile: p, selected: true, available: null },
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const remove = useCallback((id: string) => {
|
||||
setEntries((prev) => prev.filter((e) => e.profile.id !== id));
|
||||
}, []);
|
||||
|
||||
const detect = useCallback(async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const candidates = entries.map((e) => e.profile);
|
||||
const results: ProfileAvailability[] =
|
||||
await profile.detectProfiles(candidates);
|
||||
const byId = new Map(results.map((r) => [r.profile.id, r.available]));
|
||||
setEntries((prev) =>
|
||||
prev.map((e) => ({
|
||||
...e,
|
||||
available: byId.get(e.profile.id) ?? e.available,
|
||||
})),
|
||||
);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [profile, entries]);
|
||||
|
||||
const finish = useCallback(async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const chosen = entries.filter((e) => e.selected).map((e) => e.profile);
|
||||
await profile.configureProfiles(chosen);
|
||||
setIsFirstRun(false);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [profile, entries]);
|
||||
|
||||
return {
|
||||
isFirstRun,
|
||||
entries,
|
||||
error,
|
||||
busy,
|
||||
toggle,
|
||||
updateProfile,
|
||||
addCustom,
|
||||
remove,
|
||||
detect,
|
||||
finish,
|
||||
reload,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user