Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/web-shell/client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ import {
type WebShellMarkdownCustomization,
type ToolHeaderExtraRenderer,
type UserMessageContentRenderer,
type UserMessageContentParser,
type WelcomeHeaderRenderer,
type WelcomeFooterRenderer,
type ComposerToolbarStartRenderer,
Expand All @@ -184,6 +185,9 @@ import {
type MarkdownTableMode,
type WebShellTaskInfo,
type WebShellAtProvider,
type WebShellBuiltinAtProvidersConfig,
type ComposerTagClickHandler,
type ComposerTagRenderer,
type WebShellComposerTagIconMap,
type WebShellBottomStatusItem,
} from './customization';
Expand Down Expand Up @@ -414,6 +418,8 @@ export interface WebShellProps {
hiddenSlashCommands?: string[];
/** Slash command category order. Defaults to custom, skill, system. */
slashCommandCategoryOrder?: CommandDisplayCategoryOrder;
/** Built-in @ mention providers to enable. Defaults to all built-ins. */
builtinAtProviders?: WebShellBuiltinAtProvidersConfig;
/** Additional @ mention categories shown alongside built-in files/extensions. */
atProviders?: readonly WebShellAtProvider[];
/** Icon URLs for custom composer tag kinds used by @ mention chips. */
Expand All @@ -424,8 +430,16 @@ export interface WebShellProps {
renderWelcomeHeader?: WelcomeHeaderRenderer;
/** Custom renderer shown below the chat composer in the empty welcome state. */
renderWelcomeFooter?: WelcomeFooterRenderer;
/** Parse user-message text into display parts such as chips. */
parseUserMessageContent?: UserMessageContentParser;
/** Custom renderer for the inside of user chat bubbles. Defaults to plain text. */
renderUserMessageContent?: UserMessageContentRenderer;
/** Custom renderer for composer and user-message tags. */
renderComposerTag?: ComposerTagRenderer;
/** Custom hover content for composer and user-message tags. */
renderComposerTagTooltip?: ComposerTagRenderer;
/** Click handler for composer and user-message tags. */
onComposerTagClick?: ComposerTagClickHandler;
/** Custom renderer inserted before the built-in chat composer toolbar controls. */
renderComposerToolbarStart?: ComposerToolbarStartRenderer;
/** Custom renderer inserted after the built-in composer toolbar controls. */
Expand Down Expand Up @@ -836,12 +850,17 @@ export function App({
onBugReport,
hiddenSlashCommands,
slashCommandCategoryOrder,
builtinAtProviders,
atProviders,
composerTagIcons,
renderToolHeaderExtra,
renderWelcomeHeader,
renderWelcomeFooter,
parseUserMessageContent,
renderUserMessageContent,
renderComposerTag,
renderComposerTagTooltip,
onComposerTagClick,
renderComposerToolbarStart,
renderComposerToolbarEnd,
renderComposerToolbarRight,
Expand Down Expand Up @@ -957,7 +976,12 @@ export function App({
renderToolHeaderExtra,
renderWelcomeHeader,
renderWelcomeFooter,
parseUserMessageContent,
renderUserMessageContent,
composerTagIcons,
renderComposerTag,
renderComposerTagTooltip,
onComposerTagClick,
renderComposerToolbarStart,
renderComposerToolbarEnd,
renderComposerToolbarRight,
Expand All @@ -972,7 +996,12 @@ export function App({
renderToolHeaderExtra,
renderWelcomeHeader,
renderWelcomeFooter,
parseUserMessageContent,
renderUserMessageContent,
composerTagIcons,
renderComposerTag,
renderComposerTagTooltip,
onComposerTagClick,
renderComposerToolbarStart,
renderComposerToolbarEnd,
renderComposerToolbarRight,
Expand Down Expand Up @@ -5150,6 +5179,7 @@ export function App({
commands={commands}
skills={loadedSkills}
slashCommandCategoryOrder={slashCommandCategoryOrder}
builtinAtProviders={builtinAtProviders}
atProviders={atProviders}
composerTagIcons={composerTagIcons}
queuedMessages={queuedTexts}
Expand Down
100 changes: 93 additions & 7 deletions packages/web-shell/client/components/AtMentionPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { AtMentionPanel } from './AtMentionPanel';
import type { AtMentionMenuState } from '../hooks/useAtMentionMenu';
import type { WebShellAtProvider } from '../customization';
import { I18nProvider } from '../i18n';

Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
Expand All @@ -20,20 +21,34 @@ let container: HTMLDivElement | null = null;
let anchor: HTMLDivElement | null = null;
let root: Root | null = null;

function providerView(provider: WebShellAtProvider) {
return {
id: provider.id,
provider,
label: provider.label,
textValue:
provider.textValue ??
(typeof provider.label === 'string' ? provider.label : provider.id),
description: provider.description,
tabs: provider.tabs,
renderItem: provider.renderItem,
};
}

function categoriesMenu(): AtMentionMenuState {
const provider: WebShellAtProvider = {
id: 'files',
label: 'Files',
description: 'Reference workspace files',
search: async () => [],
};
return {
from: 0,
to: 1,
query: '',
level: 'categories',
selectedIndex: 0,
providers: [
{
id: 'files',
label: 'Files',
description: 'Reference workspace files',
},
],
providers: [providerView(provider)],
items: [],
loading: false,
};
Expand All @@ -54,6 +69,35 @@ function itemsMenu(): AtMentionMenuState {
};
}

function tabbedItemsMenu(): AtMentionMenuState {
const provider: WebShellAtProvider = {
id: 'tables',
label: 'Tables',
tabs: [
{ id: 'mc', label: 'MaxCompute' },
{ id: 'hg', label: 'Hologres' },
],
search: async () => [],
};
return {
...categoriesMenu(),
level: 'items',
selectedProviderId: 'tables',
providers: [providerView(provider)],
tabs: provider.tabs,
selectedTabId: 'mc',
items: [
{
id: 'orders',
label: 'orders',
subtitle: 'project_a',
description: 'daily order table',
insertText: '@orders ',
},
],
};
}

function mount(menu: AtMentionMenuState, handlers = {}) {
container = document.createElement('div');
anchor = document.createElement('div');
Expand Down Expand Up @@ -81,6 +125,7 @@ function mount(menu: AtMentionMenuState, handlers = {}) {
onAccept={vi.fn()}
onBack={vi.fn()}
onSearch={vi.fn()}
onSelectTab={vi.fn()}
{...handlers}
/>
</I18nProvider>,
Expand Down Expand Up @@ -176,6 +221,47 @@ describe('AtMentionPanel', () => {
expect(onSearch).toHaveBeenCalledWith('read');
});

it('renders provider tabs and item subtitle fields', () => {
const onSelectTab = vi.fn();
mount(tabbedItemsMenu(), { onSelectTab });

expect(document.body.textContent).toContain('MaxCompute');
expect(document.body.textContent).toContain('Hologres');
expect(document.body.textContent).toContain('orders');
expect(document.body.textContent).toContain('project_a');
expect(document.body.textContent).toContain('daily order table');

const tabs = [...document.body.querySelectorAll('[role="tab"]')];
act(() => {
tabs[1]?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});

expect(onSelectTab).toHaveBeenCalledWith('hg');
});

it('guards image icon sources', () => {
const menu = itemsMenu();
menu.items = [
{
id: 'safe',
label: 'Safe image',
icon: 'data:image/png;base64,iVBOR',
iconMode: 'image',
},
{
id: 'unsafe',
label: 'Unsafe image',
icon: 'javascript:alert(1)',
iconMode: 'image',
},
];
mount(menu);

const images = [...document.body.querySelectorAll('img')];
expect(images).toHaveLength(1);
expect(images[0]?.getAttribute('src')).toBe('data:image/png;base64,iVBOR');
});

it('focuses search when explicitly requested', () => {
vi.useFakeTimers();
mount({ ...itemsMenu(), inputMode: 'search' });
Expand Down
Loading
Loading