Add configurable terminal profiles
This commit is contained in:
@@ -116,6 +116,18 @@ const MutableMetadataGenerationConfigSchema = z
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const TerminalProfileSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
command: z.string(),
|
||||
args: z.array(z.string()).optional(),
|
||||
icon: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export type TerminalProfile = z.infer<typeof TerminalProfileSchema>;
|
||||
|
||||
export const MutableDaemonConfigSchema = z
|
||||
.object({
|
||||
mcp: z
|
||||
@@ -127,6 +139,7 @@ export const MutableDaemonConfigSchema = z
|
||||
metadataGeneration: MutableMetadataGenerationConfigSchema.default({ providers: [] }),
|
||||
autoArchiveAfterMerge: z.boolean().default(false),
|
||||
appendSystemPrompt: z.string().default(""),
|
||||
terminalProfiles: z.array(TerminalProfileSchema).optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
@@ -139,6 +152,7 @@ export const MutableDaemonConfigPatchSchema = z
|
||||
metadataGeneration: MutableMetadataGenerationConfigSchema.partial().optional(),
|
||||
autoArchiveAfterMerge: z.boolean().optional(),
|
||||
appendSystemPrompt: z.string().optional(),
|
||||
terminalProfiles: z.array(TerminalProfileSchema).optional(),
|
||||
})
|
||||
.partial()
|
||||
.passthrough();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
export const BUILTIN_PROVIDER_ICON_NAMES = [
|
||||
"claude",
|
||||
"codex",
|
||||
"copilot",
|
||||
"kiro",
|
||||
"omp",
|
||||
"opencode",
|
||||
"pi",
|
||||
];
|
||||
|
||||
export const ACP_PROVIDER_ICON_NAMES = [
|
||||
"agoragentic-acp",
|
||||
"amp-acp",
|
||||
"auggie",
|
||||
"autohand",
|
||||
"cline",
|
||||
"codebuddy-code",
|
||||
"codewhale",
|
||||
"cortex-code",
|
||||
"corust-agent",
|
||||
"crow-cli",
|
||||
"cursor",
|
||||
"deepagents",
|
||||
"dimcode",
|
||||
"dirac",
|
||||
"factory-droid",
|
||||
"fast-agent",
|
||||
"gemini",
|
||||
"glm-acp-agent",
|
||||
"goose",
|
||||
"grok",
|
||||
"junie",
|
||||
"kilo",
|
||||
"kimi",
|
||||
"minion-code",
|
||||
"mistral-vibe",
|
||||
"nova",
|
||||
"poolside",
|
||||
"qoder",
|
||||
"qwen-code",
|
||||
"sigit",
|
||||
"stakpak",
|
||||
"vtcode",
|
||||
];
|
||||
|
||||
export const TERMINAL_PROFILE_ICON_NAMES = ["agy"];
|
||||
|
||||
export const KNOWN_PROVIDER_ICON_NAMES = [
|
||||
...BUILTIN_PROVIDER_ICON_NAMES,
|
||||
...ACP_PROVIDER_ICON_NAMES,
|
||||
...TERMINAL_PROFILE_ICON_NAMES,
|
||||
];
|
||||
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DEFAULT_TERMINAL_PROFILES,
|
||||
getTerminalProfileIcon,
|
||||
guessTerminalProfileIcon,
|
||||
resolveTerminalProfiles,
|
||||
} from "./terminal-profiles.js";
|
||||
|
||||
describe("resolveTerminalProfiles", () => {
|
||||
it("returns defaults when undefined", () => {
|
||||
const result = resolveTerminalProfiles(undefined);
|
||||
expect(result).toBe(DEFAULT_TERMINAL_PROFILES);
|
||||
});
|
||||
|
||||
it("returns an empty array as-is when defined as empty", () => {
|
||||
const result = resolveTerminalProfiles([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns a custom array as-is", () => {
|
||||
const custom = [{ id: "zsh", name: "Zsh", command: "zsh" }];
|
||||
const result = resolveTerminalProfiles(custom);
|
||||
expect(result).toBe(custom);
|
||||
});
|
||||
|
||||
it("default profiles include claude, codex, opencode", () => {
|
||||
const ids = DEFAULT_TERMINAL_PROFILES.map((p) => p.id);
|
||||
expect(ids).toContain("claude");
|
||||
expect(ids).toContain("codex");
|
||||
expect(ids).toContain("opencode");
|
||||
});
|
||||
|
||||
it("claude profile has the correct icon", () => {
|
||||
const claude = DEFAULT_TERMINAL_PROFILES.find((p) => p.id === "claude");
|
||||
expect(claude?.icon).toBe("claude");
|
||||
});
|
||||
|
||||
it("codex profile has the correct icon", () => {
|
||||
const codex = DEFAULT_TERMINAL_PROFILES.find((p) => p.id === "codex");
|
||||
expect(codex?.icon).toBe("codex");
|
||||
});
|
||||
|
||||
it("opencode profile has the correct icon", () => {
|
||||
const opencode = DEFAULT_TERMINAL_PROFILES.find((p) => p.id === "opencode");
|
||||
expect(opencode?.icon).toBe("opencode");
|
||||
});
|
||||
});
|
||||
|
||||
describe("guessTerminalProfileIcon", () => {
|
||||
it.each([
|
||||
["claude", "claude"],
|
||||
["codex", "codex"],
|
||||
["opencode", "opencode"],
|
||||
["copilot", "copilot"],
|
||||
["kiro", "kiro"],
|
||||
["omp", "omp"],
|
||||
["pi", "pi"],
|
||||
["gemini", "gemini"],
|
||||
["cursor", "cursor"],
|
||||
["goose", "goose"],
|
||||
["grok", "grok"],
|
||||
["kimi", "kimi"],
|
||||
["Claude", "claude"],
|
||||
["CODEX", "codex"],
|
||||
["/usr/local/bin/claude", "claude"],
|
||||
["C:\\\\Program Files\\\\Codex\\\\codex.exe", "codex"],
|
||||
["/usr/local/bin/gemini", "gemini"],
|
||||
["agy", "agy"],
|
||||
["/usr/local/bin/agy", "agy"],
|
||||
])("guesses %s -> %s", (command, expected) => {
|
||||
expect(guessTerminalProfileIcon(command)).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([["zsh"], ["bash"], ["fish"], [""], ["/usr/bin/foo"], ["cursor-agent"]])(
|
||||
"returns undefined for unknown command %s",
|
||||
(command) => {
|
||||
expect(guessTerminalProfileIcon(command)).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("getTerminalProfileIcon", () => {
|
||||
it("returns the explicit icon when set", () => {
|
||||
const profile = { id: "1", name: "Foo", command: "zsh", icon: "claude" };
|
||||
expect(getTerminalProfileIcon(profile)).toBe("claude");
|
||||
});
|
||||
|
||||
it("guesses the icon when none is set", () => {
|
||||
const profile = { id: "1", name: "Foo", command: "claude" };
|
||||
expect(getTerminalProfileIcon(profile)).toBe("claude");
|
||||
});
|
||||
|
||||
it("returns undefined when no icon is set and command is unknown", () => {
|
||||
const profile = { id: "1", name: "Foo", command: "zsh" };
|
||||
expect(getTerminalProfileIcon(profile)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { TerminalProfile } from "./messages.js";
|
||||
import { KNOWN_PROVIDER_ICON_NAMES } from "./provider-icon-names.js";
|
||||
|
||||
export const DEFAULT_TERMINAL_PROFILES: readonly TerminalProfile[] = [
|
||||
{ id: "claude", name: "Claude Code", command: "claude", icon: "claude" },
|
||||
{ id: "codex", name: "Codex", command: "codex", icon: "codex" },
|
||||
{ id: "opencode", name: "OpenCode", command: "opencode", icon: "opencode" },
|
||||
];
|
||||
|
||||
const WELL_KNOWN_COMMAND_ICONS = new Map(KNOWN_PROVIDER_ICON_NAMES.map((name) => [name, name]));
|
||||
|
||||
function getCommandBaseName(command: string): string {
|
||||
const lastSlash = command.lastIndexOf("/");
|
||||
const lastBackslash = command.lastIndexOf("\\");
|
||||
const start = Math.max(lastSlash, lastBackslash) + 1;
|
||||
const base = command.slice(start).toLowerCase();
|
||||
const dotIndex = base.indexOf(".");
|
||||
return dotIndex > 0 ? base.slice(0, dotIndex) : base;
|
||||
}
|
||||
|
||||
export function guessTerminalProfileIcon(command: string): string | undefined {
|
||||
return WELL_KNOWN_COMMAND_ICONS.get(getCommandBaseName(command));
|
||||
}
|
||||
|
||||
export function getTerminalProfileIcon(profile: TerminalProfile): string | undefined {
|
||||
return profile.icon ?? guessTerminalProfileIcon(profile.command);
|
||||
}
|
||||
|
||||
export function resolveTerminalProfiles(
|
||||
terminalProfiles: TerminalProfile[] | undefined,
|
||||
): readonly TerminalProfile[] {
|
||||
if (terminalProfiles === undefined) {
|
||||
return DEFAULT_TERMINAL_PROFILES;
|
||||
}
|
||||
return terminalProfiles;
|
||||
}
|
||||
Reference in New Issue
Block a user