=> {
+ const connection = resolveTerminalConnection(
+ $selectedTerminalId,
+ $terminalServers ?? [],
+ $settings?.terminalServers ?? [],
+ localStorage.token
+ );
+ const items = await listTerminalSkills(connection, $chatId || null).catch(() => []);
+ terminalSkills.set(items);
+ const q = query.trim().toLowerCase();
+ return q
+ ? items.filter((skill) => `${skill.name} ${skill.description}`.toLowerCase().includes(q))
+ : items;
};
$: if (query) {
@@ -106,7 +134,7 @@
{resolveLocalizedResource(skill, $i18n.language)}
- {skill.id}
+ {skill.source === 'terminal' ? $i18n.t('Terminal') : skill.id}
diff --git a/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte b/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte
index e2e7303484..6f0e2f964d 100644
--- a/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte
+++ b/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte
@@ -3,6 +3,18 @@
import { getContext, onDestroy } from 'svelte';
import { getPrompts } from '$lib/apis/prompts';
import { getSkillItems } from '$lib/apis/skills';
+ import {
+ listTerminalSkills,
+ resolveTerminalConnection,
+ type TerminalSkill
+ } from '$lib/apis/terminal';
+ import {
+ chatId,
+ selectedTerminalId,
+ settings,
+ terminalServers,
+ terminalSkills
+ } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import ChatBubbleDotted from '$lib/components/icons/ChatBubbleDotted.svelte';
import ChatBubbleDottedChecked from '$lib/components/icons/ChatBubbleDottedChecked.svelte';
@@ -84,17 +96,33 @@
clearTimeout(searchDebounceTimer);
});
+ const getTerminalItems = async (query = ''): Promise => {
+ const connection = resolveTerminalConnection(
+ $selectedTerminalId,
+ $terminalServers ?? [],
+ $settings?.terminalServers ?? [],
+ localStorage.token
+ );
+ const items = await listTerminalSkills(connection, $chatId || null).catch(() => []);
+ terminalSkills.set(items);
+ const q = query.trim().toLowerCase();
+ return q
+ ? items.filter((skill) => `${skill.name} ${skill.description}`.toLowerCase().includes(q))
+ : items;
+ };
+
const getItems = async () => {
- const [promptRes, skillRes] = await Promise.all([
+ const [promptRes, skillRes, terminalItems] = await Promise.all([
getPrompts(localStorage.token).catch(() => null),
- getSkillItems(localStorage.token, query).catch(() => null)
+ getSkillItems(localStorage.token, query).catch(() => null),
+ getTerminalItems(query)
]);
if (promptRes) {
prompts = promptRes;
}
- skills = skillRes?.items ?? [];
+ skills = [...(skillRes?.items ?? []), ...terminalItems];
};
export const selectUp = () => {
@@ -447,7 +475,7 @@
{resolveLocalizedResource(skill, $i18n.language)}
- {skill.id}
+ {skill.source === 'terminal' ? $i18n.t('Terminal') : skill.id}
diff --git a/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte b/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte
index 920f40cec6..ad2a3109ac 100644
--- a/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte
+++ b/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte
@@ -4,11 +4,26 @@
import { getContext, onDestroy, tick } from 'svelte';
import { fly } from 'svelte/transition';
- import { user, tools as _tools, skills as _skills, toolServers } from '$lib/stores';
+ import {
+ chatId,
+ selectedTerminalId,
+ settings,
+ terminalServers,
+ terminalSkills,
+ user,
+ tools as _tools,
+ skills as _skills,
+ toolServers
+ } from '$lib/stores';
import { deleteOAuthSession } from '$lib/apis/auths';
import { getTools } from '$lib/apis/tools';
import { getSkills } from '$lib/apis/skills';
+ import {
+ listTerminalSkills,
+ resolveTerminalConnection,
+ type TerminalSkill
+ } from '$lib/apis/terminal';
import { toast } from 'svelte-sonner';
@@ -174,6 +189,18 @@
}
};
+ const getTerminalSkillItems = async (): Promise => {
+ const connection = resolveTerminalConnection(
+ $selectedTerminalId,
+ $terminalServers ?? [],
+ $settings?.terminalServers ?? [],
+ localStorage.token
+ );
+ const items = await listTerminalSkills(connection, $chatId || null).catch(() => []);
+ terminalSkills.set(items);
+ return items;
+ };
+
const loadTools = async (query = toolQuery) => {
const requestId = ++toolRequestId;
const q = query.trim();
@@ -195,7 +222,9 @@
await _skills.set(await getSkills(localStorage.token));
}
if (requestId !== skillRequestId) return;
- setSkills($_skills, q);
+ const terminalItems = await getTerminalSkillItems();
+ if (requestId !== skillRequestId) return;
+ setSkills([...($_skills ?? []), ...terminalItems], q);
};
const scheduleToolSearch = () => {
@@ -252,6 +281,9 @@
}
};
+ const skillSourceLabel = (skill: IntegrationItem | undefined) =>
+ skill?.source === 'terminal' ? $i18n.t('Terminal') : '';
+
onDestroy(() => {
clearTimeout(toolSearchDebounceTimer);
clearTimeout(skillSearchDebounceTimer);
@@ -680,6 +712,11 @@
{resolveLocalizedResource(skills?.[skillId], $i18n.language, 'name')}
+ {#if skillSourceLabel(skills?.[skillId])}
+
+ {skillSourceLabel(skills?.[skillId])}
+
+ {/if}
diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte
index e90b04d641..e085dcc9d2 100644
--- a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte
+++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte
@@ -9,7 +9,7 @@
const i18n = getContext('i18n');
import { WEBUI_BASE_URL } from '$lib/constants';
- import { skills } from '$lib/stores';
+ import { skills, terminalSkills } from '$lib/stores';
import { copyToClipboard, safeLinkUrl, unescapeHtml } from '$lib/utils';
import Image from '$lib/components/common/Image.svelte';
@@ -129,7 +129,7 @@
}}
>
{:else if token.type === 'mention'}
- {#if token.triggerChar === '$' && !$skills?.some((skill) => skill.id === token.id && skill.is_active)}
+ {#if token.triggerChar === '$' && ![...($skills ?? []), ...($terminalSkills ?? [])].some((skill) => skill.id === token.id && skill.is_active)}
{token.raw}
{:else}
diff --git a/src/lib/components/chat/SkillsModal.svelte b/src/lib/components/chat/SkillsModal.svelte
index 499fd4d013..8ad2572234 100644
--- a/src/lib/components/chat/SkillsModal.svelte
+++ b/src/lib/components/chat/SkillsModal.svelte
@@ -1,7 +1,7 @@
@@ -51,6 +53,9 @@
{resolveLocalizedResource(skill, $i18n.language, 'description')}
{/if}
+ {#if skill.source === 'terminal'}
+ {$i18n.t('Terminal')}
+ {/if}
{/each}
diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte
index 930994decc..5e4f5b1d90 100644
--- a/src/lib/components/common/RichTextInput.svelte
+++ b/src/lib/components/common/RichTextInput.svelte
@@ -2,7 +2,7 @@
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import equal from 'fast-deep-equal';
- import { skills } from '$lib/stores';
+ import { skills, terminalSkills } from '$lib/stores';
marked.use({
breaks: true,
@@ -528,7 +528,9 @@
const mentionId = id || slashSkillId;
if (
mentionChar === '$' &&
- !$skills?.some((skill) => skill.id === mentionId && skill.is_active)
+ ![...($skills ?? []), ...($terminalSkills ?? [])].some(
+ (skill) => skill.id === mentionId && skill.is_active
+ )
) {
return match;
}
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts
index 98a63429c0..7e0651994f 100644
--- a/src/lib/stores/index.ts
+++ b/src/lib/stores/index.ts
@@ -70,6 +70,7 @@ export const models: Writable = writable([]);
export const knowledge: Writable = writable(null);
export const tools = writable(null);
export const skills: Writable = writable(null);
+export const terminalSkills: Writable = writable([]);
export const functions = writable(null);
export type WorkspaceSection = 'models' | 'knowledge' | 'prompts' | 'skills' | 'tools';
diff --git a/src/lib/utils/marked/mention-extension.ts b/src/lib/utils/marked/mention-extension.ts
index efbb74fdf0..ab0d0ae3f9 100644
--- a/src/lib/utils/marked/mention-extension.ts
+++ b/src/lib/utils/marked/mention-extension.ts
@@ -1,6 +1,6 @@
// mention-extension.ts
import { get } from 'svelte/store';
-import { skills } from '$lib/stores';
+import { skills, terminalSkills } from '$lib/stores';
type MentionOptions = {
triggerChar?: string; // default "@"
@@ -23,7 +23,12 @@ function mentionStart(src: string) {
function mentionRenderer(token: any, options: MentionOptions = {}) {
const trigger = options.triggerChar ?? '@';
- if (trigger === '$' && !get(skills)?.some((skill) => skill.id === token.id && skill.is_active)) {
+ if (
+ trigger === '$' &&
+ ![...(get(skills) ?? []), ...(get(terminalSkills) ?? [])].some(
+ (skill) => skill.id === token.id && skill.is_active
+ )
+ ) {
return escapeHtml(token.raw);
}
const cls = options.className ?? 'mention';