AkurAI Build
Menu

popagent

public

Latest change 7f0ff66d6d9fb6468416c58bee46bd3d08169501 - Checkpoint browser channels and memory work 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("");
    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");
    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 max-w-4xl">
      <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 max-w-3xl"><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>;
}