дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(agents): environment variables for built-in agents (#1157)
Settings › Tools › Agents › Built-in has an Environment variables field under the executable. They are saved in the agent's file like a command override, and the agent starts with them on top of the shell environment, replacing variables of the same name. Changing the command or the environment keeps the other; a file left with only the built-in's values is removed. Closes #1133
Цей коміт міститься в:
1 джерело
e88e7e6459
коміт
5e73b1634f
12 files changed
+153
-3
No files matched your search
@@ -91,6 +91,11 @@ export const commands = {
|
||||
* built-in's driver and arguments), or returns it to auto-detection with `None`.
|
||||
*/
|
||||
agentSetCommand: (id: string, command: string | null) => typedError<null, IdeError>(__TAURI_INVOKE("agent_set_command", { id, command })),
|
||||
/**
|
||||
* Environment variables built-in agent `id` starts with (saved in its agent file);
|
||||
* an empty map clears them.
|
||||
*/
|
||||
agentSetEnv: (id: string, env: { [key in string]: string }) => typedError<null, IdeError>(__TAURI_INVOKE("agent_set_env", { id, env })),
|
||||
/**
|
||||
* Agents of the ACP Registry this machine can run, with what is installed; the list
|
||||
* saved earlier when the registry is unreachable. Cancel with `cancel_request(request)`.
|
||||
@@ -1393,6 +1398,8 @@ export type AgentProvider = {
|
||||
commandReadOnlyReason: AgentCommandReadOnlyReason | null,
|
||||
/** Whether the command was found (on `PATH`). */
|
||||
installed: boolean,
|
||||
/** Environment variables it starts with (from its agent file). */
|
||||
env: { [key in string]: string },
|
||||
/** Settings (models, modes…) the agent offered last time it ran; empty before its first session. */
|
||||
config: AgentConfigOption[],
|
||||
/** Slash commands it offered last time. */
|
||||
|
||||
@@ -93,6 +93,14 @@ function Details({
|
||||
onChanged?.();
|
||||
return command === agent.defaultProgram ? "" : (command ?? "");
|
||||
});
|
||||
const envBase = formatEnv(agent.env);
|
||||
const env = form.useExternal(`agents.${agent.id}.env`, envBase, async (draft) => {
|
||||
const parsed = parseEnv(draft);
|
||||
await backend.setEnv(agent.id, parsed);
|
||||
await refresh();
|
||||
onChanged?.();
|
||||
return formatEnv(parsed);
|
||||
});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// An edit can have been staged by a previous page instance. Its Apply callback
|
||||
// refreshes that instance; the mounted one follows the newly applied value too.
|
||||
@@ -174,6 +182,26 @@ function Details({
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{agent.commandReadOnlyReason !== "registry" && agent.commandReadOnlyReason !== "customAgent" && (
|
||||
<>
|
||||
<label className={styles.label} htmlFor={`agent-env-${agent.id}`}>
|
||||
<T>Environment variables</T>
|
||||
</label>
|
||||
<div className={styles.controls}>
|
||||
<Input
|
||||
id={`agent-env-${agent.id}`}
|
||||
mono
|
||||
value={env.value}
|
||||
disabled={env.saving}
|
||||
placeholder="NAME=value; OTHER=value"
|
||||
onChange={(e) => env.set(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<p className={styles.hint}>
|
||||
<T>Passed to the agent process, replacing variables of the same name. Changes apply to new sessions.</T>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{agent.loginHint && (
|
||||
<>
|
||||
<h4 className={styles.section}>
|
||||
@@ -193,6 +221,25 @@ function Details({
|
||||
);
|
||||
}
|
||||
|
||||
/** `NAME=value` pairs separated by `;` or new lines. */
|
||||
function parseEnv(text: string): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
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<string, string>) =>
|
||||
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();
|
||||
|
||||
@@ -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<string, string>][] = [];
|
||||
async setEnv(id: string, env: Record<string, string>) {
|
||||
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 = [
|
||||
|
||||
@@ -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")],
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface RegistryBackend {
|
||||
export interface CommandsBackend {
|
||||
providers(): Promise<AgentProvider[]>;
|
||||
set(id: string, command: string | null): Promise<void>;
|
||||
setEnv(id: string, env: Record<string, string>): Promise<void>;
|
||||
chooseExecutable(title: string, current?: string): Promise<string | null>;
|
||||
}
|
||||
|
||||
@@ -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<RegistryDistribution, string> = {
|
||||
|
||||
@@ -5910,5 +5910,7 @@
|
||||
"Shelve changes “{trim}”": "搁置更改“{trim}”",
|
||||
"Unshelve “{name}”": "取消搁置“{name}”",
|
||||
"Delete shelved changes “{name}”": "删除搁置的更改“{name}”",
|
||||
"Rename shelved changes": "重命名搁置的更改"
|
||||
"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}"
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача