Claude Code engine, image viewer, colour tags, checks per turn and more

- 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>
This commit is contained in:
Richard ZinkandClaude Sonnet 5.5 committed 2026-11-14 22:30:12 +01:00
1 parent 084452abf6
commit 62fe5bf467
42 files changed
+3734 -55

No files matched your search

+38 -2
View File
@@ -1,7 +1,7 @@
import { Icon } from "./Icon";
import { useState } from "react";
import { post } from "../api";
import { useT } from "../i18n";
import { getLang, useT } from "../i18n";
import { navigate } from "../router";
import { useStore } from "../store";
import type { PermissionRequest, QuestionRequest } from "../types";
@@ -31,7 +31,24 @@ export function PermissionCard({ req, currentId }: { req: PermissionRequest; cur
const chosen = selected.filter(Boolean).length;
const partial = hunkCount > 1 && chosen < hunkCount;
const reply = async (r: "once" | "always" | "reject") => {
const [explaining, setExplaining] = useState(false);
const [explanation, setExplanation] = useState<string>();
const [explainError, setExplainError] = useState<string>();
/** The small model explains in plain words what the action does (approving on a phone without reading code). */
const explain = async () => {
setExplaining(true);
setExplainError(undefined);
try {
const r = await post<{ text: string }>(`/permissions/${req.id}/explain`, { lang: getLang() });
setExplanation(r.text);
} catch (e: any) {
setExplainError(e.message);
} finally {
setExplaining(false);
}
};
const reply = async (r: "once" | "always" | "project" | "reject") => {
setBusy(true);
try {
const hunks = r === "once" && partial ? selected.flatMap((on, i) => (on ? [i] : [])) : undefined;
@@ -47,6 +64,10 @@ export function PermissionCard({ req, currentId }: { req: PermissionRequest; cur
? t("permission.alwaysCommand", { prefix: (command ?? "").split(/\s+/).slice(0, 2).join(" ") })
: t("permission.alwaysTool", { tool: req.tool });
// Dangerous commands are only ever allowed for the session (the server decides; this hides the button).
const dangerous = !!command && /\b(rm\s+-[a-zA-Z]*[rRf]|sudo|su|git\s+(push|reset|clean|rebase|checkout|restore)|docker|docker-compose|systemctl|chmod\s+-R|chown\s+-R|dd|mkfs|shutdown|reboot|kill|pkill|killall|crontab)\b|\|\s*(ba)?sh\b|publish/.test(command);
const projectLabel = req.tool === "bash" ? t("permission.projectCommand", { prefix: (command ?? "").split(/\s+/).slice(0, 2).join(" ") }) : t("permission.projectTool", { tool: req.tool });
return (
<div className="card" role="alertdialog" aria-label={t("permission.title")}>
<h3>
@@ -72,6 +93,16 @@ export function PermissionCard({ req, currentId }: { req: PermissionRequest; cur
</details>
)
)}
{explanation ? (
<div className="explain" role="note">
<Icon name="sparkle" className="tiny" /> {explanation}
</div>
) : (
<button className="small ghost explain-btn" disabled={explaining} onClick={explain}>
<Icon name="sparkle" /> {explaining ? t("permission.explaining") : t("permission.explain")}
</button>
)}
{explainError && <div className="hint">{explainError}</div>}
<input
style={{ width: "100%", marginTop: 8 }}
placeholder={t("permission.feedback")}
@@ -86,6 +117,11 @@ export function PermissionCard({ req, currentId }: { req: PermissionRequest; cur
<button disabled={busy || partial} onClick={() => reply("always")}>
{alwaysLabel}
</button>
{!dangerous && (
<button disabled={busy || partial} onClick={() => reply("project")} title={t("permission.projectHint")}>
{projectLabel}
</button>
)}
<button className="danger" disabled={busy} onClick={() => reply("reject")}>
{feedback ? t("permission.rejectWithFeedback") : t("permission.reject")}
</button>