setValue(e.target.value)}
+ onChange={(e) => {
+ setValue(e.target.value);
+ setSlashMenuDismissed(false);
+ }}
onInput={onInput}
onKeyDown={onKeyDown}
onPaste={onPaste}
@@ -367,6 +489,106 @@ export function ThreadComposer({
);
}
+interface SlashCommandPaletteProps {
+ commands: SlashCommand[];
+ selectedIndex: number;
+ isHero: boolean;
+ onHover: (index: number) => void;
+ onChoose: (command: SlashCommand) => void;
+}
+
+function SlashCommandPalette({
+ commands,
+ selectedIndex,
+ isHero,
+ onHover,
+ onChoose,
+}: SlashCommandPaletteProps) {
+ const { t } = useTranslation();
+ return (
+
+
+ {t("thread.composer.slash.label")}
+
+
+ {commands.map((command, index) => {
+ const Icon = COMMAND_ICONS[command.icon] ?? CircleHelp;
+ const selected = index === selectedIndex;
+ const commandKey = slashCommandI18nKey(command.command);
+ const title = t(`thread.composer.slash.commands.${commandKey}.title`, {
+ defaultValue: command.title,
+ });
+ const description = t(`thread.composer.slash.commands.${commandKey}.description`, {
+ defaultValue: command.description,
+ });
+ return (
+
+ );
+ })}
+
+
+ {t("thread.composer.slash.navigateHint")}
+ {t("thread.composer.slash.selectHint")}
+ {t("thread.composer.slash.closeHint")}
+
+
+ );
+}
+
interface AttachmentChipProps {
image: AttachedImage;
labelRemove: string;
diff --git a/webui/src/components/thread/ThreadShell.tsx b/webui/src/components/thread/ThreadShell.tsx
index 45b164b44..f15551ce5 100644
--- a/webui/src/components/thread/ThreadShell.tsx
+++ b/webui/src/components/thread/ThreadShell.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import {
BarChart3,
BookOpen,
@@ -17,7 +17,8 @@ import { StreamErrorNotice } from "@/components/thread/StreamErrorNotice";
import { ThreadViewport } from "@/components/thread/ThreadViewport";
import { useNanobotStream } from "@/hooks/useNanobotStream";
import { useSessionHistory } from "@/hooks/useSessions";
-import type { ChatSummary, UIMessage } from "@/lib/types";
+import { listSlashCommands } from "@/lib/api";
+import type { ChatSummary, SlashCommand, UIMessage } from "@/lib/types";
import { useClient } from "@/providers/ClientProvider";
interface ThreadShellProps {
@@ -66,8 +67,9 @@ export function ThreadShell({
const chatId = session?.chatId ?? null;
const historyKey = session?.key ?? null;
const { messages: historical, loading, hasPendingToolCalls } = useSessionHistory(historyKey);
- const { client, modelName } = useClient();
+ const { client, modelName, token } = useClient();
const [booting, setBooting] = useState(false);
+ const [slashCommands, setSlashCommands] = useState
([]);
const pendingFirstRef = useRef(null);
const messageCacheRef = useRef