Menu
popagent
publicLatest change fadf21d1cd584745f6f92eaa60509e0bef19d242 - fix task orchestration and compact overview cards by AkurAI Build
import { useEffect, useState, type ReactNode } from "react";
import type { ChannelSettings } from "../api-types";
import { apiFetch } from "./api";
export function ChannelSettingsPanel() {
const [settings, setSettings] = useState<ChannelSettings>();
const [error, setError] = useState("");
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
useEffect(() => {
void apiFetch("/api/settings/channels").then(async (response) => {
if (!response.ok) throw new Error("Unable to load channel settings");
setSettings(await response.json() as ChannelSettings);
}).catch((reason) => setError(reason instanceof Error ? reason.message : String(reason)));
}, []);
const change = <K extends keyof ChannelSettings>(key: K, value: ChannelSettings[K]) => {
setSettings((current) => current ? { ...current, [key]: value } : current);
setSaved(false);
};
const save = async () => {
if (!settings) return;
setSaving(true);
setError("");
try {
const { updatedAt: _updatedAt, ...input } = settings;
const response = await apiFetch("/api/settings/channels", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify(input),
});
if (response.ok) {
setSettings(await response.json() as ChannelSettings);
setSaved(true);
} else setError("Unable to save channel settings");
} catch {
setError("Unable to save channel settings");
} finally {
setSaving(false);
}
};
if (error && !settings) return <SettingsState message={error} retry={() => location.reload()} />;
if (!settings) return <SettingsState message="Loading channel settings…" />;
return (
<div className="w-full">
<h1 className="text-xl font-semibold">Channels</h1>
<p className="mt-1 text-sm text-ink-dim">Configure the shared, durable agent dispatch room.</p>
<div className="mt-6 space-y-5">
<section className="rounded border border-line bg-panel p-5">
<label className="flex items-start justify-between gap-4">
<span><span className="block font-medium">Enable shared channels</span><span className="mt-1 block text-sm text-ink-dim">Allow messages and agent task dispatches from the channel workspace.</span></span>
<input aria-label="Enable shared channels" type="checkbox" checked={settings.enabled} onChange={(event) => change("enabled", event.currentTarget.checked)} className="mt-1 size-5 accent-accent" />
</label>
</section>
<section className="rounded border border-line bg-panel p-5">
<h2 className="font-medium">Room and dispatch</h2>
<div className="mt-4 grid gap-4 sm:grid-cols-2">
<Field label="Channel name"><input aria-label="Channel name" value={settings.channelName} maxLength={65} pattern="#[a-z0-9][a-z0-9_-]*" onChange={(event) => change("channelName", event.currentTarget.value)} className="setting-input" /></Field>
<Field label="Dispatch behavior"><select aria-label="Channel dispatch behavior" value={settings.dispatchMode} onChange={(event) => change("dispatchMode", event.currentTarget.value as ChannelSettings["dispatchMode"])} className="setting-input"><option value="every-message">Dispatch every message</option><option value="mentions">Only @popagent mentions</option></select></Field>
<Field label="Context messages"><input aria-label="Channel context messages" type="number" min={0} max={100} value={settings.contextMessages} onChange={(event) => change("contextMessages", event.currentTarget.valueAsNumber)} className="setting-input" /></Field>
<Field label="Agent activity display"><select aria-label="Channel tool display" value={settings.toolDisplay} onChange={(event) => change("toolDisplay", event.currentTarget.value as ChannelSettings["toolDisplay"])} className="setting-input"><option value="compact">Compact</option><option value="timeline">Timeline</option></select></Field>
</div>
</section>
<section className="rounded border border-line bg-panel p-5">
<label className="flex items-start justify-between gap-4">
<span><span className="block font-medium">Real-time updates</span><span className="mt-1 block text-sm text-ink-dim">Use the native server event bus for message and task updates. Disabled clients use periodic refresh.</span></span>
<input aria-label="Enable channel streaming" type="checkbox" checked={settings.streaming} onChange={(event) => change("streaming", event.currentTarget.checked)} className="mt-1 size-5 accent-accent" />
</label>
</section>
{error ? <p role="alert" className="text-sm text-red-400">{error}</p> : null}
<div className="flex items-center justify-end gap-3">{saved ? <span role="status" className="text-xs text-emerald-500">Saved</span> : null}<button type="button" disabled={saving || !settings.channelName.match(/^#[a-z0-9][a-z0-9_-]{0,63}$/)} onClick={() => void save()} className="min-h-11 rounded bg-accent px-4 text-sm font-medium text-white disabled:opacity-50">{saving ? "Saving…" : "Save channel settings"}</button></div>
</div>
</div>
);
}
function Field({ label, children }: { label: string; children: ReactNode }) {
return <label className="text-sm font-medium">{label}{children}</label>;
}
function SettingsState({ message, retry }: { message: string; retry?: () => void }) {
return <div className="w-full"><h1 className="text-xl font-semibold">Channels</h1><p role={retry ? "alert" : "status"} className="mt-6 text-sm text-ink-dim">{message}</p>{retry ? <button type="button" onClick={retry} className="mt-3 min-h-11 border border-line px-4 text-sm">Retry</button> : null}</div>;
}