Skip to content

Commit 77e0f6a

Browse files
committed
feat: support useMenubarView config (#3425)
* feat: support useMenubarView config * chore: improve code
1 parent f741854 commit 77e0f6a

17 files changed

Lines changed: 376 additions & 49 deletions

File tree

packages/ai-native/src/browser/ai-core.contribution.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import { AINativeService } from './ai-native.service';
4545
import { AIChatView } from './chat/chat.view';
4646
import { AIInlineCompletionsProvider } from './inline-completions/completeProvider';
4747
import { AICompletionsService } from './inline-completions/service/ai-completions.service';
48-
import { AIChatLayoutConfig } from './layout/layout-config';
48+
import { AIChatLayoutConfig, AIMenubarLayoutConfig } from './layout/layout-config';
4949
import { AIChatTabRenderer, AILeftTabRenderer, AIRightTabRenderer } from './layout/tabbar.view';
5050
import {
5151
AINativeCoreContribution,
@@ -116,11 +116,12 @@ export class AINativeBrowserContribution
116116

117117
initialize() {
118118
this.aiNativeConfigService.enableCapabilities();
119-
this.aiNativeConfigService.enableLayout();
120119

121-
const supportsChatAssistant = this.aiNativeConfigService.capabilities.supportsChatAssistant;
120+
const { supportsChatAssistant } = this.aiNativeConfigService.capabilities;
121+
const { useMenubarView } = this.aiNativeConfigService.layout;
122122

123123
let layoutConfig = this.appConfig.layoutConfig;
124+
let layoutViewSize = this.appConfig.layoutViewSize;
124125

125126
if (supportsChatAssistant) {
126127
layoutConfig = {
@@ -129,7 +130,20 @@ export class AINativeBrowserContribution
129130
};
130131
}
131132

133+
if (useMenubarView) {
134+
layoutViewSize = {
135+
...layoutViewSize,
136+
menubarHeight: 48,
137+
};
138+
139+
layoutConfig = {
140+
...layoutConfig,
141+
...AIMenubarLayoutConfig,
142+
};
143+
}
144+
132145
this.appConfig.layoutConfig = layoutConfig;
146+
this.appConfig.layoutViewSize = layoutViewSize;
133147
}
134148

135149
private registerFeature() {

packages/ai-native/src/browser/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ChatAgentViewService } from './chat/chat-agent.view.service';
2020
import { ChatManagerService } from './chat/chat-manager.service';
2121
import { ChatFeatureRegistry } from './chat/chat.feature.registry';
2222
import { ChatService } from './chat/chat.service';
23+
import { AIMenuBarContribution } from './layout/menu-bar/menu-bar.contribution';
2324
import { ResolveConflictRegistry } from './merge-conflict/merge-conflict.feature.registry';
2425
import { RenameCandidatesProviderRegistry } from './rename/rename.feature.registry';
2526
import { AINativeCoreContribution } from './types';
@@ -31,6 +32,7 @@ export class AINativeModule extends BrowserModule {
3132
contributionProvider = AINativeCoreContribution;
3233
providers: Provider[] = [
3334
AINativeBrowserContribution,
35+
AIMenuBarContribution,
3436
{
3537
token: InlineChatFeatureRegistryToken,
3638
useClass: InlineChatFeatureRegistry,
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1-
import { AI_CHAT_CONTAINER_VIEW_ID } from '../../common';
1+
import { SlotLocation } from '@opensumi/ide-core-browser';
2+
3+
import { AI_CHAT_CONTAINER_VIEW_ID, AI_MENUBAR_CONTAINER_VIEW_ID } from '../../common';
24

35
export const AIChatLayoutConfig = {
46
[AI_CHAT_CONTAINER_VIEW_ID]: {
57
modules: [AI_CHAT_CONTAINER_VIEW_ID],
68
},
79
};
10+
11+
export const AIMenubarLayoutConfig = {
12+
[SlotLocation.top]: {
13+
modules: [AI_MENUBAR_CONTAINER_VIEW_ID],
14+
},
15+
};
16+
17+
export const AI_MENU_BAR_RIGHT = 'AI_menu_bar_right';
18+
export const AI_MENU_BAR_LEFT = 'AI_menu_bar_left';
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Autowired, Injectable } from '@opensumi/di';
2+
import { ComponentContribution, ComponentRegistry, Disposable, Domain } from '@opensumi/ide-core-browser';
3+
import { IMenuRegistry, IMenubarItem, ISubmenuItem, MenuId } from '@opensumi/ide-core-browser/lib/menu/next';
4+
import { MenubarStore } from '@opensumi/ide-menu-bar/lib/browser/menu-bar.store';
5+
6+
import { AI_MENUBAR_CONTAINER_VIEW_ID } from '../../../common';
7+
8+
import { AIMenuBarView } from './menu-bar.view';
9+
10+
@Domain(ComponentContribution)
11+
export class AIMenuBarContribution extends Disposable implements ComponentContribution {
12+
@Autowired(MenubarStore)
13+
private readonly menubarStore: MenubarStore;
14+
15+
@Autowired(IMenuRegistry)
16+
private readonly menuRegistry: IMenuRegistry;
17+
18+
constructor() {
19+
super();
20+
21+
this.menubarStore.unregisterMenusBarByCompact(MenuId.AIMenuBarTopExtra);
22+
23+
this.addDispose(
24+
this.menubarStore.onDidMenuBarChange((menubarItems: IMenubarItem[]) => {
25+
this.menuRegistry.registerMenuItems(
26+
MenuId.AIMenuBarTopExtra,
27+
menubarItems.map(
28+
(item: IMenubarItem) =>
29+
({
30+
label: item.label,
31+
submenu: item.id,
32+
iconClass: item.iconClass,
33+
group: '1_navigation',
34+
order: 100,
35+
} as ISubmenuItem),
36+
),
37+
);
38+
}),
39+
);
40+
}
41+
42+
registerComponent(registry: ComponentRegistry): void {
43+
registry.register(AI_MENUBAR_CONTAINER_VIEW_ID, {
44+
component: AIMenuBarView,
45+
id: AI_MENUBAR_CONTAINER_VIEW_ID,
46+
});
47+
}
48+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
.menu_bar_view {
2+
display: flex;
3+
background-color: var(--kt-menubar-background);
4+
align-items: center;
5+
width: 100%;
6+
7+
.container {
8+
display: flex;
9+
justify-content: space-between;
10+
width: inherit;
11+
align-items: center;
12+
padding: 0 12px 0 8px;
13+
14+
.left {
15+
display: flex;
16+
align-items: center;
17+
z-index: 2;
18+
19+
.enhance_menu {
20+
padding: 8px;
21+
}
22+
23+
.dividing {
24+
margin-left: 5px;
25+
width: 2px;
26+
height: 12px;
27+
background-color: var(--editorGroup-border);
28+
}
29+
30+
.top_menus_bar {
31+
margin-left: 6px;
32+
display: flex;
33+
align-items: center;
34+
border-radius: 6px;
35+
36+
.ai_enhance_menu {
37+
padding: 8px 6px;
38+
height: 32px;
39+
}
40+
41+
.caret_icon {
42+
font-size: 14px;
43+
margin-left: 4px;
44+
}
45+
46+
.extra_top_icon {
47+
width: 16px;
48+
height: 16px;
49+
background-image: url(./logo.svg);
50+
background-repeat: no-repeat;
51+
background-size: 100%;
52+
53+
&::before {
54+
content: initial;
55+
}
56+
57+
&:hover {
58+
background-color: transparent;
59+
}
60+
}
61+
}
62+
}
63+
64+
.center {
65+
text-align: center;
66+
position: absolute;
67+
left: 0;
68+
right: 0;
69+
z-index: 1;
70+
71+
.run {
72+
.btn {
73+
border-radius: 6px;
74+
padding: 0 13px 0 11px;
75+
border: 0;
76+
}
77+
span {
78+
color: #58ce5f;
79+
font-size: 12px;
80+
}
81+
}
82+
}
83+
84+
.right {
85+
display: flex;
86+
align-items: center;
87+
z-index: 2;
88+
89+
.input {
90+
margin-right: 16px;
91+
width: 240px;
92+
flex-shrink: 0;
93+
94+
.input_wrapper {
95+
height: 32px;
96+
background-color: var(--ai-native-border-color-common);
97+
border-radius: 8px;
98+
border: 1px solid var(--ai-native-input-color);
99+
}
100+
}
101+
102+
.ai_switch {
103+
height: 16px;
104+
width: 16px;
105+
min-width: 16px;
106+
cursor: pointer;
107+
display: flex;
108+
align-items: center;
109+
justify-content: center;
110+
.avatar_icon_large {
111+
width: 16px;
112+
height: 16px;
113+
font-size: 16px !important;
114+
}
115+
}
116+
}
117+
}
118+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import cls from 'classnames';
2+
import * as React from 'react';
3+
4+
import { AINativeConfigService, SlotLocation, SlotRenderer, getIcon, useInjectable } from '@opensumi/ide-core-browser';
5+
import { Icon } from '@opensumi/ide-core-browser/lib/components';
6+
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
7+
import { VIEW_CONTAINERS } from '@opensumi/ide-core-browser/lib/layout/view-id';
8+
import { AbstractContextMenuService, ICtxMenuRenderer, MenuId } from '@opensumi/ide-core-browser/lib/menu/next';
9+
import { CommandService } from '@opensumi/ide-core-common';
10+
import { IMainLayoutService } from '@opensumi/ide-main-layout';
11+
12+
import { AI_MENU_BAR_LEFT, AI_MENU_BAR_RIGHT } from '../layout-config';
13+
14+
import styles from './menu-bar.module.less';
15+
16+
const AIMenuBarRender = () => {
17+
const contextmenuService = useInjectable<AbstractContextMenuService>(AbstractContextMenuService);
18+
const ctxMenuRenderer = useInjectable<ICtxMenuRenderer>(ICtxMenuRenderer);
19+
20+
const iconRef = React.useRef<HTMLDivElement | null>(null);
21+
const [anchor, setAnchor] = React.useState<{ x: number; y: number } | undefined>(undefined);
22+
23+
React.useEffect(() => {
24+
if (iconRef.current) {
25+
const rect = iconRef.current.getBoundingClientRect();
26+
const { x, y, height } = rect;
27+
28+
setAnchor({
29+
x,
30+
y: y + height + 4,
31+
});
32+
}
33+
}, []);
34+
35+
const extraTopMenus = React.useMemo(
36+
() =>
37+
contextmenuService.createMenu({
38+
id: MenuId.AIMenuBarTopExtra,
39+
}),
40+
[contextmenuService],
41+
);
42+
43+
const handleClick = React.useCallback(() => {
44+
if (!anchor) {
45+
return;
46+
}
47+
48+
const menuNodes = extraTopMenus.getMergedMenuNodes();
49+
extraTopMenus.dispose();
50+
51+
ctxMenuRenderer.show({
52+
anchor,
53+
menuNodes,
54+
});
55+
}, [anchor, extraTopMenus]);
56+
57+
return (
58+
<>
59+
<EnhanceIcon
60+
wrapperClassName={styles.ai_enhance_menu}
61+
className={styles.extra_top_icon}
62+
ref={iconRef}
63+
onClick={handleClick}
64+
>
65+
<Icon className={cls(getIcon('down'), styles.caret_icon)} />
66+
</EnhanceIcon>
67+
</>
68+
);
69+
};
70+
71+
export const AIMenuBarView = () => {
72+
const commandService = useInjectable<CommandService>(CommandService);
73+
const mainLayoutService = useInjectable<IMainLayoutService>(IMainLayoutService);
74+
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
75+
const [isVisiablePanel, setIsVisiablePanel] = React.useState<boolean>(false);
76+
77+
React.useEffect(() => {
78+
requestAnimationFrame(() => {
79+
setIsVisiablePanel(isVisiable());
80+
});
81+
}, []);
82+
83+
const handleLeftMenuVisiable = React.useCallback(() => {
84+
commandService.executeCommand('main-layout.left-panel.toggle');
85+
requestAnimationFrame(() => {
86+
setIsVisiablePanel(isVisiable());
87+
});
88+
}, []);
89+
90+
const isVisiable = React.useCallback(() => {
91+
const tabbarService = mainLayoutService.getTabbarService(SlotLocation.left);
92+
return !!tabbarService.currentContainerId;
93+
}, [mainLayoutService]);
94+
95+
return (
96+
<div
97+
id={VIEW_CONTAINERS.MENUBAR}
98+
className={styles.menu_bar_view}
99+
style={{ height: aiNativeConfigService.appConfig.layoutViewSize?.menubarHeight }}
100+
>
101+
<div className={styles.container}>
102+
<div className={styles.left}>
103+
<EnhanceIcon
104+
wrapperClassName={styles.enhance_menu}
105+
icon={isVisiablePanel ? 'left-nav-open' : 'left-nav-close'}
106+
onClick={handleLeftMenuVisiable}
107+
/>
108+
<span className={styles.dividing}></span>
109+
<div className={styles.top_menus_bar}>
110+
<AIMenuBarRender />
111+
</div>
112+
<SlotRenderer slot={AI_MENU_BAR_LEFT} flex={1} overflow={'initial'} />
113+
</div>
114+
<div className={styles.right}>
115+
<SlotRenderer slot={AI_MENU_BAR_RIGHT} flex={1} overflow={'initial'} />
116+
</div>
117+
</div>
118+
</div>
119+
);
120+
};

packages/ai-native/src/browser/layout/tabbar.view.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ export const AILeftTabRenderer = ({
4646
}: {
4747
className: string;
4848
components: ComponentRegistryInfo[];
49-
}) => <DesignLeftTabRenderer className={className} components={components} tabbarView={AiLeftTabbarRenderer} />;
49+
}) => <DesignLeftTabRenderer className={className} components={components} tabbarView={AILeftTabbarRenderer} />;
5050

51-
const AiLeftTabbarRenderer: React.FC = () => {
51+
const AILeftTabbarRenderer: React.FC = () => {
5252
const tabbarService: TabbarService = useInjectable(TabbarServiceFactory)(SlotLocation.right);
5353
const layoutService = useInjectable<IMainLayoutService>(IMainLayoutService);
5454

packages/ai-native/src/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const IAINativeService = Symbol('IAINativeService');
1212

1313
export const AIInlineChatContentWidget = 'AI_Inline_Chat_Content_Widget';
1414
export const AI_CHAT_CONTAINER_VIEW_ID = 'AI_Chat';
15+
export const AI_MENUBAR_CONTAINER_VIEW_ID = 'AI_menubar';
1516

1617
export const AI_SLASH = '/';
1718

0 commit comments

Comments
 (0)