From 774c63ef9eef207fd9aad8fb581968c0dc0a829f Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 2 Apr 2026 00:23:00 +0200 Subject: [PATCH] new menus first try. see #680 --- _locales/en/messages.json | 20 ++ claude-spec/02-prompts.md | 21 +- js/mzta-menus.js | 60 +++++- js/mzta-prompts.js | 38 +++- js/mzta-utils.js | 18 ++ mzta-background.js | 114 ++--------- pages/customprompts/mzta-custom-prompts.html | 9 + pages/customprompts/mzta-custom-prompts.js | 42 +++- popup/mzta-popup.js | 204 ++----------------- 9 files changed, 239 insertions(+), 287 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index dfbd3656..36078e43 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -203,6 +203,26 @@ "message": "Composing an email", "description": "" }, + "show_in": { + "message": "Show in", + "description": "" + }, + "show_in_popup": { + "message": "Popup only", + "description": "" + }, + "show_in_context": { + "message": "Context menu only", + "description": "" + }, + "show_in_both": { + "message": "Both", + "description": "" + }, + "show_in_none": { + "message": "None", + "description": "" + }, "customPrompts_close_button": { "message": "Close button", "description": "" diff --git a/claude-spec/02-prompts.md b/claude-spec/02-prompts.md index 60b1624f..efce3539 100644 --- a/claude-spec/02-prompts.md +++ b/claude-spec/02-prompts.md @@ -28,9 +28,10 @@ Prompts are the core user-facing feature of ThunderAI. Each prompt defines an AI | Property | Type | Description | |----------|------|-------------| -| `enabled` | number | `0` = hidden, `1` = shown in popup | -| `position_display` | number | Sort order in reading view | -| `position_compose` | number | Sort order in compose view | +| `enabled` | number | `0` = hidden, `1` = shown in menus | +| `position_display` | number | Sort order in reading view (used when alphabetical ordering is off) | +| `position_compose` | number | Sort order in compose view (used when alphabetical ordering is off) | +| `show_in` | string | `"popup"` = popup only, `"context"` = context menu only, `"both"` = both, `"none"` = hidden from all menus. Default: `"popup"` for default/custom prompts, `"both"` for special prompts | ### Per-Prompt API Override Properties @@ -59,6 +60,20 @@ Some prompts trigger additional Thunderbird actions beyond just sending text to These special prompts can have their own dedicated API integration settings (configured in the Options page). The list of these special prompts is in `options/mzta-options-default.js` as `special_prompts_with_integration`. +## Menu System + +### Popup Menu +- Displays prompts filtered by `show_in` (`"popup"` or `"both"`) and by tab context (`type` property) +- Ordering: user can choose between alphabetical or position-based (using `position_display`/`position_compose`) via the `dynamic_menu_order_alphabet` preference +- Special prompts retain their colored background (CSS class `special_prompt`) in the popup based on `is_special == "1"` + +### Context Menu +- Dynamically built from all prompts with `show_in` set to `"context"` or `"both"`, filtered to reading types only (`type` 0 or 1) +- Appears as a "ThunderAI" submenu in the `message_list` context +- Special prompts (add_tags, spamfilter, summarize, translate) route through `processEmails()` for batch processing; regular prompts execute via `menus.executeMenuAction()` +- Icons: special prompts use dedicated icons (defined in `contextMenuIconsPath`); all other prompts use the addon icon (`images/icon-32.png`) +- Add Tags in context menu assigns tags automatically (`addTagsAuto: true`), while in the popup it shows the interactive tag selection form + ### Summarize: Dual-Mode Prompt System The summarize feature uses two distinct prompt pathways: diff --git a/js/mzta-menus.js b/js/mzta-menus.js index 1c86f99e..1cea4993 100644 --- a/js/mzta-menus.js +++ b/js/mzta-menus.js @@ -35,6 +35,7 @@ import { cleanupNewlines, checkIfTagLabelExists, getConnectionType, + getContextMenuIcon, } from './mzta-utils.js' import { taPromptUtils } from './mzta-utils-prompt.js'; import { taLogger } from './mzta-logger.js'; @@ -74,8 +75,7 @@ export class mzta_Menus { this.rootMenu = []; this.shortcutMenu = []; this.menu_listeners = {}; - this.allPrompts = await getPrompts(true,also_special); - this.allPrompts.sort((a, b) => a.name.localeCompare(b.name)); + this.allPrompts = await getPrompts(true,also_special); this.allPrompts.forEach((prompt) => { this.addAction(prompt) }); @@ -480,15 +480,69 @@ export class mzta_Menus { } addShortcutMenu(prompt) { - let curr_menu_entry = {id: prompt.id, label: i18nConditionalGet(prompt.name), type: prompt.type}; + let curr_menu_entry = { + id: prompt.id, + label: i18nConditionalGet(prompt.name), + type: prompt.type, + show_in: prompt.show_in || "popup", + is_special: prompt.is_special, + position_display: prompt.position_display, + position_compose: prompt.position_compose, + }; this.shortcutMenu.push(curr_menu_entry); } + async loadContextMenus() { + await browser.menus.removeAll(); + const contextPrompts = this.allPrompts.filter(p => { + const showIn = p.show_in || "popup"; + // Only show prompts that should appear in context menu and are for reading context (type 0 or 1) + return (showIn === "context" || showIn === "both") && (String(p.type) === "0" || String(p.type) === "1"); + }); + + if (contextPrompts.length === 0) { + this.logger.log("No prompts for context menu"); + return; + } + + // Create parent menu + await new Promise(resolve => + browser.menus.create({ + id: 'mzta-context-parent', + title: 'ThunderAI', + contexts: ["message_list"], + }, resolve) + ); + + // Sort alphabetically for context menu + contextPrompts.sort((a, b) => { + const nameA = i18nConditionalGet(a.name); + const nameB = i18nConditionalGet(b.name); + return nameA.localeCompare(nameB); + }); + + // Create child menu items + for (const prompt of contextPrompts) { + const title = i18nConditionalGet(prompt.name); + await new Promise(resolve => + browser.menus.create({ + id: 'mzta-ctx-' + prompt.id, + title: title, + contexts: ["message_list"], + parentId: 'mzta-context-parent', + icons: getContextMenuIcon(prompt.id), + }, resolve) + ); + } + this.logger.log("Context menus loaded: " + contextPrompts.length + " items"); + } + async loadMenus(also_special = []) { await this.initialize(also_special); await this.addMenu(this.rootMenu); this.addClickListener(); this.loadShortcutMenu(); + await this.loadContextMenus(); this.logger.log("Menus loaded"); } diff --git a/js/mzta-prompts.js b/js/mzta-prompts.js index 57b14678..089f7184 100644 --- a/js/mzta-prompts.js +++ b/js/mzta-prompts.js @@ -62,6 +62,12 @@ summaryTabId (set by _openSummaryWebchat in mzta-background.js): The tab ID of the message display tab to update with the saved summary. + Show in menu (show_in attribute): + "popup": Show only in the popup menu + "context": Show only in the context menu + "both": Show in both popup and context menus + "none": Do not show in any menu + ================ USER PROPERTIES Enabled (enabled attribute): 0: Disabled @@ -108,6 +114,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_reply_advanced', @@ -126,6 +133,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_reply_custom_command', @@ -144,6 +152,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_rewrite_polite', @@ -162,6 +171,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_rewrite_formal', @@ -180,6 +190,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_classify', @@ -198,6 +209,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_summarize_this', @@ -216,6 +228,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_proofread_this', @@ -234,6 +247,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, { id: 'prompt_this', @@ -252,6 +266,7 @@ const defaultPrompts = [ api_type: '', is_default: "1", is_special: "0", + show_in: "popup", }, ]; @@ -270,6 +285,7 @@ const specialPrompts = [ api_type: '', is_default: "1", is_special: "1", + show_in: "both", }, { id: 'prompt_get_calendar_event', @@ -285,6 +301,7 @@ const specialPrompts = [ api_type: '', is_default: "1", is_special: "1", + show_in: "both", }, { id: 'prompt_get_calendar_event_from_clipboard', @@ -300,6 +317,7 @@ const specialPrompts = [ api_type: '', is_default: "1", is_special: "1", + show_in: "both", }, { id: 'prompt_get_task', @@ -315,6 +333,7 @@ const specialPrompts = [ api_type: '', is_default: "1", is_special: "1", + show_in: "both", }, { id: 'prompt_spamfilter', @@ -330,6 +349,7 @@ const specialPrompts = [ api_type: '', is_default: "1", is_special: "1", + show_in: "both", }, { id: 'prompt_summarize', @@ -346,6 +366,7 @@ const specialPrompts = [ api_model: '', is_default: "1", is_special: "1", + show_in: "both", }, { id: 'prompt_summarize_email_template', @@ -362,6 +383,7 @@ const specialPrompts = [ api_model: '', is_default: "1", is_special: "1", + show_in: "both", }, { id: 'prompt_summarize_email_separator', @@ -378,6 +400,7 @@ const specialPrompts = [ api_model: '', is_default: "1", is_special: "1", + show_in: "both", }, { id: 'prompt_translate_this', @@ -394,6 +417,7 @@ const specialPrompts = [ api_model: '', is_default: "1", is_special: "1", + show_in: "both", } ]; @@ -464,7 +488,7 @@ export function preparePromptsForExport(prompts, include_api_settings = false){ } if(prompt.is_default == 1){ - let allowedKeys = ['id', 'enabled', 'position_compose', 'position_display', 'need_custom_text']; + let allowedKeys = ['id', 'enabled', 'position_compose', 'position_display', 'need_custom_text', 'show_in']; if(include_api_settings){ allowedKeys.push('api_type'); for (const [integration, options] of Object.entries(integration_options_config)) { @@ -534,6 +558,7 @@ async function getDefaultPrompts_withProps() { prompt.chatgpt_web_project = prefs._default_prompts_properties[prompt.id].chatgpt_web_project; prompt.chatgpt_web_custom_gpt = (prefs._default_prompts_properties[prompt.id]?.chatgpt_web_custom_gpt || '').trim(); prompt.api_type = (prefs._default_prompts_properties[prompt.id]?.api_type || '').trim(); + prompt.show_in = prefs._default_prompts_properties[prompt.id]?.show_in || prompt.show_in; }else{ prompt.position_display = pos; prompt.position_compose = pos; @@ -569,6 +594,9 @@ async function getCustomPrompts() { if(prompt.api_type === undefined){ prompt.api_type = ""; } + if(prompt.show_in === undefined){ + prompt.show_in = "popup"; + } }); return prefs._custom_prompt; } @@ -586,6 +614,7 @@ export async function setDefaultPromptsProperties(prompts) { chatgpt_web_project: (prompt.chatgpt_web_project === undefined || prompt.chatgpt_web_project === "undefined") ? "" : prompt.chatgpt_web_project, chatgpt_web_custom_gpt: (prompt.chatgpt_web_custom_gpt === undefined || prompt.chatgpt_web_custom_gpt === "undefined") ? "" : prompt.chatgpt_web_custom_gpt, api_type: (prompt.api_type === undefined || prompt.api_type === "undefined") ? "" : prompt.api_type, + show_in: (prompt.show_in === undefined || prompt.show_in === "undefined") ? "popup" : prompt.show_in, }; }); //console.log('>>>>>>>>>>>>>> default_prompts_properties: ' + JSON.stringify(default_prompts_properties)); @@ -616,6 +645,13 @@ export async function getSpecialPrompts(){ updatedPrompts.push(newPrompt); } }); + // Migrate: add show_in if missing from saved special prompts + updatedPrompts.forEach((prompt) => { + if (prompt.show_in === undefined) { + prompt.show_in = "both"; + } + }); + // console.log(">>>>>>>>>>>>> getSpecialPrompts updatedPrompts: " + JSON.stringify(updatedPrompts)); if (updatedPrompts.length !== prefs._special_prompts.length) { await browser.storage.local.set({ _special_prompts: updatedPrompts }); diff --git a/js/mzta-utils.js b/js/mzta-utils.js index ce5dc9d4..9859f7f5 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -35,6 +35,24 @@ export const contextMenuIconsPath = { [contextMenuID_Translate]: 'moz-extension:images/menu_translate.png', }; +// Map from special prompt IDs to context menu IDs +export const specialPromptToContextMenuID = { + 'prompt_add_tags': contextMenuID_AddTags, + 'prompt_spamfilter': contextMenuID_Spamfilter, + 'prompt_summarize': contextMenuID_Summarize, + 'prompt_translate_this': contextMenuID_Translate, +}; + +const defaultContextMenuIcon = 'moz-extension:images/icon-32.png'; + +export function getContextMenuIcon(promptId) { + const contextMenuId = specialPromptToContextMenuID[promptId]; + if (contextMenuId && contextMenuIconsPath[contextMenuId]) { + return contextMenuIconsPath[contextMenuId]; + } + return defaultContextMenuIcon; +} + export function getLanguageDisplayName(languageCode) { const languageDisplay = new Intl.DisplayNames([languageCode], {type: 'language'}); let lang_string = languageDisplay.of(languageCode); diff --git a/mzta-background.js b/mzta-background.js index 7a4b0a27..86555154 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -42,18 +42,12 @@ import { getMessages, getMailBody, extractJsonObject, - contextMenuID_AddTags, - contextMenuID_Spamfilter, - contextMenuID_Summarize, - contextMenuID_Translate, - contextMenuIconsPath, sanitizeChatGPTModelData, sanitizeChatGPTWebCustomData, stripHtmlKeepLines, htmlBodyToPlainText, convertNewlinesToParagraphs, getConnectionType, - checkAPIIntegration, hasSpecificIntegration, } from './js/mzta-utils.js'; import { taPromptUtils } from './js/mzta-utils-prompt.js'; @@ -1446,9 +1440,7 @@ function setupStorageChangeListener() { menus.reload(special_prompts_ids); } - reload_pref_init().then(() => { - addContextMenuItems(); - }); + reload_pref_init(); } }); } @@ -1480,94 +1472,30 @@ setupPermissionsRemovedListener(); const menus = new mzta_Menus(openChatGPT, prefs_init.do_debug); menus.loadMenus(special_prompts_ids); -// Context Menus -function addContextMenu(menu_id) { - browser.menus.remove(menu_id); - browser.menus.create({ - id: menu_id, - title: browser.i18n.getMessage("context_menu_" + menu_id), - contexts: ["message_list"], - icons: contextMenuIconsPath[menu_id], - }); - taLog.log("Context menu added: " + menu_id); - // console.log(">>>>>>> contextMenuIconsPath[menu_id]: " + contextMenuIconsPath[menu_id]); -} +// Context menu click handling +// Context menus are now created dynamically by mzta_Menus.loadContextMenus() +// based on each prompt's show_in property. The menu item IDs use the format 'mzta-ctx-'. +// Special prompts (add_tags, spamfilter, summarize, translate) are routed to processEmails() +// for batch processing. Regular prompts are executed via menus.executeMenuAction(). -function removeContextMenu(menu_id) { - browser.menus.remove(menu_id); - taLog.log("Context menu removed: " + menu_id); -} +const specialContextMenuActions = { + 'prompt_add_tags': (messages) => processEmails({ messages, addTagsAuto: true }), + 'prompt_spamfilter': (messages) => processEmails({ messages, spamFilter: true }), + 'prompt_summarize': (messages) => processEmails({ messages, summarize: true }), + 'prompt_translate_this': (messages) => processEmails({ messages, translate: true }), +}; -function addContextMenuItems() { - let itemsToAdd = []; +browser.menus.onClicked.addListener((info, tab) => { + const menuItemId = info.menuItemId; + if (typeof menuItemId !== 'string' || !menuItemId.startsWith('mzta-ctx-')) { + return; + } + const promptId = menuItemId.replace('mzta-ctx-', ''); - // Add Context menu: Add tags - if(prefs_init.add_tags && checkAPIIntegration(prefs_init.connection_type, prefs_init.add_tags_use_specific_integration,prefs_init.add_tags_connection_type)){ - itemsToAdd.push(contextMenuID_AddTags); + if (specialContextMenuActions[promptId]) { + specialContextMenuActions[promptId](getMessages(info.selectedMessages)); } else { - removeContextMenu(contextMenuID_AddTags); - } - - // Add Context menu: Spamfilter - if(prefs_init.spamfilter && checkAPIIntegration(prefs_init.connection_type, prefs_init.spamfilter_use_specific_integration,prefs_init.spamfilter_connection_type)){ - itemsToAdd.push(contextMenuID_Spamfilter); - } else { - removeContextMenu(contextMenuID_Spamfilter); - } - - // Add Context menu: Summarize - if(prefs_init.summarize && checkAPIIntegration(prefs_init.connection_type, prefs_init.summarize_use_specific_integration, prefs_init.summarize_connection_type)) { - itemsToAdd.push(contextMenuID_Summarize); - } else { - removeContextMenu(contextMenuID_Summarize); - } - - // Add Context menu: Translate - if(prefs_init.translate && checkAPIIntegration(prefs_init.connection_type, prefs_init.translate_use_specific_integration, prefs_init.translate_connection_type)){ - itemsToAdd.push(contextMenuID_Translate); - } else { - removeContextMenu(contextMenuID_Translate); - } - - itemsToAdd.sort((a, b) => { - let titleA = browser.i18n.getMessage("context_menu_" + a); - let titleB = browser.i18n.getMessage("context_menu_" + b); - return titleA.localeCompare(titleB); - }); - - itemsToAdd.forEach(menu_id => { - addContextMenu(menu_id); - }); -} - -addContextMenuItems(); - -// Listen for context menu item clicks -browser.menus.onClicked.addListener( (info, tab) => { - let _add_tags = false - let _spamfilter = false - let _summarize = false; - let _translate = false; - if(info.menuItemId === contextMenuID_AddTags){ - _add_tags = true; - } - if(info.menuItemId === contextMenuID_Spamfilter){ - _spamfilter = true; - } - if(info.menuItemId === contextMenuID_Summarize) { - _summarize = true; - } - if(info.menuItemId === contextMenuID_Translate) { - _translate = true; - } - if(_add_tags || _spamfilter || _summarize || _translate){ - processEmails({ - messages: getMessages(info.selectedMessages), - addTagsAuto: _add_tags, - spamFilter: _spamfilter, - summarize: _summarize, - translate: _translate - }); + menus.executeMenuAction(promptId); } }); diff --git a/pages/customprompts/mzta-custom-prompts.html b/pages/customprompts/mzta-custom-prompts.html index 34622d5d..9351ea3f 100644 --- a/pages/customprompts/mzta-custom-prompts.html +++ b/pages/customprompts/mzta-custom-prompts.html @@ -51,6 +51,15 @@

+ +
+ +


@@ -1120,6 +1144,17 @@ function loadPromptsList(values){ ` + `

+ __MSG_show_in__: +
+ ` + show_in_output + ` + ` + + ` +

__MSG_customPrompts_form_label_Action__:
` + action_output + `