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

+62
View File
@@ -0,0 +1,62 @@
import { useState } from "react";
import { post } from "../api";
import { useT } from "../i18n";
import { toast } from "../dialogs";
import { Icon } from "./Icon";
import { ModelSelect } from "./ModelSelect";
export const EFFORTS = ["low", "medium", "high", "max"] as const;
/** Effort choice for one turn ("" = the model's default). */
export function EffortSelect({ value, onChange, compact }: { value: string; onChange: (v: string) => void; compact?: boolean }) {
const t = useT();
return (
<select className={compact ? "effort-select" : ""} value={value} onChange={(e) => onChange(e.target.value)} title={t("effort.title")} aria-label={t("effort.title")}>
<option value="">{compact ? t("effort.autoShort") : t("effort.auto")}</option>
{EFFORTS.map((e) => (
<option key={e} value={e}>
{t(`effort.${e}` as any)}
</option>
))}
</select>
);
}
/** Redo the last turn, optionally with another model and effort. */
export function RegenerateButton({ sessionId, messageId, model }: { sessionId: string; messageId: string; model: string }) {
const t = useT();
const [open, setOpen] = useState(false);
const [m, setM] = useState(model);
const [effort, setEffort] = useState("");
const [busy, setBusy] = useState(false);
const go = async () => {
setBusy(true);
try {
await post(`/sessions/${sessionId}/regenerate`, { message_id: messageId, model: m !== model ? m : undefined, effort: effort || undefined });
setOpen(false);
} catch (e: any) {
toast(e.message);
} finally {
setBusy(false);
}
};
return (
<div className="regen">
<button className="small ghost" onClick={() => setOpen(!open)} title={t("regen.title")}>
<Icon name="refresh" /> {t("regen.button")}
</button>
{open && (
<div className="regen-panel">
<div className="hint">{t("regen.hint")}</div>
<div className="row" style={{ gap: 6, flexWrap: "wrap" }}>
<ModelSelect value={m} onChange={setM} />
<EffortSelect value={effort} onChange={setEffort} />
<button className="small primary" disabled={busy} onClick={go}>
<Icon name="refresh" /> {t("regen.go")}
</button>
</div>
</div>
)}
</div>
);
}