Intelligence
Artifacts
Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.
Repository
assignment-dispatch-button.tsxassignment-status-badge.tsxassignments-panel.tsxcreate-mission-form.tsxgovernance-panels.tsxmission-dispatch.tsxmission-edit.tsxmission-state-actions.tsxmission-status-badge.tsxobjective-edit.tsxobjective-status-badge.tsxobjective-status-select.tsxobjectives-panel.tsxreport-list.tsxwork-orders-panel.tsx
README.md
CONSTITUTION_COMPLIANCE_AUDIT_V1.mdREADME.md
repositories/aaf-holdings/hq01/components/sessions/start-session-launcher.tsx
7.3 KB
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Plus, X, Play, Loader2, Zap } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import type { SessionPreset } from "@/lib/sessions/presets";
import type { Session, StartSessionInput } from "@/lib/sessions/types";
/**
* Start-session control. Collapsed by default to a "Start Session" button;
* expands to a form. Presets (e.g. Agent Z) prefill the form for one-tap
* launches. On success it navigates to the new session's detail page.
*
* Presets are passed in from the server because they resolve repository paths
* via the filesystem-backed content config, which cannot run in the browser.
*/
export function StartSessionLauncher({ presets }: { presets: SessionPreset[] }) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [form, setForm] = useState<StartSessionInput>(EMPTY);
function applyPreset(p: SessionPreset) {
setForm({
name: p.name,
executive: p.executive ?? "",
working_directory: p.working_directory,
branch: p.branch ?? "",
mission_id: p.mission_id ?? "",
assignment_id: p.assignment_id ?? "",
prompt: p.prompt ?? "",
});
setOpen(true);
setError(null);
}
async function submit(input: StartSessionInput) {
setBusy(true);
setError(null);
try {
const res = await fetch("/api/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error ?? "Failed to start session.");
const session = data.session as Session;
router.push(`/sessions/${session.id}`);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to start session.");
setBusy(false);
}
}
return (
<div className="flex flex-col items-stretch gap-3 sm:items-end">
<div className="flex flex-wrap items-center justify-end gap-2">
{presets.map((p) => (
<Button
key={p.key}
type="button"
variant="outline"
size="sm"
onClick={() => applyPreset(p)}
disabled={busy}
title={p.blurb}
>
<Zap className="h-3.5 w-3.5" />
Start {p.label}
</Button>
))}
<Button
type="button"
size="sm"
onClick={() => {
setForm(EMPTY);
setOpen((v) => !v);
setError(null);
}}
>
{open ? <X className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
{open ? "Close" : "Start Session"}
</Button>
</div>
{open && (
<Card className="w-full max-w-xl p-5 text-left">
<form
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
submit(form);
}}
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<FormField label="Name" required>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="Agent Z"
required
/>
</FormField>
<FormField label="Executive">
<Input
value={form.executive ?? ""}
onChange={(e) => setForm({ ...form, executive: e.target.value })}
placeholder="agent-z"
/>
</FormField>
</div>
<FormField label="Working directory" required>
<Input
value={form.working_directory}
onChange={(e) =>
setForm({ ...form, working_directory: e.target.value })
}
placeholder="/srv/aaf/repositories/aaf-holdings"
className="font-mono text-xs"
required
/>
</FormField>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<FormField label="Branch">
<Input
value={form.branch ?? ""}
onChange={(e) => setForm({ ...form, branch: e.target.value })}
placeholder="mission/MS-0001"
className="font-mono text-xs"
/>
</FormField>
<FormField label="Mission">
<Input
value={form.mission_id ?? ""}
onChange={(e) => setForm({ ...form, mission_id: e.target.value })}
placeholder="MS-0001"
/>
</FormField>
<FormField label="Assignment">
<Input
value={form.assignment_id ?? ""}
onChange={(e) =>
setForm({ ...form, assignment_id: e.target.value })
}
placeholder="—"
/>
</FormField>
</div>
<FormField label="Mission prompt">
<textarea
value={form.prompt ?? ""}
onChange={(e) => setForm({ ...form, prompt: e.target.value })}
placeholder="What should this session work on?"
rows={3}
className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
/>
<p className="mt-1 text-[11px] text-muted-foreground">
Passed to the Claude CLI as <code>--print</code> input. Leave blank
to pass custom arguments instead.
</p>
</FormField>
{error && (
<div className="rounded-md bg-destructive/10 px-3 py-2 text-[13px] text-destructive">
{error}
</div>
)}
<div className="flex justify-end">
<Button type="submit" disabled={busy}>
{busy ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Play className="h-4 w-4" />
)}
Launch session
</Button>
</div>
</form>
</Card>
)}
</div>
);
}
const EMPTY: StartSessionInput = {
name: "",
executive: "",
working_directory: "",
branch: "",
mission_id: "",
assignment_id: "",
prompt: "",
};
function FormField({
label,
required,
children,
}: {
label: string;
required?: boolean;
children: React.ReactNode;
}) {
return (
<label className="block">
<span
className={cn(
"mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground",
)}
>
{label}
{required && <span className="ml-0.5 text-destructive">*</span>}
</span>
{children}
</label>
);
}
root · /srv/aaf