Official
chat
Built-in chat UI (JavaScript UI plugin).
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/chat@0.2.0
Permissions in 0.2.0
Files
views/pending.ts6.6 KB
// Inline muted pending-input cards and a semantic usage-limit notice. Native
// admission/unknown delivery remains visible without fictitious edit/withdraw.
import * as Effect from "effect/Effect";
import { html } from "convergence";
import type { Chat, ContentBlock, Element, PendingInput } from "convergence";
import type { ChatView } from "../controller.ts";
import { autoSend, editable, inputLabel, pending, policyOf } from "../scheduler-policy.ts";
import { steeringReason } from "../composition.ts";
import { column, row } from "./styles.ts";
import { resetsIn } from "./usage.ts";
const descriptions = (blocks: readonly ContentBlock[]) => blocks.filter((block) => block.type !== "text").map((block) => {
switch (block.type) {
case "image": return `Image · ${block.mimeType}`;
case "audio": return `Audio · ${block.mimeType}`;
case "file_ref": case "resource": return block.path;
case "skill": return `$${block.name}${block.input ? ` ${block.input}` : ""}`;
}
});
export function renderPending(v: ChatView, chat: Chat): Element[] {
const scheduler = v.controller.scheduler, snapshot = scheduler.snapshots[chat.id], composer = v.controller.composer(chat.id);
if (!snapshot) return [];
const act = <A, E>(effect: Effect.Effect<A, E>) => v.ctx.run(effect.pipe(Effect.catch((error) => scheduler.report(chat.id, error))));
const reason = steeringReason(v.controller, chat);
// Input handed to the agent is the user's message in the transcript; a
// card for it too would read as a queue until the agent reports pickup.
const shown = new Set(v.controller.state.transcripts[chat.id]?.items.map((item) => item.id));
const handed = (input: PendingInput) =>
(input.id === composer.dispatching && input.state === "held") || (shown.has(input.id) && !editable(input) && !input.error);
return pending(snapshot).filter((input) => !handed(input)).map((input) => {
const canEdit = editable(input), edit = canEdit && composer.pendingEdit?.id === input.id ? composer.pendingEdit : undefined;
const save = Effect.fn("Chat.savePendingEdit")(function* () {
if (!edit) return;
const blocks: ContentBlock[] = [
...(edit.text.trim() ? [{ type: "text" as const, text: edit.text }] : []),
...input.blocks.filter((block) => block.type !== "text"),
];
if (!blocks.length) return;
yield* scheduler.change({ chatId: chat.id, inputId: input.id, revision: edit.revision, blocks });
composer.pendingEdit = undefined;
v.ctx.update();
});
return html`
<div ...${column} gap="small" padding="medium" background="muted" borderRadius="md">
<div ...${row} gap="small">
<icon name=${input.intent === "steer" ? "corner-down-left" : "list-plus"} small tone="muted" />
<text style="body" size="xs" tone="muted">${inputLabel(input)}</text>
<spacer />
${canEdit ? html`
<button id=${`promote-${input.id}`} label=${v.controller.isRunning(chat) ? "Steer" : "Send now"} small variant="ghost" disabled=${!!reason} tooltip=${reason ?? "Queue → Steer; preserves your draft"} onClick=${() => act(scheduler.promote(chat.id, input.id, snapshot.revision))} />
${input.intent === "steer" ? html`<button id=${`demote-${input.id}`} label="Queue" small variant="ghost" onClick=${() => act(scheduler.change({ chatId: chat.id, inputId: input.id, revision: snapshot.revision, intent: "queue" }))} />` : null}
<button id=${`edit-input-${input.id}`} icon="pencil" small variant="ghost" tooltip="Edit held input" onClick=${() => { composer.pendingEdit = { id: input.id, revision: snapshot.revision, text: v.controller.plainText(input.blocks) }; v.ctx.update(); }} />
<button id=${`remove-input-${input.id}`} icon="x" small variant="ghost" tooltip="Remove held input" onClick=${() => act(scheduler.change({ chatId: chat.id, inputId: input.id, revision: snapshot.revision, remove: true }))} />` : null}
</div>
${edit ? html`
<input id=${`edit-pending-${input.id}`} multiline value=${edit.text} onChange=${(event: { value: string }) => { edit.text = event.value; }} />
<div ...${row} gap="small">
<button id=${`save-pending-${input.id}`} label="Save" small onClick=${() => act(save())} />
<button id=${`cancel-pending-${input.id}`} label="Cancel edit" small variant="ghost" onClick=${() => { composer.pendingEdit = undefined; v.ctx.update(); }} />
</div>` : html`<text style="body" size="xs">${v.controller.plainText(input.blocks)}</text>`}
${descriptions(input.blocks).map((text) => html`<text style="body" size="xs" tone="muted">${text}</text>`)}
${input.error ? html`<text style="body" size="xs" tone="danger">${input.error}</text>` : null}
</div>`;
});
}
export function renderLimit(v: ChatView, chat: Chat): Element | null {
const scheduler = v.controller.scheduler, snapshot = scheduler.snapshots[chat.id];
const state = snapshot ? policyOf(snapshot) : undefined, wait = state?.wait;
const active = scheduler.activeLimits[chat.id];
const grace = !!active && (snapshot?.running || v.controller.isRunning(chat)) && (!active.runId || active.runId === snapshot?.runId);
if (!wait && !grace) return null;
const enabled = autoSend(scheduler.preferences, chat.agentId);
const reset = wait?.resetAt ?? (grace ? active?.evidence.resetsAt : undefined);
const countdown = resetsIn(reset);
const resetText = countdown && reset ? `Reset ${countdown === "now" ? "reported for now" : `in ${countdown}`} · ${new Date(reset).toLocaleString()}` : "Reset time unavailable";
const status = grace ? "Usage limit reported · still working / retrying" : scheduler.checking.has(chat.id) ? "Checking availability…" :
state?.pause || wait?.status === "paused" ? "Automatic send paused" : !enabled ? "Auto-send disabled for this agent" : wait?.status === "attempting" ? wait.reason : wait?.status === "failed" ? "Recovery check failed" : "Waiting for usage-limit recovery";
return html`
<div ...${column} gap="small" padding="medium" background="warning/10" borderRadius="md" border=${{ width: 1, color: "warning/30" }}>
<div ...${row} gap="small"><icon name="clock" small tone="warning" /><text style="body" size="xs" tone="warning">${status}</text></div>
<text style="body" size="xs" tone="muted">${resetText}</text>
<text style="body" size="xs" tone="muted">${state?.pause ?? wait?.reason ?? active?.evidence.reason}</text>
<checkbox id=${`auto-limit-${chat.id}`} label="Auto-send pending input after recovery · all chats for this agent" checked=${enabled}
onChange=${(event: { value: boolean }) => v.ctx.run(scheduler.setAutoSend(chat.agentId, event.value))} />
</div>`;
}Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 105.1 KB | 5 permissions | Listed |
No comments yet.