diff --git a/crates/ide-agent/src/lib.rs b/crates/ide-agent/src/lib.rs
index 66786c1d..a65400a6 100644
--- a/crates/ide-agent/src/lib.rs
+++ b/crates/ide-agent/src/lib.rs
@@ -345,6 +345,14 @@ impl AgentManager {
provider::set_builtin_command(&dir, id, command.as_deref())
}
+ /// Sets the environment variables built-in `id` starts with; empty clears them.
+ pub fn set_builtin_env(&self, id: &str, env: std::collections::BTreeMap) -> IdeResult<()> {
+ let Some(dir) = self.shared.paths.custom_agents_dir.clone() else {
+ return Err(IdeError::unavailable("there is no custom agents directory"));
+ };
+ provider::set_builtin_env(&dir, id, env)
+ }
+
/// Re-reads configured/installed agents, drops cached probes, and notifies subscribers.
pub fn reload_providers(&self) {
let specs = provider::load(self.shared.paths.custom_agents_dir.as_deref());
diff --git a/crates/ide-agent/src/model.rs b/crates/ide-agent/src/model.rs
index a6a96e77..41cf2875 100644
--- a/crates/ide-agent/src/model.rs
+++ b/crates/ide-agent/src/model.rs
@@ -3,6 +3,7 @@
use ide_core::IpcPath;
use serde::{Deserialize, Serialize};
use specta::Type;
+use std::collections::BTreeMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
@@ -357,6 +358,8 @@ pub struct AgentProvider {
pub command_read_only_reason: Option,
/// Whether the command was found (on `PATH`).
pub installed: bool,
+ /// Environment variables it starts with (from its agent file).
+ pub env: BTreeMap,
/// Settings (models, modes…) the agent offered last time it ran; empty before its first session.
pub config: Vec,
/// Slash commands it offered last time.
diff --git a/crates/ide-agent/src/provider.rs b/crates/ide-agent/src/provider.rs
index 0b39eade..410bb89e 100644
--- a/crates/ide-agent/src/provider.rs
+++ b/crates/ide-agent/src/provider.rs
@@ -124,6 +124,7 @@ pub(crate) fn describe(spec: &ProviderSpec) -> AgentProvider {
command_editable: reason.is_none(),
command_read_only_reason: reason,
installed: installed(&spec.command),
+ env: spec.env.iter().cloned().collect(),
config: vec![],
commands: vec![],
source: spec.source.clone(),
@@ -133,6 +134,16 @@ pub(crate) fn describe(spec: &ProviderSpec) -> AgentProvider {
/// Overrides built-in `id`'s command through its agent file, preserving other
/// customizations. `None` restores the built-in command and removes a redundant file.
pub(crate) fn set_builtin_command(custom_dir: &Path, id: &str, command: Option<&str>) -> IdeResult<()> {
+ update_builtin(custom_dir, id, |agent, builtin| agent.command = command.unwrap_or(&builtin.command).to_owned())
+}
+
+/// The environment built-in `id` starts with, through its agent file (empty clears it).
+pub(crate) fn set_builtin_env(custom_dir: &Path, id: &str, env: BTreeMap) -> IdeResult<()> {
+ update_builtin(custom_dir, id, |agent, _| agent.env = env)
+}
+
+/// Changes built-in `id`'s agent file; a file left with only the built-in's values is removed.
+fn update_builtin(custom_dir: &Path, id: &str, change: impl FnOnce(&mut CustomAgent, &ProviderSpec)) -> IdeResult<()> {
let Some(builtin) = builtin_spec(id) else {
return Err(IdeError::invalid(format!("{id} is not a built-in agent")));
};
@@ -152,7 +163,8 @@ pub(crate) fn set_builtin_command(custom_dir: &Path, id: &str, command: Option<&
if agent.registry.is_some() {
return Err(IdeError::invalid(format!("{id} is installed from the ACP Registry")));
}
- if command.is_none()
+ change(&mut agent, &builtin);
+ if agent.command == builtin.command
&& agent.name.as_deref() == Some(builtin.name.as_str())
&& agent.driver == builtin.driver
&& agent.args == builtin.args
@@ -164,7 +176,6 @@ pub(crate) fn set_builtin_command(custom_dir: &Path, id: &str, command: Option<&
}
return Ok(());
}
- agent.command = command.unwrap_or(&builtin.command).to_owned();
agent.write(&file)?;
Ok(())
}
@@ -368,6 +379,23 @@ mod tests {
assert!(installed("sh"));
}
+ #[test]
+ fn set_builtin_env_writes_and_clears_the_environment() {
+ let dir = tempfile::tempdir().unwrap();
+ let env = BTreeMap::from([("CLAUDE_CODE_ENTRYPOINT".to_owned(), "openidea".to_owned())]);
+ set_builtin_env(dir.path(), "claude", env.clone()).unwrap();
+ let claude = load(Some(dir.path())).into_iter().find(|s| s.id == "claude").unwrap();
+ assert_eq!(claude.env, vec![("CLAUDE_CODE_ENTRYPOINT".to_owned(), "openidea".to_owned())]);
+ assert_eq!(describe(&claude).env, env);
+ // The command override and the environment are kept independently.
+ set_builtin_command(dir.path(), "claude", Some("/opt/claude")).unwrap();
+ set_builtin_command(dir.path(), "claude", None).unwrap();
+ assert!(dir.path().join("claude.toml").is_file());
+ set_builtin_env(dir.path(), "claude", BTreeMap::new()).unwrap();
+ assert!(!dir.path().join("claude.toml").exists());
+ set_builtin_env(dir.path(), "not-a-builtin", env).unwrap_err();
+ }
+
#[test]
fn set_builtin_command_overrides_and_clears_a_builtin() {
let dir = tempfile::tempdir().unwrap();
diff --git a/crates/ide-host/src/ops/agent.rs b/crates/ide-host/src/ops/agent.rs
index 758cebb9..9662dc07 100644
--- a/crates/ide-host/src/ops/agent.rs
+++ b/crates/ide-host/src/ops/agent.rs
@@ -30,6 +30,17 @@ pub fn set_command(host: &Host, window: &str, id: String, command: Option,
+) -> IdeResult<()> {
+ host.require_trust(window)?;
+ host.project(window)?.agents.set_builtin_env(&id, env)
+}
+
/// Where ACP Registry agents are saved (`/agents`, like the user's own) and installed.
pub(crate) fn registry_paths(host: &Host) -> RegistryPaths {
RegistryPaths::new(&host.config_dir, &host.local_data_dir)
@@ -195,6 +206,7 @@ pub(crate) fn register(d: &mut Dispatcher) {
d.add("agent_subscribe", |c| async move { subscribe(&c.host, &c.window, c.channel("onEvent")?) });
d.add("agent_providers", |c| async move { providers(&c.host, &c.window) });
d.add("agent_set_command", |c| async move { set_command(&c.host, &c.window, c.arg("id")?, c.arg("command")?) });
+ d.add("agent_set_env", |c| async move { set_env(&c.host, &c.window, c.arg("id")?, c.arg("env")?) });
d.add("agent_registry_list", |c| async move { registry_list(&c.host, &c.window, c.arg("request")?).await });
d.add("agent_registry_install", |c| async move {
registry_install(&c.host, &c.window, c.arg("id")?, c.arg("request")?, c.sender("onEvent")?).await
diff --git a/crates/ide-host/src/ops/build.rs b/crates/ide-host/src/ops/build.rs
index d024fd0c..472411a3 100644
--- a/crates/ide-host/src/ops/build.rs
+++ b/crates/ide-host/src/ops/build.rs
@@ -222,6 +222,22 @@ fn declaring_pom(
)))
}
+/// The `.toml` of a version catalog the build imports as `group:artifact:version`, once a
+/// build has downloaded it (`ide_sdk::libraries::published_version_catalog`).
+pub async fn gradle_published_catalog(
+ host: &Host,
+ window: &str,
+ coordinates: String,
+) -> IdeResult
)}
+ {agent.commandReadOnlyReason !== "registry" && agent.commandReadOnlyReason !== "customAgent" && (
+ <>
+
+
+ env.set(e.target.value)}
+ />
+
+
+ Passed to the agent process, replacing variables of the same name. Changes apply to new sessions.
+
+ >
+ )}
{agent.loginHint && (
<>
@@ -193,6 +221,25 @@ function Details({
);
}
+/** `NAME=value` pairs separated by `;` or new lines. */
+function parseEnv(text: string): Record {
+ const env: Record = {};
+ for (const raw of text.split(/[;\n]/)) {
+ const pair = raw.trim();
+ if (!pair) continue;
+ const eq = pair.indexOf("=");
+ const key = eq < 0 ? "" : pair.slice(0, eq).trim();
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(tx`Expected NAME=value: ${pair}`);
+ env[key] = pair.slice(eq + 1).trim();
+ }
+ return env;
+}
+
+const formatEnv = (env: Record) =>
+ Object.entries(env)
+ .map(([k, v]) => `${k}=${v}`)
+ .join("; ");
+
/** Login hints are plain text with optional backtick-delimited command spans. */
function LoginHint({ text }: { text: string }) {
const translate = useTx();
diff --git a/src/features/agents/agentRegistry.test.tsx b/src/features/agents/agentRegistry.test.tsx
index 98624633..76aadae1 100644
--- a/src/features/agents/agentRegistry.test.tsx
+++ b/src/features/agents/agentRegistry.test.tsx
@@ -145,6 +145,7 @@ describe("agent picker", () => {
commandEditable: true,
commandReadOnlyReason: null,
installed: true,
+ env: {},
config: [],
commands: [],
...p,
@@ -312,6 +313,11 @@ class FakeCommands implements CommandsBackend {
saved: [string, string | null][] = [];
providers = vi.fn(async () => this.list);
chooseExecutable = vi.fn(async () => "/chosen/agent");
+ savedEnv: [string, Record][] = [];
+ async setEnv(id: string, env: Record) {
+ this.savedEnv.push([id, env]);
+ this.list = this.list.map((p) => (p.id === id ? { ...p, env } : p));
+ }
async set(id: string, command: string | null) {
this.saved.push([id, command]);
// What the agent list shows after the override takes effect.
@@ -341,6 +347,7 @@ const claudeProvider = (command: string): AgentProvider => ({
commandEditable: true,
commandReadOnlyReason: null,
installed: true,
+ env: {},
config: [],
commands: [],
source: { kind: "builtin" },
@@ -436,6 +443,23 @@ describe("Built-in executable overrides", () => {
} else expect(screen.queryByRole("dialog")).toBeNull();
});
+ it("writes the agent's environment variables on Apply and shows them again", async () => {
+ const commands = new FakeCommands();
+ commands.list = [claudeProvider("claude")];
+ await openCommands(commands);
+ const env = () => screen.getByRole("textbox", { name: "Environment variables" });
+ fireEvent.change(env(), { target: { value: "CLAUDE_CODE_ENTRYPOINT=openidea; HTTPS_PROXY=http://proxy:8080" } });
+ expect(commands.savedEnv).toEqual([]);
+ await act(async () => fireEvent.click(apply()));
+ expect(commands.savedEnv).toEqual([
+ ["claude", { CLAUDE_CODE_ENTRYPOINT: "openidea", HTTPS_PROXY: "http://proxy:8080" }],
+ ]);
+ expect(env()).toHaveValue("CLAUDE_CODE_ENTRYPOINT=openidea; HTTPS_PROXY=http://proxy:8080");
+ fireEvent.change(env(), { target: { value: "" } });
+ await act(async () => fireEvent.click(apply()));
+ expect(commands.savedEnv.at(-1)).toEqual(["claude", {}]);
+ });
+
it("Cancel discards command edits without writing", async () => {
const commands = new FakeCommands();
commands.list = [
diff --git a/src/features/agents/composerCommands.test.tsx b/src/features/agents/composerCommands.test.tsx
index d4d38312..a0675ba2 100644
--- a/src/features/agents/composerCommands.test.tsx
+++ b/src/features/agents/composerCommands.test.tsx
@@ -50,6 +50,7 @@ const claude: AgentProvider = {
commandOverridden: false,
commandEditable: true,
commandReadOnlyReason: null,
+ env: {},
// Offered by the agent's last session in this project.
commands: [command("compact", "Free up context")],
};
diff --git a/src/features/agents/registry.ts b/src/features/agents/registry.ts
index 60cb80b8..04780dd4 100644
--- a/src/features/agents/registry.ts
+++ b/src/features/agents/registry.ts
@@ -22,6 +22,7 @@ export interface RegistryBackend {
export interface CommandsBackend {
providers(): Promise;
set(id: string, command: string | null): Promise;
+ setEnv(id: string, env: Record): Promise;
chooseExecutable(title: string, current?: string): Promise;
}
@@ -47,6 +48,9 @@ export const tauriCommands: CommandsBackend = {
set: async (id, command) => {
await call(commands.agentSetCommand(id, command));
},
+ setEnv: async (id, env) => {
+ await call(commands.agentSetEnv(id, env));
+ },
};
export const DISTRIBUTION_LABEL: Record = {
diff --git a/src/features/build-tools/versionCatalog.test.ts b/src/features/build-tools/versionCatalog.test.ts
index bc2c695a..24968aa5 100644
--- a/src/features/build-tools/versionCatalog.test.ts
+++ b/src/features/build-tools/versionCatalog.test.ts
@@ -1,5 +1,24 @@
-import { describe, expect, it } from "vitest";
-import { accessorSegments, catalogEntry, chainAt, declaredCatalogs, parseCatalog } from "./versionCatalog";
+import { describe, expect, it, vi } from "vitest";
+
+const ipc = vi.hoisted(() => ({ fsReadText: vi.fn(), gradlePublishedCatalog: vi.fn() }));
+vi.mock("@/platform/ipc", () => ({
+ call: async (p: Promise<{ status: string; data?: unknown; error?: unknown }>) => {
+ const r = await p;
+ if (r.status !== "ok") throw r.error;
+ return r.data;
+ },
+ commands: ipc,
+ onIdeEvent: () => () => {},
+}));
+
+import {
+ accessorSegments,
+ catalogEntry,
+ chainAt,
+ declaredCatalogs,
+ parseCatalog,
+ readCatalogs,
+} from "./versionCatalog";
const toml = `[versions]
springBoot = "3.5.16"
@@ -64,11 +83,35 @@ describe("Gradle version catalogs", () => {
create('tools') {
from(files('gradle/tools.versions.toml'))
}
+ create("platformLibs") { from("com.acme:platform-catalog:1.4.0") }
}
}`;
expect(declaredCatalogs(settings)).toEqual([
{ name: "commonLibs", file: "../shared/common.versions.toml" },
{ name: "tools", file: "gradle/tools.versions.toml" },
+ { name: "platformLibs", artifact: "com.acme:platform-catalog:1.4.0" },
]);
});
+
+ it("reads a published catalog from where a build downloaded it", async () => {
+ const files: Record = {
+ "/p/settings.gradle.kts": `dependencyResolutionManagement { versionCatalogs {
+ create("platformLibs") { from("com.acme:platform-catalog:1.4.0") }
+ create("later") { from("com.acme:later:1.0") }
+ } }`,
+ "/cache/platform-catalog-1.4.0.toml": '[libraries]\nspring-core = "org.springframework:spring-core:6.2.0"\n',
+ };
+ ipc.fsReadText.mockImplementation(async (path: string) =>
+ path in files ? { status: "ok", data: files[path] } : { status: "error", error: { kind: "notFound" } },
+ );
+ ipc.gradlePublishedCatalog.mockImplementation(async (coordinates: string) => ({
+ status: "ok",
+ data: coordinates === "com.acme:platform-catalog:1.4.0" ? "/cache/platform-catalog-1.4.0.toml" : null,
+ }));
+ const { catalogs, complete } = await readCatalogs("/p/build.gradle.kts", "/p");
+ expect(catalogs.map((c) => [c.name, c.path])).toEqual([["platformLibs", "/cache/platform-catalog-1.4.0.toml"]]);
+ expect(catalogEntry(catalogs[0]!, ["spring", "core"])?.value).toBe('"org.springframework:spring-core:6.2.0"');
+ // `later` isn't downloaded yet: read again next time.
+ expect(complete).toBe(false);
+ });
});
diff --git a/src/features/build-tools/versionCatalog.ts b/src/features/build-tools/versionCatalog.ts
index 64330a12..803ad380 100644
--- a/src/features/build-tools/versionCatalog.ts
+++ b/src/features/build-tools/versionCatalog.ts
@@ -99,19 +99,29 @@ export function chainAt(line: string, pos: number): { from: number; to: number;
return segments.length > 1 ? { from, to, segments } : null;
}
-/** Catalogs declared in settings: `create("name") { from(files("path")) }`, paths relative to the build root. */
-export function declaredCatalogs(settings: string): Array<{ name: string; file: string }> {
- const out: Array<{ name: string; file: string }> = [];
- const re = /create\(\s*["'](\w+)["']\s*\)\s*\{[^}]*?from\(\s*files\(\s*["']([^"']+)["']/g;
- for (let m = re.exec(settings); m; m = re.exec(settings)) out.push({ name: m[1]!, file: m[2]! });
+export type DeclaredCatalog = { name: string; file: string } | { name: string; artifact: string };
+
+/**
+ * Catalogs declared in settings: `create("name") { from(files("path")) }`, paths relative to the
+ * build root, or `create("name") { from("group:artifact:version") }`, a published catalog.
+ */
+export function declaredCatalogs(settings: string): DeclaredCatalog[] {
+ const out: DeclaredCatalog[] = [];
+ const re =
+ /create\(\s*["'](\w+)["']\s*\)\s*\{[^}]*?from\(\s*(?:files\(\s*["']([^"']+)["']|["']([^"':]+:[^"':]+:[^"']+)["'])/g;
+ for (let m = re.exec(settings); m; m = re.exec(settings))
+ out.push(m[2] !== undefined ? { name: m[1]!, file: m[2] } : { name: m[1]!, artifact: m[3]! });
return out;
}
const readText = (path: string) => call(commands.fsReadText(path)).catch(() => null);
const join = (dir: string, rel: string) => (rel.startsWith("/") ? rel : `${dir}/${rel.replace(/^\.\//, "")}`);
-/** The catalogs a build script sees: those of the nearest enclosing build (its settings file). */
-async function readCatalogs(script: string, root: string): Promise {
+/**
+ * The catalogs a build script sees: those of the nearest enclosing build (its settings file).
+ * `complete` is false while a published catalog isn't downloaded yet.
+ */
+export async function readCatalogs(script: string, root: string): Promise<{ catalogs: Catalog[]; complete: boolean }> {
let dir = dirname(script);
let settings: string | null = null;
for (;;) {
@@ -123,13 +133,17 @@ async function readCatalogs(script: string, root: string): Promise {
const declared = declaredCatalogs(settings ?? "");
if (!declared.some((d) => d.name === "libs")) declared.push({ name: "libs", file: "gradle/libs.versions.toml" });
const catalogs = await Promise.all(
- declared.map(async ({ name, file }) => {
- const path = join(buildRoot, file);
- const text = await readText(path);
- return text === null ? null : parseCatalog(name, path, text);
+ declared.map(async (d) => {
+ const path =
+ "file" in d
+ ? join(buildRoot, d.file)
+ : await call(commands.gradlePublishedCatalog(d.artifact)).catch(() => null);
+ const text = path ? await readText(path) : null;
+ return text === null ? null : parseCatalog(d.name, path!, text);
}),
);
- return catalogs.filter((c): c is Catalog => c !== null);
+ const complete = declared.every((d, i) => "file" in d || catalogs[i] !== null);
+ return { catalogs: catalogs.filter((c): c is Catalog => c !== null), complete };
}
const loaded = new Map();
@@ -148,11 +162,14 @@ function load(script: string): void {
pending.clear();
scripts.forEach(load);
});
- const p = readCatalogs(script, root);
- pending.set(script, p);
- void p.then((c) => {
- if (pending.get(script) === p) loaded.set(script, c);
+ const p = readCatalogs(script, root).then(({ catalogs, complete }) => {
+ if (pending.get(script) !== p) return catalogs;
+ loaded.set(script, catalogs);
+ // Read again when the script is next opened, once a build has downloaded the catalog.
+ if (!complete) pending.delete(script);
+ return catalogs;
});
+ pending.set(script, p);
}
function open(entry: CatalogEntry, path: string) {
diff --git a/src/features/services/store.ts b/src/features/services/store.ts
index 27d1b835..a9f5bd8f 100644
--- a/src/features/services/store.ts
+++ b/src/features/services/store.ts
@@ -363,13 +363,34 @@ export const pullImage = (connId: string, reference: string) =>
call(commands.dockerPull(r, endpointOf(connId), reference, out)),
);
-export const prune = (connId: string, target: PruneTarget) =>
- task(connId, `Pruning ${target === "allImages" ? "unused images" : target}…`, tx("Could not prune"), (r, out) =>
+export function prune(connId: string, target: PruneTarget) {
+ const title = {
+ containers: tx("Pruning stopped containers…"),
+ images: tx("Pruning dangling images…"),
+ allImages: tx("Pruning unused images…"),
+ volumes: tx("Pruning unused volumes…"),
+ networks: tx("Pruning unused networks…"),
+ system: tx("Pruning the system…"),
+ }[target];
+ return task(connId, title, tx("Could not prune"), (r, out) =>
call(commands.dockerPrune(r, endpointOf(connId), target, out)),
);
+}
export function composeOp(connId: string, project: string, files: string[], workingDir: string | null, op: ComposeOp) {
- return task(connId, `Compose ${op} ${project}…`, tx`Compose ${op} failed for ${project}`, (r, out) =>
+ const { title, failure } = {
+ start: { title: tx`Starting Compose project ${project}…`, failure: tx`Could not start Compose project ${project}` },
+ stop: { title: tx`Stopping Compose project ${project}…`, failure: tx`Could not stop Compose project ${project}` },
+ restart: {
+ title: tx`Restarting Compose project ${project}…`,
+ failure: tx`Could not restart Compose project ${project}`,
+ },
+ down: {
+ title: tx`Taking down Compose project ${project}…`,
+ failure: tx`Could not take down Compose project ${project}`,
+ },
+ }[op];
+ return task(connId, title, failure, (r, out) =>
call(commands.dockerComposeOp(r, endpointOf(connId), project, files, workingDir, op, out)),
);
}
diff --git a/src/features/settings/languages/LanguagePages.tsx b/src/features/settings/languages/LanguagePages.tsx
index 8357c230..aef869b3 100644
--- a/src/features/settings/languages/LanguagePages.tsx
+++ b/src/features/settings/languages/LanguagePages.tsx
@@ -297,7 +297,7 @@ export function JavaPage() {
call(commands.vcsStageHunks(path, indices)));
+ await runOp(tx`Stage hunk of ${path}`, () => call(commands.vcsStageHunks(path, indices)));
await refreshNow();
} finally {
setBusy(false);
diff --git a/src/features/vcs/LocalHistory.tsx b/src/features/vcs/LocalHistory.tsx
index da3c35f9..2c467bcf 100644
--- a/src/features/vcs/LocalHistory.tsx
+++ b/src/features/vcs/LocalHistory.tsx
@@ -97,12 +97,13 @@ export function LocalHistoryEditor({ input }: { input: EditorInput }) {
const ok = await confirmDialog({ title: "Revert", message: question, okLabel: "Revert" });
if (!ok) return;
await runOp(
- "Revert from Local History",
+ tx("Revert from Local History"),
async () => {
for (const e of targets) await call(commands.historyRestore(e.path, e.id));
},
{
- success: targets.length === 1 ? `${baseName(targets[0]!.path)} reverted` : `${targets.length} files reverted`,
+ success:
+ targets.length === 1 ? tx`${baseName(targets[0]!.path)} reverted` : tx`${targets.length} files reverted`,
refresh: true,
},
);
diff --git a/src/features/vcs/MergeEditor.tsx b/src/features/vcs/MergeEditor.tsx
index 0d5c7796..6244d82a 100644
--- a/src/features/vcs/MergeEditor.tsx
+++ b/src/features/vcs/MergeEditor.tsx
@@ -259,14 +259,14 @@ export function MergeEditor({ input }: { input: EditorInput }) {
/** Saves `text` as the resolved file and stages it; like IDEA, that closes the merge editor. */
const writeResolved = async (text: string | null) => {
const ok = await runOp(
- `Resolve ${baseName(path)}`,
+ tx`Resolve ${baseName(path)}`,
async () => {
if (text === null) await call(commands.fsDelete(path));
else await writeFileText(path, text, { save: true, exact: true });
await call(commands.vcsStage([path]));
return true;
},
- { success: `${baseName(path)}: conflicts resolved` },
+ { success: tx`${baseName(path)}: conflicts resolved` },
);
if (ok) {
setResolved(true);
diff --git a/src/features/vcs/branchOps.ts b/src/features/vcs/branchOps.ts
index b5f4adb0..6bd3f894 100644
--- a/src/features/vcs/branchOps.ts
+++ b/src/features/vcs/branchOps.ts
@@ -79,7 +79,7 @@ async function performCheckout(
let options: CheckoutOptions = { mode: "normal", diverged: null };
for (;;) {
const opts = options;
- const outcome = await runOp(`Checkout ${rev}`, () => call(commands.vcsCheckout(repo, rev, false, opts)));
+ const outcome = await runOp(tx`Checkout ${rev}`, () => call(commands.vcsCheckout(repo, rev, false, opts)));
if (!outcome) return false;
// `success` may name the branch now checked out.
if (outcome.kind === "done" || outcome.kind === "conflicts") await refreshNow();
@@ -167,7 +167,7 @@ export async function newBranch(from?: string) {
const previous = useVcs.getState().branch?.head;
if (checkout) await saveAll();
const create = () => call(commands.vcsCreateBranch(repo, r.value, from ?? null, checkout));
- const ok = await runOp(`Create branch ${r.value}`, create, {
+ const ok = await runOp(tx`Create branch ${r.value}`, create, {
success: checkout ? `Created and checked out ${r.value}` : `Created branch ${r.value}`,
});
if (ok !== undefined && checkout) {
@@ -208,8 +208,8 @@ export async function openConflictsInstead(): Promise {
export async function mergeIntoCurrent(branch: string, repo: string | null = currentRepo()) {
const current = (repo === currentRepo() ? useVcs.getState().branch?.head : null) ?? "HEAD";
await saveAll();
- await runOp(`Merge ${branch} into ${current}`, () => call(commands.vcsMerge(repo, branch)), {
- success: `Merged ${branch} into ${current}`,
+ await runOp(tx`Merge ${branch} into ${current}`, () => call(commands.vcsMerge(repo, branch)), {
+ success: tx`Merged ${branch} into ${current}`,
errorActions: conflictActions,
onError: openConflictsInstead,
});
@@ -219,8 +219,8 @@ export async function rebaseOnto(branch: string) {
const repo = currentRepo();
const current = useVcs.getState().branch?.head ?? "HEAD";
await saveAll();
- await runOp(`Rebase ${current} onto ${branch}`, () => call(commands.vcsRebase(repo, branch)), {
- success: `Rebased ${current} onto ${branch}`,
+ await runOp(tx`Rebase ${current} onto ${branch}`, () => call(commands.vcsRebase(repo, branch)), {
+ success: tx`Rebased ${current} onto ${branch}`,
errorActions: conflictActions,
onError: openConflictsInstead,
});
@@ -238,8 +238,8 @@ export async function renameBranch(name: string) {
});
if (!r) return;
const to = r.value.trim();
- const ok = await runOp(`Rename branch ${name}`, () => call(commands.vcsRenameBranch(repo, name, to)), {
- success: `Renamed branch ${name} to ${to}`,
+ const ok = await runOp(tx`Rename branch ${name}`, () => call(commands.vcsRenameBranch(repo, name, to)), {
+ success: tx`Renamed branch ${name} to ${to}`,
});
if (ok !== undefined) {
try {
@@ -257,7 +257,7 @@ export async function renameBranch(name: string) {
*/
export async function deleteBranch(name: string) {
const repo = currentRepo();
- const deleted = await runOp(`Delete branch ${name}`, () => call(commands.vcsDeleteBranch(repo, name)));
+ const deleted = await runOp(tx`Delete branch ${name}`, () => call(commands.vcsDeleteBranch(repo, name)));
if (!deleted) return;
const tracked = deleted.upstream;
const otherTracking = useVcs.getState().branches.some((b) => !b.remote && b.name !== name && b.upstream === tracked);
@@ -274,8 +274,8 @@ export async function deleteBranch(name: string) {
label: "Restore",
run: () => {
done();
- void runOp(`Restore branch ${name}`, () => call(commands.vcsRestoreBranch(repo, deleted)), {
- success: `Restored branch ${name}`,
+ void runOp(tx`Restore branch ${name}`, () => call(commands.vcsRestoreBranch(repo, deleted)), {
+ success: tx`Restored branch ${name}`,
});
},
},
@@ -379,9 +379,13 @@ export async function newBranchInRepos(roots: readonly string[], from?: string)
await eachRepo(
roots,
async (root) =>
- (await runOp(`Create branch ${name}`, () => call(commands.vcsCreateBranch(root, name, from ?? null, r.checked)), {
- refresh: false,
- })) !== undefined,
+ (await runOp(
+ tx`Create branch ${name}`,
+ () => call(commands.vcsCreateBranch(root, name, from ?? null, r.checked)),
+ {
+ refresh: false,
+ },
+ )) !== undefined,
r.checked ? `Created and checked out ${name}` : `Created branch ${name}`,
);
}
@@ -392,7 +396,7 @@ export async function mergeInRepos(branch: string, roots: readonly string[]) {
await eachRepo(
roots,
async (root) =>
- (await runOp(`Merge ${branch} (${repoName(root)})`, () => call(commands.vcsMerge(root, branch)), {
+ (await runOp(tx`Merge ${branch} (${repoName(root)})`, () => call(commands.vcsMerge(root, branch)), {
refresh: false,
errorActions: conflictActions,
onError: openConflictsInstead,
@@ -407,7 +411,7 @@ export async function rebaseInRepos(branch: string, roots: readonly string[]) {
await eachRepo(
roots,
async (root) =>
- (await runOp(`Rebase onto ${branch} (${repoName(root)})`, () => call(commands.vcsRebase(root, branch)), {
+ (await runOp(tx`Rebase onto ${branch} (${repoName(root)})`, () => call(commands.vcsRebase(root, branch)), {
refresh: false,
errorActions: conflictActions,
onError: openConflictsInstead,
@@ -428,7 +432,7 @@ export async function deleteInRepos(name: string, roots: readonly string[]) {
await eachRepo(
roots,
async (root) =>
- (await runOp(`Delete branch ${name} (${repoName(root)})`, () => call(commands.vcsDeleteBranch(root, name)), {
+ (await runOp(tx`Delete branch ${name} (${repoName(root)})`, () => call(commands.vcsDeleteBranch(root, name)), {
refresh: false,
})) !== undefined,
`Deleted branch ${name}`,
diff --git a/src/features/vcs/changeOps.tsx b/src/features/vcs/changeOps.tsx
index be93de05..a50adc30 100644
--- a/src/features/vcs/changeOps.tsx
+++ b/src/features/vcs/changeOps.tsx
@@ -90,8 +90,23 @@ export async function commitChanges(): Promise {
success: () => {
const n = plan.included.length;
const summary = firstLine(message);
- const where = repos.length > 1 ? ` in ${repos.length} repositories` : "";
- return `${n} file${n === 1 ? "" : "s"} ${amend ? "amended" : "committed"}${where}${summary ? `: ${summary}` : ""}`;
+ const several = repos.length;
+ const done = amend
+ ? several > 1
+ ? n === 1
+ ? tx`1 file amended in ${several} repositories`
+ : tx`${n} files amended in ${several} repositories`
+ : n === 1
+ ? tx("1 file amended")
+ : tx`${n} files amended`
+ : several > 1
+ ? n === 1
+ ? tx`1 file committed in ${several} repositories`
+ : tx`${n} files committed in ${several} repositories`
+ : n === 1
+ ? tx("1 file committed")
+ : tx`${n} files committed`;
+ return summary ? `${done}: ${summary}` : done;
},
},
);
@@ -199,12 +214,12 @@ function RollbackDialog({ files, onClose }: { files: FileStatus[]; onClose: () =
await saveAll();
const { unstage, discard } = rollbackPlan(chosen, deleteAdded);
await runOp(
- "Rollback",
+ tx("Rollback"),
async () => {
if (unstage.length) await call(commands.vcsUnstage(unstage));
if (discard.length) await call(commands.vcsDiscard(discard));
},
- { success: `Rolled back ${chosen.length} file${chosen.length === 1 ? "" : "s"}` },
+ { success: chosen.length === 1 ? tx("Rolled back 1 file") : tx`Rolled back ${chosen.length} files` },
);
};
return (
@@ -284,7 +299,7 @@ export async function addToVcs(paths: readonly string[]) {
});
return;
}
- await runOp(`Add ${targets.length} file${targets.length === 1 ? "" : "s"} to Git`, () =>
+ await runOp(targets.length === 1 ? tx("Add 1 file to Git") : tx`Add ${targets.length} files to Git`, () =>
call(commands.vcsStage(targets.map(stripSlash))),
);
}
@@ -299,9 +314,9 @@ export function stashChanges(repo = currentRepo()): Promise {
if (r) {
await saveAll();
await runOp(
- "Stash changes",
+ tx("Stash changes"),
() => call(commands.vcsStashPush(repo, r.message || null, r.includeUntracked, r.keepIndex)),
- { success: "Changes stashed" },
+ { success: tx("Changes stashed") },
);
void loadStashes().catch(() => undefined);
}
@@ -372,10 +387,10 @@ export interface UnstashOptions {
export function unstashTitles(o: UnstashOptions): { title: string; success: string } {
const ref = `stash@{${o.index}}`;
if (o.branch)
- return { title: tx`Unstash ${ref} as branch ${o.branch}`, success: `Unstashed ${ref} on branch ${o.branch}` };
+ return { title: tx`Unstash ${ref} as branch ${o.branch}`, success: tx`Unstashed ${ref} on branch ${o.branch}` };
return o.pop
- ? { title: tx`Pop ${ref}`, success: `Unstashed ${ref}` }
- : { title: tx`Apply ${ref}`, success: `Applied ${ref}` };
+ ? { title: tx`Pop ${ref}`, success: tx`Unstashed ${ref}` }
+ : { title: tx`Apply ${ref}`, success: tx`Applied ${ref}` };
}
export async function unstash(o: UnstashOptions, repo = currentRepo()) {
@@ -482,8 +497,8 @@ export async function dropStash(index: number, message: string, repo = currentRe
danger: true,
});
if (!ok) return;
- await runOp(`Drop stash@{${index}}`, () => call(commands.vcsStashDrop(repo, index)), {
- success: `Dropped stash@{${index}}`,
+ await runOp(tx`Drop ${`stash@{${index}}`}`, () => call(commands.vcsStashDrop(repo, index)), {
+ success: tx`Dropped ${`stash@{${index}}`}`,
});
void loadStashes().catch((e) => logConsole("err", String(e)));
}
diff --git a/src/features/vcs/confirmations.tsx b/src/features/vcs/confirmations.tsx
index e71a546c..b0a0ab7c 100644
--- a/src/features/vcs/confirmations.tsx
+++ b/src/features/vcs/confirmations.tsx
@@ -4,6 +4,7 @@
* the deletion staged (`git rm`), with the same three choices.
*/
import { onFileOperation, type FileOperation } from "@/platform/fileOps";
+import { tx } from "@/platform/i18n";
import { call, commands, isTauri } from "@/platform/ipc";
import { loadSettings } from "@/platform/project";
import { workflowSettings } from "@/platform/workflowSettings";
@@ -80,8 +81,12 @@ async function handle(op: FileOperation) {
const n = files.length;
await runOp(
op.kind === "created"
- ? `Add ${n} file${n === 1 ? "" : "s"} to Git`
- : `Remove ${n} file${n === 1 ? "" : "s"} from Git`,
+ ? n === 1
+ ? tx("Add 1 file to Git")
+ : tx`Add ${n} files to Git`
+ : n === 1
+ ? tx("Remove 1 file from Git")
+ : tx`Remove ${n} files from Git`,
() => call(commands.vcsStage(files)),
);
}
diff --git a/src/features/vcs/conflictsUi.tsx b/src/features/vcs/conflictsUi.tsx
index c24969bd..2777e3ea 100644
--- a/src/features/vcs/conflictsUi.tsx
+++ b/src/features/vcs/conflictsUi.tsx
@@ -31,7 +31,7 @@ export function openMergeEditor(path: string) {
async function acceptSide(f: FileStatus, side: "ours" | "theirs") {
const path = stripSlash(f.path);
await runOp(
- `Accept ${side === "ours" ? "yours" : "theirs"} for ${baseName(path)}`,
+ side === "ours" ? tx`Accept yours for ${baseName(path)}` : tx`Accept theirs for ${baseName(path)}`,
async () => {
const v = await call(commands.vcsConflictVersions(path));
const text = side === "ours" ? v.ours : v.theirs;
@@ -43,7 +43,7 @@ async function acceptSide(f: FileStatus, side: "ours" | "theirs") {
}
await call(commands.vcsStage([path]));
},
- { success: `${baseName(path)} resolved` },
+ { success: tx`${baseName(path)} resolved` },
);
}
diff --git a/src/features/vcs/gitMenuDialogs.tsx b/src/features/vcs/gitMenuDialogs.tsx
index 12171251..cc19fb22 100644
--- a/src/features/vcs/gitMenuDialogs.tsx
+++ b/src/features/vcs/gitMenuDialogs.tsx
@@ -238,9 +238,9 @@ function MergeDialog({ onClose }: { onClose: () => void }) {
await saveAll();
const target = head ?? "HEAD";
await runOp(
- `Merge ${branch} into ${target}`,
+ tx`Merge ${branch} into ${target}`,
() => call(commands.vcsMergeWith(repo, [branch], mergeOptionsOf(flags, withMessage ? message : null))),
- { success: `Merged ${branch} into ${target}`, errorActions: conflictActions, onError: openConflictsInstead },
+ { success: tx`Merged ${branch} into ${target}`, errorActions: conflictActions, onError: openConflictsInstead },
);
await refreshNow();
};
@@ -313,7 +313,7 @@ function RebaseDialog({ onClose }: { onClose: () => void }) {
const target = useOnto && onto ? onto : upstream;
const current = head ?? "HEAD";
await runOp(
- `Rebase ${current} onto ${target}`,
+ tx`Rebase ${current} onto ${target}`,
() =>
call(
commands.vcsRebaseWith(repo, upstream, {
@@ -322,7 +322,7 @@ function RebaseDialog({ onClose }: { onClose: () => void }) {
keepEmpty,
}),
),
- { success: `Rebased ${current} onto ${target}`, errorActions: conflictActions, onError: openConflictsInstead },
+ { success: tx`Rebased ${current} onto ${target}`, errorActions: conflictActions, onError: openConflictsInstead },
);
await refreshNow();
};
diff --git a/src/features/vcs/logChanges.ts b/src/features/vcs/logChanges.ts
index 87e1b0d2..c292d945 100644
--- a/src/features/vcs/logChanges.ts
+++ b/src/features/vcs/logChanges.ts
@@ -184,7 +184,7 @@ export async function getFromRevision(changes: readonly LogChange[]) {
if (!ok) return;
await saveAll();
await runOp(
- "Get from Revision",
+ tx("Get from Revision"),
async () => {
for (const c of changes) {
const path = absolutePath(c.path, c.repo);
@@ -211,7 +211,7 @@ export async function revertSelectedChanges(changes: readonly LogChange[], commi
const ids = [...commits].reverse().map((c) => c.id);
const paths = new Set(changes.flatMap((c) => (c.oldPath ? [c.path, c.oldPath] : [c.path])));
const outcome = await runOp(
- "Revert selected changes",
+ tx("Revert selected changes"),
async () => {
const patch = filterPatch(
await call(
diff --git a/src/features/vcs/operations.tsx b/src/features/vcs/operations.tsx
index d0a9bbe0..51ac6ec4 100644
--- a/src/features/vcs/operations.tsx
+++ b/src/features/vcs/operations.tsx
@@ -165,7 +165,7 @@ export async function controlOperation(control: "continue" | "skip" | "abort", e
{
progress,
success,
- failure: `${controlLabel(op, control)} failed`,
+ failure: tx`${controlLabel(op, control)} failed`,
onError: control === "abort" ? undefined : conflictsInstead,
},
);
@@ -241,8 +241,8 @@ export async function cherryPick(commits: Commit[]) {
{ op: "cherryPick", commits: ordered.map((c) => c.id) },
{
progress: "Cherry-picking",
- success: `Cherry-picked ${label}`,
- failure: "Cherry-pick failed",
+ success: tx`Cherry-picked ${label}`,
+ failure: tx("Cherry-pick failed"),
onError: conflictsInstead,
},
commitRepo(commits[0]!) ?? currentRepo(),
@@ -256,7 +256,7 @@ export async function revertCommits(commits: Commit[]) {
ordered.length === 1 ? `${short(ordered[0]!.id)} “${ordered[0]!.summary}”` : `${ordered.length} commits`;
await runGitOperation(
{ op: "revert", commits: ordered.map((c) => c.id) },
- { progress: "Reverting", success: `Reverted ${label}`, failure: "Revert failed", onError: conflictsInstead },
+ { progress: "Reverting", success: tx`Reverted ${label}`, failure: tx("Revert failed"), onError: conflictsInstead },
commitRepo(commits[0]!) ?? currentRepo(),
);
}
@@ -294,8 +294,8 @@ function ResetDialog({ commit, onClose }: { commit?: Commit; onClose: () => void
{ op: "reset", commit: rev, mode },
{
progress: "Resetting",
- success: `Reset ${head} to ${commit ? short(commit.id) : rev} (${mode})`,
- failure: "Reset failed",
+ success: tx`Reset ${head} to ${commit ? short(commit.id) : rev} (${mode})`,
+ failure: tx("Reset failed"),
},
repo,
);
@@ -374,8 +374,8 @@ export async function newTagOn(commit: Commit) {
});
if (!r) return;
const create = () => call(commands.vcsCreateTag(repo, r.value, commit.id, null, false));
- const ok = await runOp(`Create tag ${r.value}`, create, {
- success: `Created tag ${r.value} on ${short(commit.id)}`,
+ const ok = await runOp(tx`Create tag ${r.value}`, create, {
+ success: tx`Created tag ${r.value} on ${short(commit.id)}`,
});
// Tags are not part of the refs signature that reloads the log.
if (ok !== undefined) void reloadLog();
@@ -402,9 +402,9 @@ function TagDialog({ onClose }: { onClose: () => void }) {
onClose();
const tag = name.trim();
const ok = await runOp(
- `Create tag ${tag}`,
+ tx`Create tag ${tag}`,
() => call(commands.vcsCreateTag(repo, tag, commit.trim() || "HEAD", message || null, force)),
- { success: `Created tag ${tag}` },
+ { success: tx`Created tag ${tag}` },
);
if (ok !== undefined && useGitLog.getState().commits.length) void reloadLog();
};
diff --git a/src/features/vcs/ops.ts b/src/features/vcs/ops.ts
index db134452..504dd309 100644
--- a/src/features/vcs/ops.ts
+++ b/src/features/vcs/ops.ts
@@ -89,7 +89,7 @@ export async function runOp(title: string, fn: () => Promise, opts: OpOpti
notifications().notify({
group: "vcs",
level: "error",
- title: `${title} failed`,
+ title: tx`${title} failed`,
body: message,
actions: opts.errorActions?.(message),
});
diff --git a/src/features/vcs/patches.tsx b/src/features/vcs/patches.tsx
index ab3a5883..0f4ee082 100644
--- a/src/features/vcs/patches.tsx
+++ b/src/features/vcs/patches.tsx
@@ -116,7 +116,7 @@ function CreatePatchDialog({ origin, onClose }: { origin: PatchOrigin; onClose:
reverse,
};
const done = await runOp(
- "Create patch",
+ tx("Create patch"),
async () => {
const patch =
origin.kind === "shelf"
@@ -250,7 +250,7 @@ function ApplyPatchDialog({ patch, source, onClose }: { patch: string; source: s
const run = async () => {
onClose();
await saveAll();
- const outcome = await runOp(`Apply patch (${source})`, () => call(commands.vcsApplyPatch(repo, patch)), {
+ const outcome = await runOp(tx`Apply patch (${source})`, () => call(commands.vcsApplyPatch(repo, patch)), {
refresh: false,
});
if (outcome) await reportApply(outcome, "Patch applied");
diff --git a/src/features/vcs/rebase.tsx b/src/features/vcs/rebase.tsx
index 044b0218..a6a74155 100644
--- a/src/features/vcs/rebase.tsx
+++ b/src/features/vcs/rebase.tsx
@@ -62,8 +62,8 @@ export async function undoCommit(commit: Commit) {
if (!ok) return;
}
const undo = () => call(commands.vcsUndoCommit(repo, commit.id));
- const message = await runOp(`Undo commit ${short(commit.id)}`, undo, {
- success: `Undid commit ${short(commit.id)} “${commit.summary}”; its changes are staged`,
+ const message = await runOp(tx`Undo commit ${short(commit.id)}`, undo, {
+ success: tx`Undid commit ${short(commit.id)} “${commit.summary}”; its changes are staged`,
});
// IDEA offers the undone message for the next commit.
if (message !== undefined && !useCommit.getState().message.trim()) useCommit.setState({ message });
@@ -123,7 +123,7 @@ async function runRebase(base: string | null, entries: RebaseEntry[], done: stri
{
progress: "Rebasing",
success: () => (inProgress() === "rebase" ? "Rebase stopped for editing; continue the rebase when done" : done),
- failure: "Rebase failed",
+ failure: tx("Rebase failed"),
onError: conflictsInstead,
},
repo,
diff --git a/src/features/vcs/shelf.tsx b/src/features/vcs/shelf.tsx
index 9e808b91..07d8bf59 100644
--- a/src/features/vcs/shelf.tsx
+++ b/src/features/vcs/shelf.tsx
@@ -144,7 +144,7 @@ function ShelveDialog({
if (!chosen.length || !name.trim()) return;
onClose();
await saveAll();
- const entry = await runOp(`Shelve changes “${name.trim()}”`, () => call(commands.vcsShelve(name.trim(), chosen)));
+ const entry = await runOp(tx`Shelve changes “${name.trim()}”`, () => call(commands.vcsShelve(name.trim(), chosen)));
if (!entry) return;
void loadShelves().catch(() => undefined);
notifications().notify({
@@ -218,7 +218,7 @@ function UnshelveDialog({ entry, path, onClose }: { entry: ShelvedChangeList; pa
onClose();
await saveAll();
const outcome = await runOp(
- `Unshelve “${entry.name}”`,
+ tx`Unshelve “${entry.name}”`,
() => call(commands.vcsUnshelve(entry.id, remove, path ?? null)),
{
refresh: false,
@@ -272,7 +272,7 @@ export async function deleteShelf(entry: ShelvedChangeList, path?: string) {
danger: true,
});
if (!ok) return;
- await runOp(`Delete shelved changes “${entry.name}”`, () => call(commands.vcsShelfDelete(entry.id, path ?? null)), {
+ await runOp(tx`Delete shelved changes “${entry.name}”`, () => call(commands.vcsShelfDelete(entry.id, path ?? null)), {
refresh: false,
});
void loadShelves().catch(() => undefined);
@@ -286,7 +286,7 @@ export async function renameShelf(entry: ShelvedChangeList) {
okLabel: "Rename",
});
if (!r || r.value === entry.name) return;
- await runOp("Rename shelved changes", () => call(commands.vcsShelfRename(entry.id, r.value)), { refresh: false });
+ await runOp(tx("Rename shelved changes"), () => call(commands.vcsShelfRename(entry.id, r.value)), { refresh: false });
void loadShelves().catch(() => undefined);
}
@@ -388,7 +388,7 @@ export async function importShelfPatches(paths?: string[]) {
if (!picked) return;
for (const path of typeof picked === "string" ? [picked] : picked) {
await runOp(
- "Import patches",
+ tx("Import patches"),
async () => call(commands.vcsShelfImport(repo, baseName(path), await call(commands.fsReadText(path)))),
{ refresh: false },
);
diff --git a/src/platform/colorScheme.test.ts b/src/platform/colorScheme.test.ts
index 67cecc22..08e95786 100644
--- a/src/platform/colorScheme.test.ts
+++ b/src/platform/colorScheme.test.ts
@@ -92,6 +92,19 @@ describe("color schemes", () => {
root.dataset.theme = "dark";
await Promise.resolve();
expect(scheme()).toBe("dark");
+ // A scheme chosen in the same change as the theme (one Apply) goes with it.
+ await new Promise((r) => setTimeout(r));
+ useProject.setState({ settings: { editor: { colorScheme: "solarized-dark" } } as unknown as Settings });
+ root.dataset.theme = "light";
+ await Promise.resolve();
+ expect(scheme()).toBe("solarized-dark");
+ // A later theme change on its own still switches it back.
+ await new Promise((r) => setTimeout(r));
+ root.dataset.theme = "dark";
+ await Promise.resolve();
+ root.dataset.theme = "light";
+ await Promise.resolve();
+ expect(scheme()).toBe("theme-default");
} finally {
dispose();
delete root.dataset.colorTheme;
diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json
index ed0d5867..4a0dfc89 100644
--- a/src/platform/locales/text/zh.json
+++ b/src/platform/locales/text/zh.json
@@ -1478,7 +1478,7 @@
"Passed as -Dmaven.repo.local.": "以 -Dmaven.repo.local 传入。",
"Passed as -s (mirrors, servers, proxies).": "以 -s 传入(镜像、服务器、代理)。",
"Passed to rustfmt when reformatting, e.g. --edition 2024": "重新格式化时传递给 rustfmt,例如 --edition 2024",
- "Passed to the JVM running jdtls (java.jdt.ls.vmargs), e.g. -Xmx4g. jdtls runs with -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Xmx2G -Xms100m by default; an option set here replaces the default for the same setting, and another collector (e.g. -XX:+UseG1GC) replaces the default collector options. A Lombok agent is added automatically: the one installed in Language Servers, else, when the project uses Lombok (its build, an inherited parent POM or Gradle build, or its sources' imports), the newest from the Maven / Gradle caches.": "传递给运行 jdtls 的 JVM (java.jdt.ls.vmargs),例如 -Xmx4g。jdtls 默认使用 -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Xmx2G -Xms100m 运行;此处设置的选项会替换同一设置的默认值,指定其他垃圾回收器(例如 -XX:+UseG1GC)时会替换默认的回收器选项。会自动添加 Lombok 代理:优先使用在“语言服务器”中安装的版本;否则当项目使用 Lombok(构建文件、继承的父 POM 或 Gradle 构建、源码中的 import)时,使用 Maven / Gradle 缓存中最新的版本。",
+ "Passed to the JVM running jdtls (java.jdt.ls.vmargs), e.g. -Xmx4g. jdtls runs with -XX:+UseG1GC -XX:G1PeriodicGCInterval=60000 -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=30 -Xmx2G -Xms100m by default; an option set here replaces the default for the same setting, and another collector (e.g. -XX:+UseZGC) replaces the default collector options. A Lombok agent is added automatically: the one installed in Language Servers, else, when the project uses Lombok (its build, an inherited parent POM or Gradle build, or its sources' imports), the newest from the Maven / Gradle caches.": "传递给运行 jdtls 的 JVM (java.jdt.ls.vmargs),例如 -Xmx4g。jdtls 默认使用 -XX:+UseG1GC -XX:G1PeriodicGCInterval=60000 -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=30 -Xmx2G -Xms100m 运行;此处设置的选项会替换同一设置的默认值,指定其他垃圾回收器(例如 -XX:+UseZGC)时会替换默认的回收器选项。会自动添加 Lombok 代理:优先使用在“语言服务器”中安装的版本;否则当项目使用 Lombok(构建文件、继承的父 POM 或 Gradle 构建、源码中的 import)时,使用 Maven / Gradle 缓存中最新的版本。",
"Pop stash": "弹出储藏",
"Preview Source": "预览源代码",
"Reinstate index": "恢复索引",
@@ -3420,13 +3420,11 @@
"Remove all networks not used by a container?": "移除所有未被容器使用的网络?",
"Remove stopped containers, unused networks, dangling images and the build cache?": "移除已停止的容器、未使用的网络、悬空镜像和构建缓存?",
"Prune {target}": "清理{target}",
- "Pruning {target}…": "正在清理 {target}…",
"Pulling {name}…": "正在拉取 {name}…",
"containers": "容器",
"images": "镜像",
"volumes": "卷",
"networks": "网络",
- "unused images": "未使用的镜像",
"Compose project '{name}'": "Compose 项目 '{name}'",
"Engine {server} · CLI {client}": "引擎 {server} · CLI {client}",
" · context {name}": " · 上下文 {name}",
@@ -5847,5 +5845,72 @@
"Could not remove network": "无法删除网络",
"Could not pull {reference}": "无法拉取 {reference}",
"Could not prune": "无法清理",
- "Compose {op} failed for {project}": "Compose {op} 在 {project} 上失败"
+ "Pruning stopped containers…": "正在清理已停止的容器…",
+ "Pruning dangling images…": "正在清理悬空镜像…",
+ "Pruning unused images…": "正在清理未使用的镜像…",
+ "Pruning unused volumes…": "正在清理未使用的卷…",
+ "Pruning unused networks…": "正在清理未使用的网络…",
+ "Pruning the system…": "正在清理系统…",
+ "Starting Compose project {project}…": "正在启动 Compose 项目 {project}…",
+ "Stopping Compose project {project}…": "正在停止 Compose 项目 {project}…",
+ "Restarting Compose project {project}…": "正在重启 Compose 项目 {project}…",
+ "Taking down Compose project {project}…": "正在停止并移除 Compose 项目 {project}…",
+ "Could not start Compose project {project}": "无法启动 Compose 项目 {project}",
+ "Could not stop Compose project {project}": "无法停止 Compose 项目 {project}",
+ "Could not restart Compose project {project}": "无法重启 Compose 项目 {project}",
+ "Could not take down Compose project {project}": "无法停止并移除 Compose 项目 {project}",
+ "Stage hunk of {path}": "暂存 {path} 的块",
+ "{path} reverted": "已还原 {path}",
+ "{length} files reverted": "已还原 {length} 个文件",
+ "Resolve {path}": "解决 {path}",
+ "{path}: conflicts resolved": "{path}:冲突已解决",
+ "Create branch {value}": "创建分支 {value}",
+ "Merge {branch} into {current}": "将 {branch} 合并到 {current}",
+ "Merged {branch} into {current}": "已将 {branch} 合并到 {current}",
+ "Rebase {current} onto {branch}": "将 {current} 变基到 {branch}",
+ "Rebased {current} onto {branch}": "已将 {current} 变基到 {branch}",
+ "Rename branch {name}": "重命名分支 {name}",
+ "Renamed branch {name} to {to}": "已将分支 {name} 重命名为 {to}",
+ "Delete branch {name}": "删除分支 {name}",
+ "Restore branch {name}": "恢复分支 {name}",
+ "Merge {branch} ({root})": "合并 {branch}({root})",
+ "Rebase onto {branch} ({root})": "变基到 {branch}({root})",
+ "Delete branch {name} ({root})": "删除分支 {name}({root})",
+ "1 file amended in {several} repositories": "已在 {several} 个仓库中修改 1 个文件的提交",
+ "{n} files amended in {several} repositories": "已在 {several} 个仓库中修改 {n} 个文件的提交",
+ "1 file amended": "已修改 1 个文件的提交",
+ "{n} files amended": "已修改 {n} 个文件的提交",
+ "1 file committed in {several} repositories": "已在 {several} 个仓库中提交 1 个文件",
+ "{n} files committed in {several} repositories": "已在 {several} 个仓库中提交 {n} 个文件",
+ "1 file committed": "已提交 1 个文件",
+ "{n} files committed": "已提交 {n} 个文件",
+ "Rolled back 1 file": "已回滚 1 个文件",
+ "Rolled back {length} files": "已回滚 {length} 个文件",
+ "Add 1 file to Git": "将 1 个文件添加到 Git",
+ "Add {length} files to Git": "将 {length} 个文件添加到 Git",
+ "Stash changes": "储藏更改",
+ "Changes stashed": "已储藏更改",
+ "Unstashed {ref} on branch {branch}": "已在分支 {branch} 上取消储藏 {ref}",
+ "Unstashed {ref}": "已取消储藏 {ref}",
+ "Applied {ref}": "已应用 {ref}",
+ "Dropped {ref}": "已删除 {ref}",
+ "Remove 1 file from Git": "从 Git 中移除 1 个文件",
+ "Remove {n} files from Git": "从 Git 中移除 {n} 个文件",
+ "Accept yours for {path}": "对 {path} 接受你的版本",
+ "Accept theirs for {path}": "对 {path} 接受他们的版本",
+ "{path} resolved": "{path} 已解决",
+ "Revert selected changes": "还原所选更改",
+ "Create tag {value}": "创建标签 {value}",
+ "Created tag {value} on {id}": "已在 {id} 上创建标签 {value}",
+ "Created tag {tag}": "已创建标签 {tag}",
+ "Create patch": "创建补丁",
+ "Apply patch ({source})": "应用补丁({source})",
+ "Undo commit {id}": "撤销提交 {id}",
+ "Undid commit {id} “{summary}”; its changes are staged": "已撤销提交 {id}“{summary}”,其更改已暂存",
+ "Shelve changes “{trim}”": "搁置更改“{trim}”",
+ "Unshelve “{name}”": "取消搁置“{name}”",
+ "Delete shelved changes “{name}”": "删除搁置的更改“{name}”",
+ "Rename shelved changes": "重命名搁置的更改",
+ "Passed to the agent process, replacing variables of the same name. Changes apply to new sessions.": "传给智能体进程,覆盖同名变量。对新会话生效。",
+ "Expected NAME=value: {pair}": "应为 NAME=value:{pair}"
}