Add worktrees, per-hunk review, effort per message, regenerate, voice input

- Isolated git worktree per session (create, status, merge, discard)
- Permission card: accept file-edit hunks individually; agent learns what was rejected
- Reasoning effort per message; regenerate the last answer with another model/effort
- Voice input via the Web Speech API
- Folder-up button in the file browser; ssh, rsync and mc in the image

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 22:29:58 +01:00
1 parent df1924b0a1
commit fca1c41087
22 files changed
+821 -32

No files matched your search

+17 -6
View File
@@ -5,7 +5,7 @@ import { useT } from "../i18n";
import { navigate } from "../router";
import { useStore } from "../store";
import type { PermissionRequest, QuestionRequest } from "../types";
import { Diff } from "./Diff";
import { Diff, HunkDiff, splitHunks } from "./Diff";
function SubagentHint({ sessionId, currentId }: { sessionId: string; currentId: string }) {
const t = useT();
@@ -25,11 +25,17 @@ export function PermissionCard({ req, currentId }: { req: PermissionRequest; cur
const t = useT();
const [feedback, setFeedback] = useState("");
const [busy, setBusy] = useState(false);
// File edits with several hunks can be accepted piece by piece.
const hunkCount = req.diff && (req.tool === "edit" || req.tool === "write") ? splitHunks(req.diff).hunks.length : 0;
const [selected, setSelected] = useState<boolean[]>(() => Array(hunkCount).fill(true));
const chosen = selected.filter(Boolean).length;
const partial = hunkCount > 1 && chosen < hunkCount;
const reply = async (r: "once" | "always" | "reject") => {
setBusy(true);
try {
await post(`/permissions/${req.id}`, { reply: r, message: r === "reject" ? feedback : undefined });
const hunks = r === "once" && partial ? selected.flatMap((on, i) => (on ? [i] : [])) : undefined;
await post(`/permissions/${req.id}`, { reply: r, message: r === "reject" || partial ? feedback || undefined : undefined, hunks });
} finally {
setBusy(false);
}
@@ -49,7 +55,12 @@ export function PermissionCard({ req, currentId }: { req: PermissionRequest; cur
<SubagentHint sessionId={req.session_id} currentId={currentId} />
<div style={{ margin: "6px 0" }}>{req.title}</div>
{command && <pre className="mono" style={{ whiteSpace: "pre-wrap", margin: "6px 0" }}>$ {command}</pre>}
{req.diff ? (
{req.diff && hunkCount > 1 ? (
<>
<div className="hint">{t("permission.hunksHint", { n: hunkCount })}</div>
<HunkDiff diff={req.diff} selected={selected} onToggle={(i) => setSelected((s) => s.map((v, j) => (j === i ? !v : v)))} />
</>
) : req.diff ? (
<Diff diff={req.diff} />
) : (
!command && (
@@ -69,10 +80,10 @@ export function PermissionCard({ req, currentId }: { req: PermissionRequest; cur
onKeyDown={(e) => e.key === "Enter" && reply("reject")}
/>
<div className="actions">
<button className="primary" disabled={busy} onClick={() => reply("once")} autoFocus>
{t("permission.allowOnce")}
<button className="primary" disabled={busy || (hunkCount > 1 && chosen === 0)} onClick={() => reply("once")} autoFocus>
{partial ? t("permission.applySelected", { k: chosen, n: hunkCount }) : t("permission.allowOnce")}
</button>
<button disabled={busy} onClick={() => reply("always")}>
<button disabled={busy || partial} onClick={() => reply("always")}>
{alwaysLabel}
</button>
<button className="danger" disabled={busy} onClick={() => reply("reject")}>