P3 exit: Computer panel with all 8 apps, themed and deep-linkable
- DevicePanel in the 448px SlidePanel: ?app= routes (home + 8 sub-apps via dynamic imports), ?device=full|tablet|phone size toggle, per-agent accent-derived wallpaper theme + feTurbulence grain, glassy dock + grid home screen, Computer button in the chat header - Apps: Files (3 drives, real listings), Skills (installed + add from library), Routines (list/refresh/empty state), Claw Chat (threads + sensitive badge + detail), Settings (push/pop nav: edit profile PATCHes the system prompt, Other-Claws access toggle PUTs the policy, confirmed destructive delete), Slack (§7.3 pre-connect gate), Add Apps (live /api/apps directory + search), Browser (chrome + spec'd empty state) - /skills Skill Library page + nav entry; curated /api/apps directory endpoint; e2e seed gains a catalog skill - P3 exit E2E (6 journeys): themed home screen + device toggle in URL, agent-written file appears in Files, agent-scheduled routine appears in Routines, system-prompt edit persists across reload, deep-link cold-load of ?app=settings&device=full, every app reachable, library installs 132 Rust + 63 frontend tests + 20 Playwright journeys. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
67f918439c
commit
1fd2c287f1
@@ -0,0 +1,197 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, type FormEvent } from "react";
|
||||
|
||||
import type { Agent } from "@/lib/api/schemas";
|
||||
import { Avatar } from "@/components/ui/Avatar";
|
||||
import { useFetchJson } from "@/lib/api/use-fetch";
|
||||
|
||||
interface SettingsPayload {
|
||||
agent: Agent;
|
||||
access_policy: {
|
||||
humans: { mode: string };
|
||||
agents: { mode: string };
|
||||
};
|
||||
managed_by_name: string;
|
||||
}
|
||||
|
||||
type Screen = "main" | "edit";
|
||||
|
||||
/** The Settings app (§7.7): identity, access toggles, edit profile
|
||||
* (the Job Description IS the system prompt), destructive delete. */
|
||||
export default function SettingsApp({ agent }: { agent: Agent }) {
|
||||
const router = useRouter();
|
||||
const [screen, setScreen] = useState<Screen>("main");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
||||
const settings = useFetchJson<SettingsPayload>(
|
||||
`/api/claws/settings/full?clawId=${agent.id}`,
|
||||
);
|
||||
|
||||
async function saveProfile(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setSaving(true);
|
||||
const data = new FormData(event.currentTarget);
|
||||
await fetch(`/api/claws/${agent.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: data.get("name"),
|
||||
job_title: data.get("job_title"),
|
||||
system_prompt: data.get("system_prompt"),
|
||||
}),
|
||||
});
|
||||
setSaving(false);
|
||||
setScreen("main");
|
||||
settings.refresh();
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
async function setAgentsMode(mode: "any" | "specific") {
|
||||
const current = settings.data?.access_policy;
|
||||
await fetch(`/api/claws/${agent.id}/access`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
humans: current?.humans ?? { mode: "entire_team" },
|
||||
agents: mode === "any" ? { mode: "any" } : { mode: "specific", ids: [] },
|
||||
}),
|
||||
});
|
||||
settings.refresh();
|
||||
}
|
||||
|
||||
async function deleteClaw() {
|
||||
await fetch(`/api/claws/${agent.id}`, { method: "DELETE" });
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
const live = settings.data?.agent ?? agent;
|
||||
|
||||
if (screen === "edit") {
|
||||
return (
|
||||
<form onSubmit={saveProfile} className="flex flex-col gap-3 p-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setScreen("main")}
|
||||
className="self-start text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
‹ Edit profile
|
||||
</button>
|
||||
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||
Name
|
||||
<input
|
||||
name="name"
|
||||
defaultValue={live.name}
|
||||
className="rounded-(--radius) border border-input bg-subtle px-2 py-1.5 text-sm text-foreground outline-none focus:border-accent"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||
Job Title
|
||||
<input
|
||||
name="job_title"
|
||||
defaultValue={live.job_title}
|
||||
className="rounded-(--radius) border border-input bg-subtle px-2 py-1.5 text-sm text-foreground outline-none focus:border-accent"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||
Job description
|
||||
<textarea
|
||||
name="system_prompt"
|
||||
rows={5}
|
||||
defaultValue={live.system_prompt}
|
||||
placeholder="Describe how this claw should think..."
|
||||
className="rounded-(--radius) border border-input bg-subtle px-2 py-1.5 text-sm text-foreground outline-none focus:border-accent"
|
||||
/>
|
||||
<span className="text-xxs">Becomes part of its system prompt.</span>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="rounded-(--radius-button) bg-accent px-4 py-1.5 text-xs font-medium text-background hover:bg-coral-light disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Saving…" : "Save profile"}
|
||||
</button>
|
||||
{confirmingDelete ? (
|
||||
<div className="rounded-(--radius) border border-destructive p-2 text-xs">
|
||||
<p>Delete {live.name} permanently? This cannot be undone.</p>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmingDelete(false)}
|
||||
className="rounded-(--radius-button) border border-border px-3 py-1"
|
||||
>
|
||||
Keep
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={deleteClaw}
|
||||
className="rounded-(--radius-button) bg-destructive px-3 py-1 text-foreground"
|
||||
>
|
||||
Delete claw
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmingDelete(true)}
|
||||
className="self-start text-xs text-accent hover:underline"
|
||||
>
|
||||
🗑 Delete claw
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const agentsMode = settings.data?.access_policy.agents.mode ?? "any";
|
||||
return (
|
||||
<div className="flex flex-col gap-3 p-3">
|
||||
<div className="flex flex-col items-center gap-1 py-2">
|
||||
<Avatar name={live.name} accent={live.accent} size="lg" />
|
||||
<p className="text-sm font-medium">{live.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{live.job_title}</p>
|
||||
</div>
|
||||
<dl className="rounded-(--radius) border border-border bg-subtle text-xs">
|
||||
<div className="flex justify-between border-b border-border px-2 py-2">
|
||||
<dt className="text-muted-foreground">Managed by</dt>
|
||||
<dd>{settings.data?.managed_by_name ?? "…"}</dd>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setScreen("edit")}
|
||||
className="flex w-full justify-between px-2 py-2 hover:bg-surface-warm"
|
||||
>
|
||||
Edit profile <span aria-hidden>›</span>
|
||||
</button>
|
||||
</dl>
|
||||
<p className="pt-1 text-xxs uppercase tracking-wide text-muted-foreground">
|
||||
Who else has access
|
||||
</p>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Other Claws"
|
||||
className="rounded-(--radius) border border-border bg-subtle p-2 text-xs"
|
||||
>
|
||||
<p className="pb-1">Other Claws</p>
|
||||
{(["any", "specific"] as const).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={agentsMode === mode}
|
||||
onClick={() => setAgentsMode(mode)}
|
||||
className="flex w-full items-center gap-2 px-1 py-1 text-left hover:bg-surface-warm"
|
||||
>
|
||||
<span aria-hidden>{agentsMode === mode ? "◉" : "○"}</span>
|
||||
{mode === "any"
|
||||
? "Any Claw on the team"
|
||||
: "Specific claws — pick claws"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user