diff --git a/features/agent.feature b/features/agent.feature index e4bb64d4..4e6d1be9 100644 --- a/features/agent.feature +++ b/features/agent.feature @@ -148,8 +148,10 @@ Feature: Agent Workflow - Tool Usage and Multi-Round Conversation # Click attachment button to open autocomplete When I click on a "attach button" element with selector "[data-testid='agent-attach-button']" # Autocomplete should open showing image option + tiddler options - And I should see a "attachment autocomplete input" element with selector "[data-testid='attachment-autocomplete-input']" - And I should see a "attachment listbox" element with selector "[data-testid='attachment-listbox']" + And I should see "attachment autocomplete input and attachment listbox" elements with selectors: + | element description | selector | + | attachment autocomplete input | [data-testid='attachment-autocomplete-input'] | + | attachment listbox | [data-testid='attachment-listbox'] | # Click on our test tiddler option When I click on a "test tiddler option" element with selector "[data-testid='attachment-option-tiddler-TestAttachmentTiddler']" # Verify the chip is displayed @@ -159,9 +161,6 @@ Feature: Agent Workflow - Tool Usage and Multi-Round Conversation When I type "请分析这个条目" in "chat input" element with selector "[data-testid='agent-message-input']" And I press "Enter" key # Verify the mock server received the rendered content (wikitext converted to plain text) - Then the last AI request user message should contain "WikiTestContentMarker123" - And the last AI request user message should contain "Wiki Entry from" - And the last AI request user message should contain "TestAttachmentTiddler" + Then the last AI request user message should contain "WikiTestHeader\n\nThis is a test with WikiTestContentMarker123" # Verify wikitext was converted to plain text (!! becomes "Header", not raw !!) - And the last AI request user message should contain "WikiTestHeader" And the last AI request user message should not contain "!!" diff --git a/features/stepDefinitions/agent.ts b/features/stepDefinitions/agent.ts index 7d38a6b4..4fc5e511 100644 --- a/features/stepDefinitions/agent.ts +++ b/features/stepDefinitions/agent.ts @@ -284,8 +284,12 @@ Then('the last AI request user message should contain {string}', async function( } const lastUserMessage = userMessages[userMessages.length - 1]; - if (!lastUserMessage.content || !lastUserMessage.content.includes(expectedText)) { - throw new Error(`Expected user message to contain "${expectedText}", but got: "${lastUserMessage.content}"`); + const content = lastUserMessage.content ?? ''; + + const normalizedExpectedText = expectedText.replaceAll('\\n', '\n'); + const contentHasExpectedText = content.includes(expectedText) || content.includes(normalizedExpectedText); + if (!contentHasExpectedText) { + throw new Error(`Expected user message to contain "${expectedText}", but got: "${content}"`); } }); diff --git a/localization/locales/en/agent.json b/localization/locales/en/agent.json index f51a3f64..d140b772 100644 --- a/localization/locales/en/agent.json +++ b/localization/locales/en/agent.json @@ -16,6 +16,13 @@ "Title": "API Debug Logs" }, "Agent": { + "Attachment": { + "AddAttachment": "Add attachment", + "AddImage": "📷 Add Image", + "NoOptions": "No options available", + "SearchPlaceholder": "Search...", + "SelectAttachment": "Select attachment" + }, "EditTitle": "Edit agent name", "InvalidTabType": "Invalid tab type. A chat tab is required.", "LoadingChat": "Loading conversation...", @@ -96,6 +103,10 @@ "SetupAgentDescription": "Configure your new agent by choosing a name and template", "Title": "Create New Agent" }, + "WikiEmbed": { + "Error": "Failed to embed wiki", + "Loading": "Loading wiki..." + }, "EditAgent": { "AgentDescription": "Agent Description", "AgentDescriptionHelper": "Describe the functionality and purpose of your intelligent agent.", diff --git a/localization/locales/fr/agent.json b/localization/locales/fr/agent.json index 736fe7c1..42a74a28 100644 --- a/localization/locales/fr/agent.json +++ b/localization/locales/fr/agent.json @@ -16,6 +16,13 @@ "Title": "Journal de débogage API" }, "Agent": { + "Attachment": { + "AddAttachment": "Ajouter une pièce jointe", + "AddImage": "📷 Ajouter une image", + "NoOptions": "Aucune option disponible", + "SearchPlaceholder": "Rechercher...", + "SelectAttachment": "Sélectionner une pièce jointe" + }, "EditTitle": "Modifier le nom de l'agent intelligent", "InvalidTabType": "Type d'onglet non valide. Un onglet de chat est requis.", "LoadingChat": "Chargement de la conversation en cours...", @@ -96,6 +103,10 @@ "SetupAgentDescription": "Nommez votre agent et choisissez un modèle comme point de départ", "Title": "Créer un nouvel agent intelligent" }, + "WikiEmbed": { + "Error": "Échec de l’intégration du wiki", + "Loading": "Chargement du wiki..." + }, "EditAgent": { "AgentDescription": "Description de l'agent intelligent", "AgentDescriptionHelper": "Décrivez les fonctionnalités et l'utilité de votre agent intelligent", diff --git a/localization/locales/ja/agent.json b/localization/locales/ja/agent.json index cc8f4db9..95fbc861 100644 --- a/localization/locales/ja/agent.json +++ b/localization/locales/ja/agent.json @@ -16,6 +16,13 @@ "Title": "API デバッグログ" }, "Agent": { + "Attachment": { + "AddAttachment": "添付を追加", + "AddImage": "📷 画像を追加", + "NoOptions": "利用可能な項目がありません", + "SearchPlaceholder": "検索...", + "SelectAttachment": "添付を選択" + }, "EditTitle": "編集エージェント名", "InvalidTabType": "無効なタブタイプです。チャットタブが必要です。", "LoadingChat": "会話を読み込んでいます...", @@ -96,6 +103,10 @@ "SetupAgentDescription": "あなたのエージェントに名前を付け、テンプレートを出発点として選択してください", "Title": "新しいインテリジェントエージェントを作成する" }, + "WikiEmbed": { + "Error": "Wiki の埋め込みに失敗しました", + "Loading": "Wiki を読み込んでいます..." + }, "EditAgent": { "AgentDescription": "エージェントの説明", "AgentDescriptionHelper": "あなたのエージェントの機能と用途を説明してください", diff --git a/localization/locales/ru/agent.json b/localization/locales/ru/agent.json index 42fede39..566a58f6 100644 --- a/localization/locales/ru/agent.json +++ b/localization/locales/ru/agent.json @@ -16,6 +16,13 @@ "Title": "Журнал отладки API" }, "Agent": { + "Attachment": { + "AddAttachment": "Добавить вложение", + "AddImage": "📷 Добавить изображение", + "NoOptions": "Нет доступных вариантов", + "SearchPlaceholder": "Поиск...", + "SelectAttachment": "Выбрать вложение" + }, "EditTitle": "редактировать имя интеллектуального агента", "InvalidTabType": "Неверный тип вкладки. Требуется вкладка чата.", "LoadingChat": "Загрузка диалога...", @@ -96,6 +103,10 @@ "SetupAgentDescription": "Назовите своего агента и выберите шаблон в качестве отправной точки", "Title": "Создать нового интеллектуального агента" }, + "WikiEmbed": { + "Error": "Не удалось встроить Wiki", + "Loading": "Загрузка Wiki..." + }, "EditAgent": { "AgentDescription": "описание агента", "AgentDescriptionHelper": "Опишите функции и назначение вашего агента.", diff --git a/localization/locales/zh-Hans/agent.json b/localization/locales/zh-Hans/agent.json index 9af173f3..6ae9b05a 100644 --- a/localization/locales/zh-Hans/agent.json +++ b/localization/locales/zh-Hans/agent.json @@ -16,6 +16,13 @@ "Title": "外部接口调试日志" }, "Agent": { + "Attachment": { + "AddAttachment": "添加附件", + "AddImage": "📷 添加图片", + "NoOptions": "暂无可选项", + "SearchPlaceholder": "搜索...", + "SelectAttachment": "选择附件" + }, "EditTitle": "编辑智能体名字", "InvalidTabType": "无效的标签页类型。需要聊天标签页。", "LoadingChat": "正在加载对话...", @@ -96,6 +103,10 @@ "SetupAgentDescription": "为您的智能体命名并选择一个模板作为起点", "Title": "创建新智能体" }, + "WikiEmbed": { + "Error": "嵌入 Wiki 失败", + "Loading": "正在加载 Wiki..." + }, "EditAgent": { "AgentDescription": "智能体描述", "AgentDescriptionHelper": "描述您的智能体的功能和用途", diff --git a/localization/locales/zh-Hant/agent.json b/localization/locales/zh-Hant/agent.json index 36952ea6..485ff0b8 100644 --- a/localization/locales/zh-Hant/agent.json +++ b/localization/locales/zh-Hant/agent.json @@ -16,6 +16,13 @@ "Title": "外部介面除錯日誌" }, "Agent": { + "Attachment": { + "AddAttachment": "新增附件", + "AddImage": "📷 新增圖片", + "NoOptions": "沒有可用選項", + "SearchPlaceholder": "搜尋...", + "SelectAttachment": "選擇附件" + }, "EditTitle": "編輯智慧體名字", "InvalidTabType": "無效的標籤頁類型。需要聊天標籤頁。", "LoadingChat": "正在載入對話...", @@ -95,6 +102,10 @@ "SetupAgentDescription": "為您的智慧體命名並選擇一個模板作為起點", "Title": "創建新智慧體" }, + "WikiEmbed": { + "Error": "嵌入 Wiki 失敗", + "Loading": "正在載入 Wiki..." + }, "EditAgent": { "AgentDescription": "智能體描述", "AgentDescriptionHelper": "描述您的智慧體的功能和用途", diff --git a/src/pages/Agent/TabContent/TabTypes/WikiEmbedTabContent.tsx b/src/pages/Agent/TabContent/TabTypes/WikiEmbedTabContent.tsx index 83f627d2..7f781370 100644 --- a/src/pages/Agent/TabContent/TabTypes/WikiEmbedTabContent.tsx +++ b/src/pages/Agent/TabContent/TabTypes/WikiEmbedTabContent.tsx @@ -165,7 +165,7 @@ export const WikiEmbedTabContent: React.FC = ({ tab, i return ( - {t('WikiEmbed.Error', 'Failed to embed wiki')} + {t('WikiEmbed.Error')} {error} @@ -178,7 +178,7 @@ export const WikiEmbedTabContent: React.FC = ({ tab, i - {t('WikiEmbed.Loading', 'Loading wiki...')} + {t('WikiEmbed.Loading')} )} diff --git a/src/pages/ChatTabContent/components/InputContainer.tsx b/src/pages/ChatTabContent/components/InputContainer.tsx index 5e1c0469..154f215e 100644 --- a/src/pages/ChatTabContent/components/InputContainer.tsx +++ b/src/pages/ChatTabContent/components/InputContainer.tsx @@ -48,6 +48,9 @@ interface InputContainerProps { onWikiTiddlerSelect?: (tiddler: WikiTiddlerAttachment) => void; onRemoveWikiTiddler?: (index: number) => void; } +const attachmentListboxSlotProps: React.HTMLAttributes & { 'data-testid': string } = { + 'data-testid': 'attachment-listbox', +}; /** * Input container component for message entry @@ -146,7 +149,7 @@ export const InputContainer: React.FC = ({ try { // Build options: first is "Add Image", then wiki tiddlers from all non-hibernated workspaces const options: Array<{ type: 'image' | 'tiddler'; title: string; workspaceName?: string; testId?: string }> = [ - { type: 'image', title: t('Agent.Attachment.AddImage', '📷 Add Image'), workspaceName: '', testId: 'AddImage' }, + { type: 'image', title: t('Agent.Attachment.AddImage'), workspaceName: '', testId: 'AddImage' }, ]; // Get all workspaces @@ -294,7 +297,7 @@ export const InputContainer: React.FC = ({ disabled={disabled || isStreaming} color={(selectedFile || selectedWikiTiddlers.length > 0) ? 'primary' : 'default'} data-testid='agent-attach-button' - title={t('Agent.Attachment.AddAttachment', 'Add attachment')} + title={t('Agent.Attachment.AddAttachment')} > @@ -346,15 +349,13 @@ export const InputContainer: React.FC = ({ getOptionLabel={(option) => option.title} onChange={handleSelectAttachment} slotProps={{ - listbox: { - 'data-testid': 'attachment-listbox', - }, + listbox: attachmentListboxSlotProps, }} renderInput={(parameters) => ( @@ -367,7 +368,7 @@ export const InputContainer: React.FC = ({ ); }} - noOptionsText={t('Agent.Attachment.NoOptions', 'No options available')} + noOptionsText={t('Agent.Attachment.NoOptions')} /> diff --git a/src/pages/ChatTabContent/index.tsx b/src/pages/ChatTabContent/index.tsx index 73e8a7d1..003b727b 100644 --- a/src/pages/ChatTabContent/index.tsx +++ b/src/pages/ChatTabContent/index.tsx @@ -50,7 +50,7 @@ export const ChatTabContent: React.FC = ({ tab, isSplitView return ( - {t('Agent.InvalidTabType', 'Invalid tab type. Expected chat tab.')} + {t('Agent.InvalidTabType')} ); diff --git a/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx b/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx index edea9202..e3d63c9e 100644 --- a/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx +++ b/src/pages/Main/WorkspaceIconAndSelector/SortableWorkspaceSelectorButton.tsx @@ -109,7 +109,7 @@ export function SortableWorkspaceSelectorButton({ index, workspace, showSidebarT editFlags: { canCopy: false }, }); }, - [t, workspace], + [t, workspace, id], ); return (
diff --git a/src/services/menu/createTalkWithAIMenuItems.ts b/src/services/menu/createTalkWithAIMenuItems.ts new file mode 100644 index 00000000..09a6381f --- /dev/null +++ b/src/services/menu/createTalkWithAIMenuItems.ts @@ -0,0 +1,82 @@ +import { IAskAIWithSelectionData, WindowChannel } from '@/constants/channels'; +import type { AgentDefinition, IAgentDefinitionService } from '@services/agentDefinition/interface'; +import type { IWindowService } from '@services/windows/interface'; +import { WindowNames } from '@services/windows/WindowProperties'; +import type { MenuItemConstructorOptions } from 'electron'; +import type { TFunction } from 'i18next'; + +interface ICreateTalkWithAIMenuItemsOptions { + /** Agent definition service to get available agents */ + agentDefinitionService: Pick; + /** Selected text to send to AI (empty string if none) */ + selectionText: string; + /** Translation function */ + t: TFunction; + /** Wiki URL for context */ + wikiUrl?: string; + /** Window service to get main window */ + windowService: Pick; + /** Workspace ID for context */ + workspaceId?: string; +} + +/** + * Create "Talk with AI" menu items with default agent and other agents submenu + * This is shared between context menu (page right-click) and workspace menu (workspace icon right-click) + */ +export async function createTalkWithAIMenuItems( + options: ICreateTalkWithAIMenuItemsOptions, +): Promise { + const { agentDefinitionService, selectionText, t, wikiUrl, windowService, workspaceId } = options; + + // Get all agent definitions + const defaultAgentDefinition = await agentDefinitionService.getAgentDef(); // No parameter = default agent + const allAgentDefinitions = await agentDefinitionService.getAgentDefs(); + const otherAgentDefinitions = allAgentDefinitions.filter((definition: AgentDefinition) => definition.id !== defaultAgentDefinition?.id); + + const menuItems: MenuItemConstructorOptions[] = []; + + // Add menu item for default agent + menuItems.push({ + id: 'talk-with-ai', + label: t('ContextMenu.TalkWithAI'), + click: async () => { + const data: IAskAIWithSelectionData = { + selectionText, + wikiUrl, + workspaceId, + agentDefId: undefined, // Use default agent + }; + // Send to main window + const mainWindow = windowService.get(WindowNames.main); + if (mainWindow !== undefined) { + mainWindow.webContents.send(WindowChannel.askAIWithSelection, data); + } + }, + }); + + // Add submenu for other agents if there are any + if (otherAgentDefinitions.length > 0) { + menuItems.push({ + id: 'talk-with-ai-more', + label: t('ContextMenu.TalkWithAIMore'), + submenu: otherAgentDefinitions.map((agentDefinition: AgentDefinition) => ({ + label: agentDefinition.name, + click: async () => { + const data: IAskAIWithSelectionData = { + selectionText, + wikiUrl, + workspaceId, + agentDefId: agentDefinition.id, + }; + const mainWindow = windowService.get(WindowNames.main); + if (mainWindow !== undefined) { + mainWindow.webContents.send(WindowChannel.askAIWithSelection, data); + } + }, + })), + }); + } + + return menuItems; +} diff --git a/src/services/menu/index.ts b/src/services/menu/index.ts index f651fa70..de9f1709 100644 --- a/src/services/menu/index.ts +++ b/src/services/menu/index.ts @@ -1,6 +1,5 @@ -import { IAskAIWithSelectionData, WindowChannel } from '@/constants/channels'; import { getWorkspaceIdFromUrl } from '@/constants/urls'; -import type { AgentDefinition, IAgentDefinitionService } from '@services/agentDefinition/interface'; +import type { IAgentDefinitionService } from '@services/agentDefinition/interface'; import type { IAuthenticationService } from '@services/auth/interface'; import { container } from '@services/container'; import type { IContextService } from '@services/context/interface'; @@ -149,6 +148,7 @@ export class MenuService implements IMenuService { // Add workspace-specific menu items if workspace exists and is a wiki if (workspace !== undefined && isWikiWorkspace(workspace)) { const services = { + agentDefinition: container.get(serviceIdentifier.AgentDefinition), auth: container.get(serviceIdentifier.Authentication), context: container.get(serviceIdentifier.Context), externalAPI: this.externalAPIService, @@ -318,6 +318,7 @@ export class MenuService implements IMenuService { const menu = contextMenuBuilder.buildMenuForElement(info); const workspaces = await workspaceService.getWorkspacesAsList(); const services = { + agentDefinition: container.get(serviceIdentifier.AgentDefinition), auth: authService, context: contextService, externalAPI: this.externalAPIService, @@ -336,64 +337,6 @@ export class MenuService implements IMenuService { // workspace menus (template items are added at the end via insert(0) in reverse order) menu.append(new MenuItem({ type: 'separator' })); - // Add "Talk with AI" menu items when there's selected text - if (info.selectionText && info.selectionText.trim().length > 0) { - const wikiUrl = webContents.getURL(); - const workspaceId = getWorkspaceIdFromUrl(wikiUrl); - - // Get all agent definitions - const agentDefinitionService = container.get(serviceIdentifier.AgentDefinition); - const defaultAgentDefinition = await agentDefinitionService.getAgentDef(); // No parameter = default agent - const allAgentDefinitions = await agentDefinitionService.getAgentDefs(); - const otherAgentDefinitions = allAgentDefinitions.filter((definition: AgentDefinition) => definition.id !== defaultAgentDefinition?.id); - - // Add menu item for default agent - menu.append( - new MenuItem({ - label: i18n.t('ContextMenu.TalkWithAI'), - click: async () => { - const data: IAskAIWithSelectionData = { - selectionText: info.selectionText!, - wikiUrl, - workspaceId: workspaceId ?? undefined, - agentDefId: undefined, // Use default agent - }; - // Only send to main window to avoid duplicate processing - const mainWindow = windowService.get(WindowNames.main); - if (mainWindow !== undefined) { - mainWindow.webContents.send(WindowChannel.askAIWithSelection, data); - } - }, - }), - ); - - // Add submenu for other agents if there are any - if (otherAgentDefinitions.length > 0) { - menu.append( - new MenuItem({ - label: i18n.t('ContextMenu.TalkWithAIMore'), - submenu: otherAgentDefinitions.map((agentDefinition: AgentDefinition) => ({ - label: agentDefinition.name, - click: async () => { - const data: IAskAIWithSelectionData = { - selectionText: info.selectionText!, - wikiUrl, - workspaceId: workspaceId ?? undefined, - agentDefId: agentDefinition.id, - }; - const mainWindow = windowService.get(WindowNames.main); - if (mainWindow !== undefined) { - mainWindow.webContents.send(WindowChannel.askAIWithSelection, data); - } - }, - })), - }), - ); - } - - menu.append(new MenuItem({ type: 'separator' })); - } - // Note: Simplified menu and "Current Workspace" are now provided by the frontend template // (from SortableWorkspaceSelectorButton or content view), so we don't add them here menu.append( diff --git a/src/services/workspaces/getWorkspaceMenuTemplate.ts b/src/services/workspaces/getWorkspaceMenuTemplate.ts index a12b5168..fa7ad3dd 100644 --- a/src/services/workspaces/getWorkspaceMenuTemplate.ts +++ b/src/services/workspaces/getWorkspaceMenuTemplate.ts @@ -1,10 +1,12 @@ import { WikiChannel } from '@/constants/channels'; import { getDefaultHTTPServerIP } from '@/constants/urls'; +import type { IAgentDefinitionService } from '@services/agentDefinition/interface'; import type { IAuthenticationService } from '@services/auth/interface'; import type { IContextService } from '@services/context/interface'; import type { IExternalAPIService } from '@services/externalAPI/interface'; import type { IGitService } from '@services/git/interface'; import { createBackupMenuItems, createSyncMenuItems } from '@services/git/menuItems'; +import { createTalkWithAIMenuItems } from '@services/menu/createTalkWithAIMenuItems'; import type { INativeService } from '@services/native/interface'; import type { IPreferenceService } from '@services/preferences/interface'; import type { ISyncService } from '@services/sync/interface'; @@ -22,6 +24,7 @@ import type { IWorkspace, IWorkspaceService } from './interface'; import { isWikiWorkspace } from './interface'; interface IWorkspaceMenuRequiredServices { + agentDefinition: Pick; auth: Pick; context: Pick; externalAPI: Pick; @@ -32,7 +35,7 @@ interface IWorkspaceMenuRequiredServices { view: Pick; wiki: Pick; wikiGitWorkspace: Pick; - window: Pick; + window: Pick; workspace: Pick; workspaceView: Pick< IWorkspaceViewService, @@ -62,6 +65,19 @@ export async function getSimplifiedWorkspaceMenuTemplate( const { id, storageService, isSubWiki } = workspace; const template: MenuItemConstructorOptions[] = []; + const lastUrl = await service.view.getViewCurrentUrl(id, WindowNames.main); + const talkWithAIMenuItems = await createTalkWithAIMenuItems({ + agentDefinitionService: service.agentDefinition, + selectionText: '', + t, + wikiUrl: lastUrl, + windowService: service.window, + workspaceId: id, + }); + + template.push(...talkWithAIMenuItems); + template.push({ type: 'separator' }); + // Add "Current Workspace" submenu with full menu const fullMenuTemplate = await getWorkspaceMenuTemplate(workspace, t, service); if (fullMenuTemplate.length > 0) {