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
@@ -12,6 +12,8 @@ import { useReasoningMode } from "../prefs";
|
||||
import { Composer } from "./Composer";
|
||||
import { ShareDialog } from "./ShareDialog";
|
||||
import { RenameDialog } from "./RenameDialog";
|
||||
import { RegenerateButton } from "./Regenerate";
|
||||
import { WorktreeChip } from "./WorktreeChip";
|
||||
import { QueueList } from "./QueueList";
|
||||
import { BudgetChip } from "./Budget";
|
||||
import { PermissionCard, QuestionCard } from "./Interaction";
|
||||
@@ -215,14 +217,14 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
}
|
||||
};
|
||||
|
||||
const send = async (raw: string, images: ImageData[]) => {
|
||||
const send = async (raw: string, images: ImageData[], effort?: string) => {
|
||||
const txt = raw.trim();
|
||||
if (txt === "/new") {
|
||||
setText("");
|
||||
return navigate("/new");
|
||||
}
|
||||
try {
|
||||
await post(`/sessions/${sessionId}/prompt`, { text: txt, images });
|
||||
await post(`/sessions/${sessionId}/prompt`, { text: txt, images, effort: effort || undefined });
|
||||
setText("");
|
||||
follow(true);
|
||||
} catch (e: any) {
|
||||
@@ -230,6 +232,7 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
}
|
||||
};
|
||||
|
||||
const lastAssistantIdx = messages.reduce((acc, m, i) => (m.role === "assistant" ? i : acc), -1);
|
||||
const [rewinding, setRewinding] = useState<Message>();
|
||||
const rewind = (m: Message) => setRewinding(m);
|
||||
const rewindDone = async () => {
|
||||
@@ -296,6 +299,7 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
{fmtCost(stats.total.cost, stats.currency)}
|
||||
</button>
|
||||
)}
|
||||
<WorktreeChip session={session} busy={busy} />
|
||||
<ShellsChip sessionId={sessionId} busy={busy} />
|
||||
{isAdmin && <BudgetChip />}
|
||||
{session.permission_mode === "bypass" && <span className="chip warn">{t("permissionMode.bypass")}</span>}
|
||||
@@ -323,16 +327,33 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
</button>
|
||||
)}
|
||||
<details className="menu-pop">
|
||||
<summary className="small ghost" title={t("chat.export")}>
|
||||
<Icon name="download" />
|
||||
<summary className="small ghost" title={t("chat.more")} aria-label={t("chat.more")}>
|
||||
<Icon name="more" />
|
||||
</summary>
|
||||
<div className="menu-pop-body">
|
||||
<a href={`/api/sessions/${sessionId}/export?format=md`} download>
|
||||
{t("chat.exportMd")}
|
||||
<Icon name="download" /> {t("chat.exportMd")}
|
||||
</a>
|
||||
<a href={`/api/sessions/${sessionId}/export?format=json`} download>
|
||||
{t("chat.exportJson")}
|
||||
<Icon name="download" /> {t("chat.exportJson")}
|
||||
</a>
|
||||
{!session.parent_id && !session.worktree && (
|
||||
<a
|
||||
href="#"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
(e.currentTarget.closest("details") as HTMLDetailsElement).open = false;
|
||||
try {
|
||||
await post(`/sessions/${sessionId}/worktree`, { action: "create" });
|
||||
toast(t("worktree.created"), "success");
|
||||
} catch (err: any) {
|
||||
toast(err.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon name="fork" /> {t("worktree.create")}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
<button className="small ghost" onClick={() => post(`/sessions/${sessionId}/compact`).catch((e) => toast(e.message))} disabled={busy} title={t("command.compact")}>
|
||||
@@ -366,11 +387,13 @@ export function ChatView({ sessionId, onMenu }: { sessionId: string; onMenu: ()
|
||||
<div className="chat">
|
||||
{showStats && <UsageOverlay session={session} onClose={() => setShowStats(false)} />}
|
||||
{messages.length === 0 && !draft && <div className="empty">{t("chat.empty")}</div>}
|
||||
{messages.map((m) => {
|
||||
{messages.map((m, idx) => {
|
||||
if (m.role === "assistant") {
|
||||
const lastAnswer = idx === lastAssistantIdx && !busy && !readOnly && !session.parent_id;
|
||||
return (
|
||||
<div key={m.id} className="msg">
|
||||
<AssistantParts parts={m.parts} results={results} running={busy} live={false} baseDir={session.cwd} />
|
||||
{lastAnswer && <RegenerateButton sessionId={sessionId} messageId={m.id} model={session.model} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user