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
@@ -2,10 +2,12 @@ import { Icon } from "./Icon";
|
||||
import { formatSize } from "./FileFigure";
|
||||
import { uploadFile } from "../api";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useAgents, useFetch } from "../hooks";
|
||||
import { useAgents, useFetch, useModels } from "../hooks";
|
||||
import { useT, type Key } from "../i18n";
|
||||
import type { ImageData, Session } from "../types";
|
||||
import { ModelSelect } from "./ModelSelect";
|
||||
import { EffortSelect } from "./Regenerate";
|
||||
import { VoiceButton } from "./Voice";
|
||||
import { confirmDialog, promptDialog, toast } from "../dialogs";
|
||||
|
||||
interface Suggestion {
|
||||
@@ -49,7 +51,7 @@ export function Composer({
|
||||
busy: boolean;
|
||||
text: string;
|
||||
setText: (s: string) => void;
|
||||
onSend: (text: string, images: ImageData[]) => void;
|
||||
onSend: (text: string, images: ImageData[], effort?: string) => void;
|
||||
onAbort: () => void;
|
||||
onUpdate: (patch: Partial<Session>) => void;
|
||||
}) {
|
||||
@@ -60,6 +62,12 @@ export function Composer({
|
||||
const [files, setFiles] = useState<{ name: string; path: string; size: number }[]>([]);
|
||||
const [uploading, setUploading] = useState(0);
|
||||
const [active, setActive] = useState(0);
|
||||
// Reasoning effort for the next message only ("" = model default).
|
||||
const [effort, setEffort] = useState("");
|
||||
const [interim, setInterim] = useState("");
|
||||
const models = useModels();
|
||||
const [prov, ...mid] = session.model.split("/");
|
||||
const reasoning = !!models.find((p) => p.provider === prov)?.models.find((m) => m.id === mid.join("/"))?.reasoning;
|
||||
// @file mentions: token before the caret and matching files.
|
||||
const [mention, setMention] = useState<{ start: number; end: number; q: string }>();
|
||||
const [mentionHits, setMentionHits] = useState<{ rel: string; is_dir: boolean }[]>([]);
|
||||
@@ -130,7 +138,8 @@ export function Composer({
|
||||
if ((!text.trim() && images.length === 0 && files.length === 0) || uploading > 0) return;
|
||||
// Files are referenced by path; the agent reads them with its tools.
|
||||
const refs = files.length ? `\n\n${t("composer.attachedFiles")}\n${files.map((f) => `- ${f.path} (${formatSize(f.size)})`).join("\n")}` : "";
|
||||
onSend(`${text}${refs}`, images);
|
||||
onSend(`${text}${refs}`, images, effort);
|
||||
setEffort("");
|
||||
setImages([]);
|
||||
setFiles([]);
|
||||
};
|
||||
@@ -258,7 +267,7 @@ export function Composer({
|
||||
ref={ref}
|
||||
rows={1}
|
||||
value={text}
|
||||
placeholder={busy ? t("composer.placeholderBusy") : t("composer.placeholder")}
|
||||
placeholder={interim ? `${interim} …` : busy ? t("composer.placeholderBusy") : t("composer.placeholder")}
|
||||
onChange={(e) => {
|
||||
setText(e.target.value);
|
||||
setActive(0);
|
||||
@@ -312,6 +321,8 @@ export function Composer({
|
||||
<option value="bypass">{t("permissionMode.bypass")}</option>
|
||||
</select>
|
||||
<ModelSelect value={session.model} onChange={(m) => onUpdate({ model: m })} title={t("composer.model")} />
|
||||
{reasoning && <EffortSelect value={effort} onChange={setEffort} compact />}
|
||||
<VoiceButton onText={(d) => setText(text ? `${text.replace(/\s+$/, "")} ${d}` : d)} onInterim={setInterim} />
|
||||
<label className="ghost" style={{ cursor: "pointer", padding: "3px 6px" }} title={t("composer.attach")}>
|
||||
<Icon name="clip" />
|
||||
<input type="file" multiple hidden onChange={(e) => e.target.files && (addFiles(e.target.files), (e.target.value = ""))} />
|
||||
|
||||
Reference in new issue
Block a user