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:
2026-06-06 01:27:01 +02:00
parent 55b3bee2c8
commit 307ae71857
273 changed files with 48740 additions and 0 deletions

View 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,
};
}