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:
1 parent
df1924b0a1
commit
fca1c41087
22 files changed
+821
-32
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { getLang, useT } from "../i18n";
|
||||
import { toast } from "../dialogs";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
/* Web Speech API (Chrome/Edge/Safari; Firefox has none). */
|
||||
type Recognition = {
|
||||
lang: string;
|
||||
continuous: boolean;
|
||||
interimResults: boolean;
|
||||
start(): void;
|
||||
stop(): void;
|
||||
onresult: ((e: any) => void) | null;
|
||||
onend: (() => void) | null;
|
||||
onerror: ((e: any) => void) | null;
|
||||
};
|
||||
const Impl: (new () => Recognition) | undefined = (window as any).SpeechRecognition ?? (window as any).webkitSpeechRecognition;
|
||||
|
||||
export const voiceSupported = () => !!Impl;
|
||||
|
||||
const LOCALES: Record<string, string> = { de: "de-DE", en: "en-US" };
|
||||
|
||||
/** Microphone button: dictated text is appended to the message. */
|
||||
export function VoiceButton({ onText, onInterim }: { onText: (text: string) => void; onInterim: (text: string) => void }) {
|
||||
const t = useT();
|
||||
const [listening, setListening] = useState(false);
|
||||
const rec = useRef<Recognition | null>(null);
|
||||
|
||||
useEffect(() => () => rec.current?.stop(), []);
|
||||
if (!Impl) return null;
|
||||
|
||||
const toggle = () => {
|
||||
if (listening) {
|
||||
rec.current?.stop();
|
||||
return;
|
||||
}
|
||||
const r = new Impl!();
|
||||
r.lang = LOCALES[getLang()] ?? navigator.language;
|
||||
r.continuous = true;
|
||||
r.interimResults = true;
|
||||
r.onresult = (e: any) => {
|
||||
let interim = "";
|
||||
for (let i = e.resultIndex; i < e.results.length; i++) {
|
||||
const res = e.results[i];
|
||||
if (res.isFinal) onText(res[0].transcript.trim());
|
||||
else interim += res[0].transcript;
|
||||
}
|
||||
onInterim(interim);
|
||||
};
|
||||
r.onerror = (e: any) => {
|
||||
if (e.error === "not-allowed" || e.error === "service-not-allowed") toast(t("voice.denied"));
|
||||
else if (e.error !== "no-speech" && e.error !== "aborted") toast(`${t("voice.error")}: ${e.error}`);
|
||||
};
|
||||
r.onend = () => {
|
||||
setListening(false);
|
||||
onInterim("");
|
||||
};
|
||||
rec.current = r;
|
||||
r.start();
|
||||
setListening(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<button type="button" className={`ghost small voice-btn ${listening ? "on" : ""}`} onClick={toggle} title={t(listening ? "voice.stop" : "voice.start")} aria-label={t(listening ? "voice.stop" : "voice.start")} aria-pressed={listening}>
|
||||
<Icon name="mic" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user