chore: v2.8.12 — clone queue and progress, session defaults, chat agents, drops as @ mentions

Whole tree from parallel sessions, plus the version bump:
- Clone: a queue with live progress (Add workspace and extra folders),
  clones run under nice/ionice so the machine stays responsive.
- Settings → AI tools → Session defaults: a new session's default
  permission mode / bypass per account.
- Chat lists the background subagents that have not reported back yet.
- A file pasted or dropped into a claude pane goes in as an @ mention.
- Changes panel: say why amend/undo is refused on a pushed commit.
- openapi.yaml regenerated (Artifacts, Host, resumeById).
This commit is contained in:
2026-10-19 17:19:41 +07:00
parent 1a6b550e89
commit 49f87c4da3
61 changed files with 2824 additions and 114 deletions
@@ -10,7 +10,8 @@
* sent to Settings → Git first — and an HTTPS host with no saved login offers
* the login row up front (optional: a public repo needs none). The identity is
* read again when the window regains focus, so a key made in another tab
* counts at once.
* counts at once. The running clone shows its queue place, then git's
* progress (`CloneProgress`); **Partial clone** sends `partial: true`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -18,6 +19,7 @@ import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Check, Loader2, X } from "lucide-react";
import { api, type GitIdentity, type LaneSourcesView } from "../../lib/api";
import { CloneProgress, newCloneToken } from "./CloneProgress";
type Row = {
url: string;
@@ -25,6 +27,8 @@ type Row = {
note?: string;
/** The note points at Settings → Git (no key, or a key the host refused). */
ssh?: boolean;
/** While busy: the progress token CloneProgress polls. */
token?: string;
};
type Login = { host: string; username: string; token: string; remember: boolean };
@@ -71,6 +75,7 @@ export function CloneSourcesForm({
const [rows, setRows] = useState<Row[]>([]);
const [busy, setBusy] = useState(false);
const [login, setLogin] = useState<Login | null>(null);
const [partial, setPartial] = useState(false);
const [identity, setIdentity] = useState<GitIdentity | null>(null);
// An unknown identity (an older server, a failed read) checks nothing: the
@@ -116,10 +121,13 @@ export function CloneSourcesForm({
for (const row of next) {
if (row.state === "failed") continue;
row.state = "busy";
row.token = newCloneToken();
show();
try {
const view = await api.lanes.cloneFolder(laneId, {
url: row.url,
progressToken: row.token,
...(partial ? { partial: true } : {}),
...(login?.username.trim() && login.token
? {
credentials: {
@@ -151,6 +159,7 @@ export function CloneSourcesForm({
row.note = e.message || String(err);
}
}
row.token = undefined;
show();
}
if (asked) setLogin(asked);
@@ -191,6 +200,18 @@ export function CloneSourcesForm({
className="w-full rounded border border-border bg-surface-1 px-2 py-1 font-mono text-[11px] text-fg-primary"
/>
<p className="text-[11px] text-fg-muted">{t("home.cloneHint", { path: parent })}</p>
<label
className="flex items-center gap-1.5 text-[11px] text-fg-secondary"
title={t("clone.partialHint")}
>
<input
type="checkbox"
data-testid="workspace-clone-partial"
checked={partial}
onChange={(e) => setPartial(e.target.checked)}
/>
{t("clone.partial")}
</label>
{sshHosts.length > 0 && (
<p
role="alert"
@@ -280,6 +301,11 @@ export function CloneSourcesForm({
{r.url}
</span>
</span>
{r.state === "busy" && r.token && (
<div className="ml-[18px] mt-0.5">
<CloneProgress token={r.token} />
</div>
)}
{r.note && (
<span className="ml-[18px] block text-status-danger">
{r.note}