fix(client): copies over plain HTTP, Vietnamese IME Enter, stale replies; react-router 7

- Every Copy goes through copyText: on http://<lan-ip> navigator.clipboard
  is undefined, so the push token (shown once), snapshot links, tool paths and
  hosts copied nothing, silently or with a TypeError.
- Inputs that submit on Enter use isSubmitEnter, so committing an IME
  composition (Vietnamese) no longer saves a half-typed pet name, jumps the
  settings search, or submits a cut-off plugin/MCP/marketplace spec.
- LaneChangesPanel, Workspace features/proof and AiToolConfig drop a reply
  that lands after the next request started: opening file B before file A's
  read returned showed (and let the editor save) A's text under B's header.
- Error-state Retry buttons spin through useSpin; ImageLightbox uses useDialog.
- react-router-dom 6.30 -> 7.18.4 (GHSA-wrjc-x8rr-h8h6, GHSA-337j-9hxr-rhxg);
  screen snapshots gain only Link's data-discover attribute.
- StatusBar test follows v2.8.17's "Settings → AI tools → Profiles" text.
This commit is contained in:
2026-10-22 12:17:18 +07:00
parent fa74cce3fc
commit 50ac0f0daf
27 changed files with 191 additions and 110 deletions
+2 -1
View File
@@ -92,6 +92,7 @@ import { formatBytes } from "../../lib/format";
import { copyText } from "../../lib/clipboard";
import { ContextMenu, MenuItem, MenuSeparator, useContextMenu } from "../ContextMenu";
import { CloneHereDialog } from "./CloneHereDialog";
import { isSubmitEnter } from "../../lib/keys";
/** One upload batch at most (the server caps one file the same). */
export const FX_BATCH_MAX_BYTES = 500 * 1024 * 1024;
@@ -1987,7 +1988,7 @@ function InlineName({
onDoubleClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === "Enter") onDone();
if (isSubmitEnter(e)) onDone();
if (e.key === "Escape") onCancel();
}}
onBlur={onDone}
@@ -1063,6 +1063,9 @@ export function LaneChangesPanel({
const [historyLoading, setHistoryLoading] = useState(false);
useEffect(() => {
if (mode !== "history") return;
// A reply for the previous ref/file/page lands after the next request
// started: drop it, or it overwrites the list the user is looking at.
let live = true;
setHistoryLoading(true);
void api.lanes
.gitLog(target, {
@@ -1070,16 +1073,23 @@ export function LaneChangesPanel({
...(historyRef ? { ref: historyRef } : {}),
...(historyFile ? { file: historyFile } : {}),
})
.then(forLane(laneId, (r) => setCommits(r.commits)))
.catch((err) => setError(err instanceof Error ? err.message : String(err)))
.finally(() => laneNow.current === laneId && setHistoryLoading(false));
.then(forLane(laneId, (r) => live && setCommits(r.commits)))
.catch((err) => live && setError(err instanceof Error ? err.message : String(err)))
.finally(() => live && laneNow.current === laneId && setHistoryLoading(false));
return () => {
live = false;
};
}, [mode, laneId, reread, historyLimit, historyRef, historyFile]);
useEffect(() => {
if (mode !== "history") return;
let live = true;
void api.lanes
.gitBranches(target)
.then(forLane(laneId, (r) => setHistoryBranches(r.branches)))
.catch(() => setHistoryBranches([]));
.then(forLane(laneId, (r) => live && setHistoryBranches(r.branches)))
.catch(() => live && setHistoryBranches([]));
return () => {
live = false;
};
}, [mode, laneId, reread]);
// Re-run when a lane switch empties the cache too: the full-width sidebar
@@ -1173,24 +1183,33 @@ export function LaneChangesPanel({
// own fetch — and only while the viewer is actually showing it.
useEffect(() => {
if (mode !== "file" || !(whole || editable) || !openFile) return;
// Opening file B before file A's read came back must not show (and let the
// editor save) A's text under B's header.
let live = true;
void api.lanes
.gitFile(target, openFile, range === "commit" && sha ? sha : "work")
.then(
forLane(laneId, (r) =>
setText(
r.available
? {
path: r.path,
lines: r.lines,
truncated: r.truncated,
binary: r.binary,
sha: r.sha,
}
: null
)
forLane(
laneId,
(r) =>
live &&
setText(
r.available
? {
path: r.path,
lines: r.lines,
truncated: r.truncated,
binary: r.binary,
sha: r.sha,
}
: null
)
)
)
.catch((err) => setError(err instanceof Error ? err.message : String(err)));
.catch((err) => live && setError(err instanceof Error ? err.message : String(err)));
return () => {
live = false;
};
}, [mode, whole, editable, openFile, laneId, range, sha]);
// The editor's original side, read beside the working file. A file that is
// not there (new since) diffs against nothing: every line is added.
@@ -1198,10 +1217,17 @@ export function LaneChangesPanel({
useEffect(() => {
if (mode !== "file" || !(whole || editable) || !openFile || range === "commit") return;
const rev = range === "branch" && mergeBase ? mergeBase : "HEAD";
let live = true;
void api.lanes.gitFile(target, openFile, rev).then(
forLane(laneId, (r) => setBaseText(r.available && !r.binary ? r.lines.join("\n") : "")),
forLane(laneId, () => setBaseText(""))
forLane(
laneId,
(r) => live && setBaseText(r.available && !r.binary ? r.lines.join("\n") : "")
),
forLane(laneId, () => live && setBaseText(""))
);
return () => {
live = false;
};
}, [mode, whole, editable, openFile, laneId, range, mergeBase]);
useEffect(() => {