Intelligence

Artifacts

Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.

Repository
.gitignoreDockerfilenext-env.d.tsnext.config.mjspackage-lock.jsonpackage.jsonpostcss.config.mjsREADME.mdtailwind.config.tstsconfig.jsontsconfig.tsbuildinfo
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