- Claude Code as an engine: chats on a "claude-code/<alias>" model run through the claude CLI (stream-json); tool approvals go through our permission system. Sign in from the web UI (URL + pasted code), CLI version and update from Settings; the Docker image contains the CLI. - Small models (8k context) for side tasks: input is cut to the model's context window; one-line chat status, "What does this do?" on approvals, "Plan as automation" (job draft from a chat). - Mobile: top bar no longer widens the page, image viewer (zoom, swipe, mark up, use in message), jump to previous own message, message menu, time under every message. - Approvals: push actions (allow/reject), "always allow in this project" (never for dangerous commands) with revoke. - Per turn: changed files with diffs and rollback; project check command with a green/red badge and "let the agent fix it". - Colour tags for chats in the colours of the active theme. - Project defaults for new chats, project page (timeline), offline queue. Tested in a throwaway container with a mock model (8k small model): 26 API checks passed (permissions, diffs, checks, defaults, job draft, side-task budget, Claude Code status/login URL, unsigned-in chat). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
40 lines
1.5 KiB
TypeScript
40 lines
1.5 KiB
TypeScript
import { useEffect } from "react";
|
|
import { useT } from "../i18n";
|
|
import { flushOutbox, removeFromOutbox, useOutbox } from "../outbox";
|
|
import { useStore } from "../store";
|
|
import { Icon } from "./Icon";
|
|
|
|
/** Messages waiting for a connection: sent automatically once the server is reachable again. */
|
|
export function OutboxBar({ sessionId }: { sessionId: string }) {
|
|
const t = useT();
|
|
const items = useOutbox(sessionId);
|
|
const connected = useStore((s) => s.connected);
|
|
useEffect(() => {
|
|
if (!items.length) return;
|
|
const go = () => void flushOutbox(sessionId);
|
|
if (connected && navigator.onLine) go();
|
|
window.addEventListener("online", go);
|
|
return () => window.removeEventListener("online", go);
|
|
}, [sessionId, connected, items.length]);
|
|
if (!items.length) return null;
|
|
return (
|
|
<div className="outbox" role="status">
|
|
<div className="outbox-head">
|
|
<Icon name="clock" className="tiny" /> {t("outbox.waiting", { n: items.length })}
|
|
<span className="grow" />
|
|
<button className="small" onClick={() => void flushOutbox(sessionId)}>
|
|
{t("outbox.sendNow")}
|
|
</button>
|
|
</div>
|
|
{items.map((i) => (
|
|
<div key={i.id} className="outbox-item">
|
|
<span>{i.text.length > 120 ? `${i.text.slice(0, 120)}…` : i.text}</span>
|
|
<button className="small ghost" onClick={() => removeFromOutbox(i.id)} aria-label={t("outbox.discard")} title={t("outbox.discard")}>
|
|
<Icon name="close" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|