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:
1 parent
084452abf6
commit
62fe5bf467
42 files changed
+3734
-55
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user