Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126import { adopt, define, sheet } from "@nibling/web/internals";import type { Files } from "../files.ts";import type { Unsubscribe } from "../shell/workbench.ts";
const agentSheet = sheet(` :host { display: flex; flex-direction: column; overflow: hidden; container: nib-agent / size; font: var(--nib-font-ui); } [part="log"] { flex: 1; margin: 0; padding: var(--nib-space-sm) var(--nib-space-sm) var(--nib-space-sm) var(--nib-space-xl); overflow: auto; list-style: none; } [part="log"] li { margin-bottom: var(--nib-space-sm); } /* The role reads as text, not colour alone; muted keeps it secondary to the message. */ [part="role"] { font-weight: var(--nib-font-weight-strong); margin-inline-end: var(--nib-space-xs); color: var(--nib-color-text-muted); } form { display: flex; gap: var(--nib-space-xs); padding: var(--nib-space-xs); background: var(--nib-color-surface); border-top: var(--nib-size-border) solid var(--nib-color-border); } textarea { flex: 1; min-width: 0; min-height: 0; resize: none; font: inherit; border: var(--nib-size-border) solid var(--nib-color-border); background: var(--nib-color-canvas); color: var(--nib-color-text); } button { border: var(--nib-size-border) solid var(--nib-color-border); background: var(--nib-color-surface); color: var(--nib-color-text); font: inherit; } textarea:focus-visible, button:focus-visible { outline: var(--nib-size-focus-ring) solid var(--nib-color-focus-ring); outline-offset: calc(-1 * var(--nib-size-focus-ring)); } /* A narrow pane (the host's own size, not the viewport) stacks the form and tightens message spacing. */ @container nib-agent (max-width: 280px) { form { flex-direction: column; } [part="log"] { padding: var(--nib-space-xs) var(--nib-space-xs) var(--nib-space-xs) var(--nib-space-lg); } [part="log"] li { margin-bottom: var(--nib-space-xs); } }`);
/** * A conversation is a transcript file, so an agent, the shell, and other viewers all see the same * messages. No model is called here: the agent loop is userland (D8). */export class AgentElement extends HTMLElement { readonly #log: HTMLOListElement; readonly #textarea: HTMLTextAreaElement; #submit: (() => void) | undefined;
constructor() { super(); const root = this.attachShadow({ mode: "open", delegatesFocus: true }); adopt(root, agentSheet); this.#log = document.createElement("ol"); this.#log.part = "log"; const form = document.createElement("form"); this.#textarea = document.createElement("textarea"); const button = document.createElement("button"); button.type = "submit"; button.textContent = "Send"; form.append(this.#textarea, button); root.append(this.#log, form); // Enter sends; Shift+Enter falls through to the textarea's default newline insertion. this.#textarea.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); this.#submit?.(); } }); form.addEventListener("submit", (event) => { event.preventDefault(); this.#submit?.(); }); }
connectedCallback(): void { this.#log.setAttribute("role", "log"); this.#log.setAttribute("aria-label", "Conversation"); this.#textarea.setAttribute("aria-label", "Message"); }
connect(files: Files, conversation: string): Unsubscribe { const path = `.agents/${conversation}.json`; const messages = (): { role: string; text: string }[] => JSON.parse(files.read(path) ?? "[]"); const draw = () => { this.#log.replaceChildren( ...messages().map((message) => { const item = document.createElement("li"); const role = document.createElement("span"); role.part = "role"; // The role reads as text, not colour alone, so it survives in a screen reader or greyscale. role.textContent = message.role === "user" ? "You" : "Agent"; const text = document.createElement("span"); text.textContent = message.text; item.append(role, text); return item; }), ); }; this.#submit = () => { const text = this.#textarea.value.trim(); if (!text) return; files.write(path, JSON.stringify([...messages(), { role: "user", text }])); this.#textarea.value = ""; }; draw(); return files.watch((changed) => changed === path && draw()); }}
declare global { interface HTMLElementTagNameMap { "nib-agent": AgentElement; }}
define("nib-agent", AgentElement);