mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: ship the AI assistant as a first-party in-process plugin
Moves src/backend/ai and its routes into plugins/ai, mounted through a small /ai dispatcher instead of its own port. Moves the ai-enabled and ai-private-endpoints settings routes out of user-settings-routes.ts into the plugin too, so it has no leftover core dependents.
This commit is contained in:
1 parent
c58ccbfc3f
commit
504710b065
43 files changed
+568
-262
No files matched your search
@@ -205,7 +205,7 @@ export async function markAiProposalRunInTerminal(
|
||||
|
||||
export async function getAiGloballyEnabled(): Promise<boolean> {
|
||||
try {
|
||||
return (await authApi.get("/users/ai-enabled")).data.enabled;
|
||||
return (await authApi.get("/ai/enabled")).data.enabled;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "get AI enabled setting");
|
||||
}
|
||||
@@ -213,7 +213,7 @@ export async function getAiGloballyEnabled(): Promise<boolean> {
|
||||
|
||||
export async function setAiGloballyEnabled(enabled: boolean): Promise<boolean> {
|
||||
try {
|
||||
return (await authApi.patch("/users/ai-enabled", { enabled })).data.enabled;
|
||||
return (await authApi.patch("/ai/enabled", { enabled })).data.enabled;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "update AI enabled setting");
|
||||
}
|
||||
@@ -221,7 +221,7 @@ export async function setAiGloballyEnabled(enabled: boolean): Promise<boolean> {
|
||||
|
||||
export async function getAiPrivateEndpoints(): Promise<string[]> {
|
||||
try {
|
||||
return (await authApi.get("/users/ai-private-endpoints")).data.hosts;
|
||||
return (await authApi.get("/ai/private-endpoints")).data.hosts;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "get AI endpoint allowlist");
|
||||
}
|
||||
@@ -231,8 +231,7 @@ export async function setAiPrivateEndpoints(
|
||||
hosts: string[],
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
return (await authApi.patch("/users/ai-private-endpoints", { hosts })).data
|
||||
.hosts;
|
||||
return (await authApi.patch("/ai/private-endpoints", { hosts })).data.hosts;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "update AI endpoint allowlist");
|
||||
}
|
||||
|
||||
@@ -22,9 +22,17 @@ import { getPlugins, type PluginSummary } from "@/api/plugins-api";
|
||||
/**
|
||||
* Built-in tab types owned by a plugin, keyed by plugin id. The terminal has
|
||||
* no rail item: it opens from a host, the command palette or quick connect.
|
||||
*
|
||||
* ai is a rail-view/right-dock panel rather than a tab opened via
|
||||
* registerTabComponent (AiPanel is rendered directly by AppShell.tsx and
|
||||
* tabUtils.tsx, the same way TerminalTabContent is for the terminal), so it
|
||||
* belongs here for the same reason: a built-in surface gated by whether its
|
||||
* owning plugin is installed and enabled, not something registered at
|
||||
* runtime.
|
||||
*/
|
||||
const BUILT_IN_TABS_BY_PLUGIN: Record<string, string[]> = {
|
||||
"ssh-terminal": ["terminal"],
|
||||
ai: ["ai"],
|
||||
};
|
||||
|
||||
let pluginEnabled = new Map<string, boolean>();
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
mentionLabel,
|
||||
toolLabel,
|
||||
} from "@/features/ai/labels";
|
||||
import { AI_TOOLS } from "../../../../backend/ai/tools/catalog";
|
||||
import { AI_TOOLS } from "../../../../../plugins/ai/backend/tools/catalog";
|
||||
|
||||
describe("humanize", () => {
|
||||
it("splits camelCase into words", () => {
|
||||
|
||||
@@ -49,6 +49,24 @@ function plugin(overrides: Partial<PluginSummary> = {}): PluginSummary {
|
||||
};
|
||||
}
|
||||
|
||||
function aiPlugin(overrides: Partial<PluginSummary> = {}): PluginSummary {
|
||||
return plugin({
|
||||
id: "ai",
|
||||
name: "AI Assistant",
|
||||
contributes: {
|
||||
tabs: [
|
||||
{
|
||||
id: "ai",
|
||||
titleKey: "nav.ai",
|
||||
icon: "Sparkles",
|
||||
openFrom: ["rail", "palette"],
|
||||
},
|
||||
],
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
describe("plugin state applied to the shell", () => {
|
||||
afterEach(() => {
|
||||
resetPluginState();
|
||||
@@ -139,6 +157,30 @@ describe("plugin state applied to the shell", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("allows the ai tab while the plugin is enabled", () => {
|
||||
applyPluginState([aiPlugin({ enabled: true })]);
|
||||
expect(isTabTypeAvailable("ai")).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks the ai tab when the plugin is disabled", () => {
|
||||
applyPluginState([aiPlugin({ enabled: false })]);
|
||||
expect(isTabTypeAvailable("ai")).toBe(false);
|
||||
});
|
||||
|
||||
it("allows the ai tab again when the plugin is re-enabled", () => {
|
||||
applyPluginState([aiPlugin({ enabled: false })]);
|
||||
expect(isTabTypeAvailable("ai")).toBe(false);
|
||||
|
||||
applyPluginState([aiPlugin({ enabled: true })]);
|
||||
expect(isTabTypeAvailable("ai")).toBe(true);
|
||||
});
|
||||
|
||||
it("disabling ai does not affect the terminal, and vice versa", () => {
|
||||
applyPluginState([aiPlugin({ enabled: false }), plugin({ enabled: true })]);
|
||||
expect(isTabTypeAvailable("ai")).toBe(false);
|
||||
expect(isTabTypeAvailable("terminal")).toBe(true);
|
||||
});
|
||||
|
||||
it("reports enablement, defaulting to true before the first load", () => {
|
||||
expect(isPluginEnabled("ssh-terminal")).toBe(true);
|
||||
|
||||
|
||||
Reference in new issue
Block a user