feat(webui): highlight slash commands and app mentions (#4933)
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
splitCapabilityMentionSegments,
|
||||
type CapabilityMentionSegment,
|
||||
} from "@/components/CliAppMentionText";
|
||||
import { INLINE_TOKEN_HIGHLIGHT_COLOR } from "@/components/InlineTokenHighlight";
|
||||
import {
|
||||
Activity,
|
||||
ArrowUp,
|
||||
@@ -88,55 +89,15 @@ import {
|
||||
logoFallbackUrls,
|
||||
providerBrand,
|
||||
} from "@/lib/provider-brand";
|
||||
import {
|
||||
isSideChannelLifecycle,
|
||||
slashCommandLifecycle,
|
||||
} from "@/lib/slash-command";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const VOICE_SHORTCUT_CODE = "KeyD";
|
||||
const VOICE_SHORTCUT_ARIA = "Control+Shift+D";
|
||||
type VoiceShortcutPlatform = "apple" | "chromeos" | "linux" | "other" | "windows";
|
||||
type ResolvedSlashCommandLifecycle =
|
||||
| "side_channel"
|
||||
| "finalize_active_turn"
|
||||
| "stop_active_turn"
|
||||
| "agent_turn";
|
||||
|
||||
function slashCommandName(content: string): string {
|
||||
return content.split(/\s+/, 1)[0];
|
||||
}
|
||||
|
||||
function slashCommandArgs(content: string, commandName: string): string {
|
||||
return content.slice(commandName.length).trim();
|
||||
}
|
||||
|
||||
function matchingSlashCommand(content: string, slashCommands: SlashCommand[]): SlashCommand | null {
|
||||
const commandName = slashCommandName(content);
|
||||
if (!commandName.startsWith("/")) return null;
|
||||
const command = slashCommands.find((item) => item.command === commandName);
|
||||
if (!command) return null;
|
||||
if (slashCommandArgs(content, command.command).length > 0 && !command.acceptsArgs) return null;
|
||||
return command;
|
||||
}
|
||||
|
||||
function slashCommandLifecycle(
|
||||
content: string,
|
||||
slashCommands: SlashCommand[],
|
||||
): ResolvedSlashCommandLifecycle | null {
|
||||
const command = matchingSlashCommand(content, slashCommands);
|
||||
if (!command) return null;
|
||||
if (command.lifecycle === "agent_turn_with_args") {
|
||||
return slashCommandArgs(content, command.command).length > 0
|
||||
? "agent_turn"
|
||||
: "side_channel";
|
||||
}
|
||||
return command.lifecycle;
|
||||
}
|
||||
|
||||
function isSideChannelLifecycle(lifecycle: ResolvedSlashCommandLifecycle | null): boolean {
|
||||
return (
|
||||
lifecycle === "side_channel"
|
||||
|| lifecycle === "finalize_active_turn"
|
||||
|| lifecycle === "stop_active_turn"
|
||||
);
|
||||
}
|
||||
|
||||
function formatBytes(n: number): string {
|
||||
if (n < 1024) return `${n} B`;
|
||||
@@ -2553,7 +2514,7 @@ function MentionCandidateLogo({
|
||||
}) {
|
||||
const color = (candidate.kind === "cli"
|
||||
? candidate.app.brand_color
|
||||
: candidate.preset.brand_color) || "hsl(var(--primary))";
|
||||
: candidate.preset.brand_color) || INLINE_TOKEN_HIGHLIGHT_COLOR;
|
||||
const rawLogoUrl = candidate.kind === "cli" ? candidate.app.logo_url : candidate.preset.logo_url;
|
||||
const logoUrls = useMemo(() => logoFallbackUrls(rawLogoUrl), [rawLogoUrl]);
|
||||
const { logoUrl, onLogoError, onLogoLoad } = useLogoFallback(logoUrls);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { MessageBubble } from "@/components/MessageBubble";
|
||||
import { AgentActivityCluster } from "@/components/thread/AgentActivityCluster";
|
||||
import { normalizeActivityTimeline, type TurnUnit } from "@/lib/activity-timeline";
|
||||
import type { CliAppInfo, McpPresetInfo, UIMessage } from "@/lib/types";
|
||||
import type { CliAppInfo, McpPresetInfo, SlashCommand, UIMessage } from "@/lib/types";
|
||||
|
||||
interface ThreadMessagesProps {
|
||||
messages: UIMessage[];
|
||||
@@ -12,6 +12,7 @@ interface ThreadMessagesProps {
|
||||
hiddenUserMessageCount?: number;
|
||||
cliApps?: CliAppInfo[];
|
||||
mcpPresets?: McpPresetInfo[];
|
||||
slashCommands?: SlashCommand[];
|
||||
forkBoundaryMessageCount?: number | null;
|
||||
onOpenFilePreview?: (path: string) => void;
|
||||
onForkFromMessage?: (beforeUserIndex: number) => void;
|
||||
@@ -51,6 +52,7 @@ export function ThreadMessages({
|
||||
hiddenUserMessageCount = 0,
|
||||
cliApps = [],
|
||||
mcpPresets = [],
|
||||
slashCommands = [],
|
||||
forkBoundaryMessageCount = null,
|
||||
onOpenFilePreview,
|
||||
onForkFromMessage,
|
||||
@@ -116,6 +118,7 @@ export function ThreadMessages({
|
||||
}
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
slashCommands={slashCommands}
|
||||
onOpenFilePreview={onOpenFilePreview}
|
||||
onForkFromHere={
|
||||
onForkFromMessage && forkIndex !== undefined
|
||||
|
||||
@@ -238,7 +238,7 @@ function useInstalledSettingItems<Payload, Item>({
|
||||
const payload = await fetchPayload(token);
|
||||
if (!isCancelled?.()) setItems(selectItems(payload));
|
||||
} catch {
|
||||
if (!isCancelled?.()) setItems([]);
|
||||
// Keep the last successful catalog during transient focus/visibility refresh failures.
|
||||
}
|
||||
}, [fetchPayload, selectItems, token]);
|
||||
|
||||
@@ -842,6 +842,7 @@ export function ThreadShell({
|
||||
showScrollToBottomButton={!!session}
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
slashCommands={slashCommands}
|
||||
forkBoundaryMessageCount={forkBoundaryMessageCount}
|
||||
hasMoreBefore={hasMoreBefore}
|
||||
loadingOlder={loadingOlder}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
promptTop,
|
||||
} from "@/components/thread/promptNavigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { CliAppInfo, McpPresetInfo, UIMessage } from "@/lib/types";
|
||||
import type { CliAppInfo, McpPresetInfo, SlashCommand, UIMessage } from "@/lib/types";
|
||||
|
||||
export interface ThreadViewportHandle {
|
||||
jumpToUserPrompt: (promptId: string) => void;
|
||||
@@ -40,6 +40,7 @@ interface ThreadViewportProps {
|
||||
showScrollToBottomButton?: boolean;
|
||||
cliApps?: CliAppInfo[];
|
||||
mcpPresets?: McpPresetInfo[];
|
||||
slashCommands?: SlashCommand[];
|
||||
forkBoundaryMessageCount?: number | null;
|
||||
hasMoreBefore?: boolean;
|
||||
loadingOlder?: boolean;
|
||||
@@ -111,6 +112,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
showScrollToBottomButton = true,
|
||||
cliApps = [],
|
||||
mcpPresets = [],
|
||||
slashCommands = [],
|
||||
forkBoundaryMessageCount = null,
|
||||
hasMoreBefore = false,
|
||||
loadingOlder = false,
|
||||
@@ -526,6 +528,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
hiddenUserMessageCount={hiddenUserMessageCount}
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
slashCommands={slashCommands}
|
||||
forkBoundaryMessageCount={visibleForkBoundaryMessageCount}
|
||||
onOpenFilePreview={onOpenFilePreview}
|
||||
onForkFromMessage={onForkFromMessage}
|
||||
|
||||
Reference in New Issue
Block a user