synabun 2.0.7 → 2.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +1 -1
- package/hooks/claude-code/post-plan.mjs +6 -30
- package/hooks/claude-code/session-start.mjs +1 -4
- package/hooks/claude-code/shared.mjs +106 -72
- package/lib/package-processes.js +670 -0
- package/lib/runtime-dependencies.js +225 -0
- package/mcp-server/dist/services/browser-bench.d.ts +1 -1
- package/mcp-server/dist/services/browser-bench.d.ts.map +1 -1
- package/mcp-server/dist/services/browser-bench.js +1 -1
- package/mcp-server/dist/services/browser-bench.js.map +1 -1
- package/mcp-server/dist/services/categories.d.ts +35 -2
- package/mcp-server/dist/services/categories.d.ts.map +1 -1
- package/mcp-server/dist/services/categories.js +77 -33
- package/mcp-server/dist/services/categories.js.map +1 -1
- package/mcp-server/dist/services/desktop-bench.d.ts +7 -7
- package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -1
- package/mcp-server/dist/services/desktop-bench.js +1 -1
- package/mcp-server/dist/services/desktop-bench.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +1 -1
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +1 -1
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/bluesky.js +1 -1
- package/mcp-server/dist/tools/bluesky.js.map +1 -1
- package/mcp-server/dist/tools/browser-advanced.d.ts +1 -1
- package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-advanced.js +1 -1
- package/mcp-server/dist/tools/browser-advanced.js.map +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-batch.js +1 -1
- package/mcp-server/dist/tools/browser-batch.js.map +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.js +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -1
- package/mcp-server/dist/tools/browser-interact.d.ts +1 -1
- package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-interact.js +1 -1
- package/mcp-server/dist/tools/browser-interact.js.map +1 -1
- package/mcp-server/dist/tools/browser-navigate.d.ts +1 -1
- package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-navigate.js +1 -1
- package/mcp-server/dist/tools/browser-navigate.js.map +1 -1
- package/mcp-server/dist/tools/browser-observe.d.ts +1 -1
- package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-observe.js +1 -1
- package/mcp-server/dist/tools/browser-observe.js.map +1 -1
- package/mcp-server/dist/tools/card-tools.d.ts +1 -1
- package/mcp-server/dist/tools/card-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/card-tools.js +1 -1
- package/mcp-server/dist/tools/card-tools.js.map +1 -1
- package/mcp-server/dist/tools/category.d.ts +1 -1
- package/mcp-server/dist/tools/category.d.ts.map +1 -1
- package/mcp-server/dist/tools/category.js +5 -1
- package/mcp-server/dist/tools/category.js.map +1 -1
- package/mcp-server/dist/tools/choice.d.ts +1 -1
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +1 -1
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/computer.d.ts +1 -1
- package/mcp-server/dist/tools/computer.d.ts.map +1 -1
- package/mcp-server/dist/tools/computer.js +1 -1
- package/mcp-server/dist/tools/computer.js.map +1 -1
- package/mcp-server/dist/tools/discord-channel.d.ts +1 -1
- package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-channel.js +1 -1
- package/mcp-server/dist/tools/discord-channel.js.map +1 -1
- package/mcp-server/dist/tools/discord-guild.d.ts +1 -1
- package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-guild.js +1 -1
- package/mcp-server/dist/tools/discord-guild.js.map +1 -1
- package/mcp-server/dist/tools/discord-member.d.ts +1 -1
- package/mcp-server/dist/tools/discord-member.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-member.js +1 -1
- package/mcp-server/dist/tools/discord-member.js.map +1 -1
- package/mcp-server/dist/tools/discord-message.d.ts +1 -1
- package/mcp-server/dist/tools/discord-message.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-message.js +1 -1
- package/mcp-server/dist/tools/discord-message.js.map +1 -1
- package/mcp-server/dist/tools/discord-onboarding.d.ts +1 -1
- package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-onboarding.js +1 -1
- package/mcp-server/dist/tools/discord-onboarding.js.map +1 -1
- package/mcp-server/dist/tools/discord-role.d.ts +1 -1
- package/mcp-server/dist/tools/discord-role.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-role.js +1 -1
- package/mcp-server/dist/tools/discord-role.js.map +1 -1
- package/mcp-server/dist/tools/discord-thread.d.ts +1 -1
- package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-thread.js +1 -1
- package/mcp-server/dist/tools/discord-thread.js.map +1 -1
- package/mcp-server/dist/tools/discord-webhook.d.ts +1 -1
- package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-webhook.js +1 -1
- package/mcp-server/dist/tools/discord-webhook.js.map +1 -1
- package/mcp-server/dist/tools/fb-groups.d.ts +1 -1
- package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -1
- package/mcp-server/dist/tools/fb-groups.js +1 -1
- package/mcp-server/dist/tools/fb-groups.js.map +1 -1
- package/mcp-server/dist/tools/forget.d.ts +1 -1
- package/mcp-server/dist/tools/forget.d.ts.map +1 -1
- package/mcp-server/dist/tools/forget.js +1 -1
- package/mcp-server/dist/tools/forget.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.d.ts +1 -1
- package/mcp-server/dist/tools/git-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +1 -1
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/gsc/experience.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/experience.js +1 -1
- package/mcp-server/dist/tools/gsc/experience.js.map +1 -1
- package/mcp-server/dist/tools/gsc/indexing.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/indexing.js +1 -1
- package/mcp-server/dist/tools/gsc/indexing.js.map +1 -1
- package/mcp-server/dist/tools/gsc/inspect.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/inspect.js +1 -1
- package/mcp-server/dist/tools/gsc/inspect.js.map +1 -1
- package/mcp-server/dist/tools/gsc/navigate.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/navigate.js +1 -1
- package/mcp-server/dist/tools/gsc/navigate.js.map +1 -1
- package/mcp-server/dist/tools/gsc/performance.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/performance.js +1 -1
- package/mcp-server/dist/tools/gsc/performance.js.map +1 -1
- package/mcp-server/dist/tools/gsc/settings.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/settings.js +1 -1
- package/mcp-server/dist/tools/gsc/settings.js.map +1 -1
- package/mcp-server/dist/tools/image-tools.d.ts +1 -1
- package/mcp-server/dist/tools/image-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/image-tools.js +1 -1
- package/mcp-server/dist/tools/image-tools.js.map +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -1
- package/mcp-server/dist/tools/loop.d.ts +1 -1
- package/mcp-server/dist/tools/loop.d.ts.map +1 -1
- package/mcp-server/dist/tools/loop.js +1 -1
- package/mcp-server/dist/tools/loop.js.map +1 -1
- package/mcp-server/dist/tools/memories.d.ts +1 -1
- package/mcp-server/dist/tools/memories.d.ts.map +1 -1
- package/mcp-server/dist/tools/memories.js +1 -1
- package/mcp-server/dist/tools/memories.js.map +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +1 -1
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/profile.d.ts +1 -1
- package/mcp-server/dist/tools/profile.d.ts.map +1 -1
- package/mcp-server/dist/tools/profile.js +1 -1
- package/mcp-server/dist/tools/profile.js.map +1 -1
- package/mcp-server/dist/tools/recall.d.ts +1 -1
- package/mcp-server/dist/tools/recall.d.ts.map +1 -1
- package/mcp-server/dist/tools/recall.js +1 -1
- package/mcp-server/dist/tools/recall.js.map +1 -1
- package/mcp-server/dist/tools/reflect.d.ts +1 -1
- package/mcp-server/dist/tools/reflect.d.ts.map +1 -1
- package/mcp-server/dist/tools/reflect.js +1 -1
- package/mcp-server/dist/tools/reflect.js.map +1 -1
- package/mcp-server/dist/tools/remember.d.ts +1 -1
- package/mcp-server/dist/tools/remember.d.ts.map +1 -1
- package/mcp-server/dist/tools/remember.js +13 -11
- package/mcp-server/dist/tools/remember.js.map +1 -1
- package/mcp-server/dist/tools/restore.d.ts +1 -1
- package/mcp-server/dist/tools/restore.d.ts.map +1 -1
- package/mcp-server/dist/tools/restore.js +1 -1
- package/mcp-server/dist/tools/restore.js.map +1 -1
- package/mcp-server/dist/tools/style-guide-tools.d.ts +1 -1
- package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/style-guide-tools.js +1 -1
- package/mcp-server/dist/tools/style-guide-tools.js.map +1 -1
- package/mcp-server/dist/tools/sync.d.ts +1 -1
- package/mcp-server/dist/tools/sync.d.ts.map +1 -1
- package/mcp-server/dist/tools/sync.js +1 -1
- package/mcp-server/dist/tools/sync.js.map +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.d.ts +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.js +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -1
- package/mcp-server/dist/tools/utils.d.ts +1 -1
- package/mcp-server/dist/tools/utils.d.ts.map +1 -1
- package/mcp-server/dist/tools/utils.js +1 -1
- package/mcp-server/dist/tools/utils.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.js +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +3 -3
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
- package/mcp-server/dist/tools/youtube/analytics.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/analytics.js +1 -1
- package/mcp-server/dist/tools/youtube/analytics.js.map +1 -1
- package/mcp-server/dist/tools/youtube/download.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/download.js +1 -1
- package/mcp-server/dist/tools/youtube/download.js.map +1 -1
- package/mcp-server/dist/tools/youtube/media.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/media.js +1 -1
- package/mcp-server/dist/tools/youtube/media.js.map +1 -1
- package/mcp-server/dist/tools/youtube/metadata.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/metadata.js +1 -1
- package/mcp-server/dist/tools/youtube/metadata.js.map +1 -1
- package/mcp-server/dist/tools/youtube/navigate.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/navigate.js +1 -1
- package/mcp-server/dist/tools/youtube/navigate.js.map +1 -1
- package/mcp-server/dist/tools/youtube/playlist.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/playlist.js +1 -1
- package/mcp-server/dist/tools/youtube/playlist.js.map +1 -1
- package/mcp-server/dist/tools/youtube/schedule.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/schedule.js +1 -1
- package/mcp-server/dist/tools/youtube/schedule.js.map +1 -1
- package/mcp-server/dist/tools/youtube/source.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/source.js +1 -1
- package/mcp-server/dist/tools/youtube/source.js.map +1 -1
- package/mcp-server/dist/tools/youtube/upload.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/upload.js +4 -9
- package/mcp-server/dist/tools/youtube/upload.js.map +1 -1
- package/mcp-server/dist/tui.js +16 -4
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/package-lock.json +3 -3
- package/mcp-server/package.json +3 -3
- package/mcp-server/src/services/browser-bench.ts +1 -1
- package/mcp-server/src/services/categories.ts +77 -32
- package/mcp-server/src/services/desktop-bench.ts +1 -1
- package/mcp-server/src/tools/agents.ts +1 -1
- package/mcp-server/src/tools/bluesky.ts +1 -1
- package/mcp-server/src/tools/browser-advanced.ts +1 -1
- package/mcp-server/src/tools/browser-batch.ts +1 -1
- package/mcp-server/src/tools/browser-cheatsheet.ts +1 -1
- package/mcp-server/src/tools/browser-interact.ts +1 -1
- package/mcp-server/src/tools/browser-navigate.ts +1 -1
- package/mcp-server/src/tools/browser-observe.ts +1 -1
- package/mcp-server/src/tools/card-tools.ts +1 -1
- package/mcp-server/src/tools/category.ts +6 -1
- package/mcp-server/src/tools/choice.ts +1 -1
- package/mcp-server/src/tools/computer.ts +1 -1
- package/mcp-server/src/tools/discord-channel.ts +1 -1
- package/mcp-server/src/tools/discord-guild.ts +1 -1
- package/mcp-server/src/tools/discord-member.ts +1 -1
- package/mcp-server/src/tools/discord-message.ts +1 -1
- package/mcp-server/src/tools/discord-onboarding.ts +1 -1
- package/mcp-server/src/tools/discord-role.ts +1 -1
- package/mcp-server/src/tools/discord-thread.ts +1 -1
- package/mcp-server/src/tools/discord-webhook.ts +1 -1
- package/mcp-server/src/tools/fb-groups.ts +1 -1
- package/mcp-server/src/tools/forget.ts +1 -1
- package/mcp-server/src/tools/git-tools.ts +1 -1
- package/mcp-server/src/tools/gsc/experience.ts +1 -1
- package/mcp-server/src/tools/gsc/indexing.ts +1 -1
- package/mcp-server/src/tools/gsc/inspect.ts +1 -1
- package/mcp-server/src/tools/gsc/navigate.ts +1 -1
- package/mcp-server/src/tools/gsc/performance.ts +1 -1
- package/mcp-server/src/tools/gsc/settings.ts +1 -1
- package/mcp-server/src/tools/image-tools.ts +1 -1
- package/mcp-server/src/tools/leonardo-browser-tools.ts +1 -1
- package/mcp-server/src/tools/loop.ts +1 -1
- package/mcp-server/src/tools/memories.ts +1 -1
- package/mcp-server/src/tools/morelogin.ts +1 -1
- package/mcp-server/src/tools/profile.ts +1 -1
- package/mcp-server/src/tools/recall.ts +1 -1
- package/mcp-server/src/tools/reflect.ts +1 -1
- package/mcp-server/src/tools/remember.ts +13 -12
- package/mcp-server/src/tools/restore.ts +1 -1
- package/mcp-server/src/tools/style-guide-tools.ts +1 -1
- package/mcp-server/src/tools/sync.ts +1 -1
- package/mcp-server/src/tools/tictactoe-tools.ts +1 -1
- package/mcp-server/src/tools/utils.ts +1 -1
- package/mcp-server/src/tools/whatsapp-send-file.ts +1 -1
- package/mcp-server/src/tools/whiteboard-tools.ts +1 -1
- package/mcp-server/src/tools/youtube/analytics.ts +1 -1
- package/mcp-server/src/tools/youtube/download.ts +1 -1
- package/mcp-server/src/tools/youtube/media.ts +1 -1
- package/mcp-server/src/tools/youtube/metadata.ts +1 -1
- package/mcp-server/src/tools/youtube/navigate.ts +1 -1
- package/mcp-server/src/tools/youtube/playlist.ts +1 -1
- package/mcp-server/src/tools/youtube/schedule.ts +1 -1
- package/mcp-server/src/tools/youtube/source.ts +1 -1
- package/mcp-server/src/tools/youtube/upload.ts +4 -12
- package/mcp-server/src/tui.ts +22 -9
- package/neural-interface/i18n/en.json +18 -0
- package/neural-interface/i18n/pt-BR.json +18 -0
- package/neural-interface/lib/agent-sandbox-elevation.js +102 -0
- package/neural-interface/lib/agent-sandbox-setting.js +57 -0
- package/neural-interface/lib/assistant-api.js +19 -0
- package/neural-interface/lib/assistant-route-gate-codex.js +3 -1
- package/neural-interface/lib/assistant-runtime.js +5 -1
- package/neural-interface/lib/codex-account-auth.js +4 -2
- package/neural-interface/lib/codex-accounts.js +14 -1
- package/neural-interface/lib/codex-settings-schema.js +5 -3
- package/neural-interface/lib/exec-loop-command.js +3 -1
- package/neural-interface/lib/install-details.js +23 -5
- package/neural-interface/lib/native-loop-providers.js +6 -1
- package/neural-interface/lib/provider-panel-terminal-auth.js +3 -2
- package/neural-interface/lib/session-indexer.js +3 -1
- package/neural-interface/lib/session-title-generator.js +2 -0
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/shared/account-menu.js +4 -1
- package/neural-interface/public/shared/account-usage.js +62 -2
- package/neural-interface/public/shared/account-view.js +2 -1
- package/neural-interface/public/shared/assistant/asst-account-menu.js +298 -0
- package/neural-interface/public/shared/assistant/asst-api.js +7 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +232 -75
- package/neural-interface/public/shared/assistant/asst-menu.js +204 -77
- package/neural-interface/public/shared/assistant/asst-model-chip.js +135 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +39 -2
- package/neural-interface/public/shared/assistant/asst-styles.js +66 -17
- package/neural-interface/public/shared/cdx/cdx-panel.js +1 -1
- package/neural-interface/public/shared/cdx/cdx-styles.js +45 -5
- package/neural-interface/public/shared/cdx/cdx-tabs.js +3 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +4 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +51 -2
- package/neural-interface/public/shared/showcase-privacy.js +2 -1
- package/neural-interface/public/shared/styles.css +3 -1
- package/neural-interface/public/shared/ui-claude-panel.js +50 -4
- package/neural-interface/public/shared/ui-explorer.js +2 -0
- package/neural-interface/public/shared/ui-sync.js +2 -0
- package/neural-interface/public/shared/ui-update.js +4 -2
- package/neural-interface/scripts/rebuild-pty.js +14 -2
- package/neural-interface/server.js +180 -86
- package/npm-shrinkwrap.json +4424 -0
- package/package.json +54 -2
- package/postinstall.js +61 -47
- package/preuninstall.js +8 -35
- package/setup.js +76 -44
- package/skills/synabun/modules/idea.md +1 -1
- package/skills/synabun/modules/schedule.md +1 -1
- package/updater.cmd +2 -2
- package/updater.mjs +97 -3
|
@@ -4,14 +4,23 @@
|
|
|
4
4
|
// One body-level popover for every assistant menu (brain fields, model menu,
|
|
5
5
|
// sessions, "⋯", route mode). Appended to <body> so it escapes the viewport's
|
|
6
6
|
// paint containment; positioned with fixed coordinates next to its anchor and
|
|
7
|
-
//
|
|
7
|
+
// kept inside the Assistant that holds the anchor (the sidepanel, or a tab's own
|
|
8
|
+
// root) and the window. Only one menu is open at a time.
|
|
8
9
|
//
|
|
9
10
|
// openMenu(anchor, { title, items, placement:'auto'|'above'|'below'|'prefer-below', filter,
|
|
10
11
|
// width, role:'menu'|'listbox', label, className, onClose,
|
|
11
|
-
// zIndex (a menu opened from a layer above the panel's own, e.g. Settings)
|
|
12
|
+
// zIndex (a menu opened from a layer above the panel's own, e.g. Settings),
|
|
13
|
+
// footer, bounds, onEscape })
|
|
12
14
|
// 'prefer-below' opens under the anchor whenever a usable list fits there (the
|
|
13
15
|
// menu scrolls inside that room), even when more room is above: a menu opened
|
|
14
16
|
// from Settings must not cover the Settings header. The panel's menus use 'auto'.
|
|
17
|
+
// `footer`: items under the list that stay in view while the list scrolls.
|
|
18
|
+
// Every menu is measured at the width it is shown at, never wider than its box less the margins,
|
|
19
|
+
// and opens where that box has room (the list scrolls inside it). The box is the Assistant
|
|
20
|
+
// panel or root around the anchor, else the window.
|
|
21
|
+
// `bounds`: an element (or () => DOMRect) to use as that box instead: the menu is then exactly
|
|
22
|
+
// `width` wide (or the box's room) and follows the box on resize.
|
|
23
|
+
// `onEscape`: returns true when it handled Esc (e.g. went back a view): the menu stays open.
|
|
15
24
|
// Item kinds:
|
|
16
25
|
// item (default) { label, desc, icon, tag, disabled, danger, keywords, onSelect }
|
|
17
26
|
// radio { ...item, selected } → menuitemradio / option
|
|
@@ -21,10 +30,15 @@
|
|
|
21
30
|
// info { label, value } → non-interactive row
|
|
22
31
|
// tag { label } → non-interactive badge row
|
|
23
32
|
// input { placeholder, value, onSubmit } → inline text field
|
|
33
|
+
// buttons { label, buttons: [item…] } → one row of compact items (a group)
|
|
34
|
+
// `render(button, { checked })` on an item draws its content instead of label/desc/tag.
|
|
35
|
+
// `className` on an item or an info row adds its own class.
|
|
36
|
+
// `keepOpen` on an item leaves the menu open when it is chosen.
|
|
24
37
|
// `restoreFocus` on an item or input sends focus back to the anchor once it is chosen.
|
|
25
38
|
// Keys: ↑/↓ move, Home/End jump, Enter/Space activate, Esc closes (focus back
|
|
26
39
|
// to the anchor), Tab closes. The optional filter hides non-matching items and
|
|
27
|
-
// any group left empty.
|
|
40
|
+
// any group left empty. The handle's update({ title, items, footer, focus }) redraws
|
|
41
|
+
// an open menu in place (focus: the data-id to land on).
|
|
28
42
|
|
|
29
43
|
let _open = null;
|
|
30
44
|
let _uid = 0;
|
|
@@ -87,11 +101,64 @@ export function currentMenu() {
|
|
|
87
101
|
return _open?.menu || null;
|
|
88
102
|
}
|
|
89
103
|
|
|
104
|
+
function itemButton(item, kind, { role, list, onActivate }) {
|
|
105
|
+
const btn = el('button', `asst-dd-item${item.danger ? ' danger' : ''}${item.className ? ` ${item.className}` : ''}`);
|
|
106
|
+
btn.type = 'button';
|
|
107
|
+
btn.tabIndex = -1;
|
|
108
|
+
btn.dataset.kind = kind;
|
|
109
|
+
if (item.id != null) btn.dataset.id = String(item.id);
|
|
110
|
+
btn.dataset.search = `${item.label || ''} ${item.desc || ''} ${item.keywords || ''}`.toLowerCase();
|
|
111
|
+
const checked = kind === 'radio' ? !!item.selected : kind === 'check' ? !!item.checked : false;
|
|
112
|
+
if (role === 'listbox') {
|
|
113
|
+
btn.setAttribute('role', 'option');
|
|
114
|
+
btn.setAttribute('aria-selected', checked ? 'true' : 'false');
|
|
115
|
+
} else if (kind === 'radio') {
|
|
116
|
+
btn.setAttribute('role', 'menuitemradio');
|
|
117
|
+
btn.setAttribute('aria-checked', checked ? 'true' : 'false');
|
|
118
|
+
} else if (kind === 'check') {
|
|
119
|
+
btn.setAttribute('role', 'menuitemcheckbox');
|
|
120
|
+
btn.setAttribute('aria-checked', checked ? 'true' : 'false');
|
|
121
|
+
} else {
|
|
122
|
+
btn.setAttribute('role', role === 'listbox' ? 'option' : 'menuitem');
|
|
123
|
+
}
|
|
124
|
+
if (item.disabled) btn.disabled = true;
|
|
125
|
+
if (typeof item.render === 'function') {
|
|
126
|
+
item.render(btn, { checked });
|
|
127
|
+
} else {
|
|
128
|
+
const lead = kind === 'radio' || kind === 'check'
|
|
129
|
+
? `<span class="asst-dd-check" aria-hidden="true">${checked ? ICON_CHECK : ''}</span>`
|
|
130
|
+
: '';
|
|
131
|
+
btn.innerHTML = `${lead}${item.icon ? `<span class="asst-icon" aria-hidden="true"${item.color ? ` style="color:${esc(item.color)}"` : ''}>${item.icon}</span>` : ''}<span class="asst-dd-item-main"><span class="asst-dd-item-label">${esc(item.label)}</span>${item.desc ? `<span class="asst-dd-item-desc">${esc(item.desc)}</span>` : ''}</span>${item.tag ? `<span class="asst-dd-item-tag">${esc(item.tag)}</span>` : ''}${item.shortcut ? `<kbd class="asst-dd-kbd">${esc(item.shortcut)}</kbd>` : ''}`;
|
|
132
|
+
}
|
|
133
|
+
if (item.title) btn.title = item.title;
|
|
134
|
+
if (item.ariaLabel) btn.setAttribute('aria-label', item.ariaLabel);
|
|
135
|
+
btn.addEventListener('click', (e) => { e.stopPropagation(); if (!btn.disabled) onActivate(item, btn); });
|
|
136
|
+
btn.addEventListener('mousemove', () => {
|
|
137
|
+
if (btn.classList.contains('focused') || btn.disabled) return;
|
|
138
|
+
const menu = list.closest('.asst-dd-menu');
|
|
139
|
+
menu?.querySelectorAll('.asst-dd-item.focused').forEach(n => n.classList.remove('focused'));
|
|
140
|
+
btn.classList.add('focused');
|
|
141
|
+
// Hover never steals focus from the filter / inline input.
|
|
142
|
+
if (!document.activeElement?.classList?.contains('asst-dd-input')) { try { btn.focus({ preventScroll: true }); } catch { /* ignore */ } }
|
|
143
|
+
});
|
|
144
|
+
if (checked) btn.dataset.checked = '1';
|
|
145
|
+
return btn;
|
|
146
|
+
}
|
|
147
|
+
|
|
90
148
|
function buildItems(list, items, { role, onActivate }) {
|
|
91
|
-
const itemRole = role === 'listbox' ? 'option' : 'menuitem';
|
|
92
149
|
for (const item of items || []) {
|
|
93
150
|
const kind = itemKind(item);
|
|
94
151
|
if (kind === 'separator') { list.appendChild(el('div', 'asst-dd-sep')).setAttribute('role', 'separator'); continue; }
|
|
152
|
+
if (kind === 'buttons') {
|
|
153
|
+
const row = el('div', 'asst-dd-btnrow');
|
|
154
|
+
row.setAttribute('role', 'group');
|
|
155
|
+
if (item.id != null) row.dataset.id = String(item.id);
|
|
156
|
+
if (item.ariaLabel || item.label) row.setAttribute('aria-label', item.ariaLabel || item.label);
|
|
157
|
+
if (item.label) row.appendChild(el('span', 'asst-dd-btnrow-label', item.label)).setAttribute('aria-hidden', 'true');
|
|
158
|
+
for (const button of item.buttons || []) row.appendChild(itemButton(button, itemKind(button), { role, list, onActivate }));
|
|
159
|
+
list.appendChild(row);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
95
162
|
if (kind === 'header') {
|
|
96
163
|
const h = el('div', 'asst-dd-group');
|
|
97
164
|
h.setAttribute('role', 'presentation');
|
|
@@ -100,7 +167,7 @@ function buildItems(list, items, { role, onActivate }) {
|
|
|
100
167
|
continue;
|
|
101
168
|
}
|
|
102
169
|
if (kind === 'info' || kind === 'tag') {
|
|
103
|
-
const row = el('div', kind === 'tag' ? 'asst-dd-tagrow' : 'asst-dd-info');
|
|
170
|
+
const row = el('div', `${kind === 'tag' ? 'asst-dd-tagrow' : 'asst-dd-info'}${item.className ? ` ${item.className}` : ''}`);
|
|
104
171
|
row.setAttribute('role', 'presentation');
|
|
105
172
|
row.innerHTML = kind === 'tag'
|
|
106
173
|
? `<span class="asst-dd-tag">${esc(item.label)}</span>`
|
|
@@ -121,42 +188,7 @@ function buildItems(list, items, { role, onActivate }) {
|
|
|
121
188
|
list.appendChild(input);
|
|
122
189
|
continue;
|
|
123
190
|
}
|
|
124
|
-
|
|
125
|
-
btn.type = 'button';
|
|
126
|
-
btn.tabIndex = -1;
|
|
127
|
-
btn.dataset.kind = kind;
|
|
128
|
-
if (item.id != null) btn.dataset.id = String(item.id);
|
|
129
|
-
btn.dataset.search = `${item.label || ''} ${item.desc || ''} ${item.keywords || ''}`.toLowerCase();
|
|
130
|
-
const checked = kind === 'radio' ? !!item.selected : kind === 'check' ? !!item.checked : false;
|
|
131
|
-
if (role === 'listbox') {
|
|
132
|
-
btn.setAttribute('role', 'option');
|
|
133
|
-
btn.setAttribute('aria-selected', checked ? 'true' : 'false');
|
|
134
|
-
} else if (kind === 'radio') {
|
|
135
|
-
btn.setAttribute('role', 'menuitemradio');
|
|
136
|
-
btn.setAttribute('aria-checked', checked ? 'true' : 'false');
|
|
137
|
-
} else if (kind === 'check') {
|
|
138
|
-
btn.setAttribute('role', 'menuitemcheckbox');
|
|
139
|
-
btn.setAttribute('aria-checked', checked ? 'true' : 'false');
|
|
140
|
-
} else {
|
|
141
|
-
btn.setAttribute('role', itemRole);
|
|
142
|
-
}
|
|
143
|
-
if (item.disabled) btn.disabled = true;
|
|
144
|
-
const lead = kind === 'radio' || kind === 'check'
|
|
145
|
-
? `<span class="asst-dd-check" aria-hidden="true">${checked ? ICON_CHECK : ''}</span>`
|
|
146
|
-
: '';
|
|
147
|
-
btn.innerHTML = `${lead}${item.icon ? `<span class="asst-icon" aria-hidden="true"${item.color ? ` style="color:${esc(item.color)}"` : ''}>${item.icon}</span>` : ''}<span class="asst-dd-item-main"><span class="asst-dd-item-label">${esc(item.label)}</span>${item.desc ? `<span class="asst-dd-item-desc">${esc(item.desc)}</span>` : ''}</span>${item.tag ? `<span class="asst-dd-item-tag">${esc(item.tag)}</span>` : ''}${item.shortcut ? `<kbd class="asst-dd-kbd">${esc(item.shortcut)}</kbd>` : ''}`;
|
|
148
|
-
if (item.title) btn.title = item.title;
|
|
149
|
-
btn.addEventListener('click', (e) => { e.stopPropagation(); if (!btn.disabled) onActivate(item, btn); });
|
|
150
|
-
btn.addEventListener('mousemove', () => {
|
|
151
|
-
if (btn.classList.contains('focused') || btn.disabled) return;
|
|
152
|
-
const menu = list.closest('.asst-dd-menu');
|
|
153
|
-
menu?.querySelectorAll('.asst-dd-item.focused').forEach(n => n.classList.remove('focused'));
|
|
154
|
-
btn.classList.add('focused');
|
|
155
|
-
// Hover never steals focus from the filter / inline input.
|
|
156
|
-
if (!document.activeElement?.classList?.contains('asst-dd-input')) { try { btn.focus({ preventScroll: true }); } catch { /* ignore */ } }
|
|
157
|
-
});
|
|
158
|
-
if (checked) btn.dataset.checked = '1';
|
|
159
|
-
list.appendChild(btn);
|
|
191
|
+
list.appendChild(itemButton(item, kind, { role, list, onActivate }));
|
|
160
192
|
}
|
|
161
193
|
if (!list.querySelector('.asst-dd-item, .asst-dd-input, .asst-dd-info, .asst-dd-tagrow')) list.appendChild(el('div', 'asst-dd-empty', '—'));
|
|
162
194
|
}
|
|
@@ -167,6 +199,8 @@ export function applyMenuFilter(list, query, emptyLabel = 'No matches') {
|
|
|
167
199
|
const rows = [...list.children];
|
|
168
200
|
for (const row of rows) {
|
|
169
201
|
if (row.classList.contains('asst-dd-item')) row.hidden = !!q && !row.dataset.search.includes(q);
|
|
202
|
+
// A button row stays whole: shown while one of its buttons matches.
|
|
203
|
+
else if (row.classList.contains('asst-dd-btnrow')) row.hidden = !!q && ![...row.querySelectorAll('.asst-dd-item')].some(b => b.dataset.search.includes(q));
|
|
170
204
|
else if (row.classList.contains('asst-dd-info') || row.classList.contains('asst-dd-tagrow')) row.hidden = !!q;
|
|
171
205
|
}
|
|
172
206
|
// Group headers: visible only when an item until the next header/separator is visible.
|
|
@@ -192,7 +226,7 @@ export function applyMenuFilter(list, query, emptyLabel = 'No matches') {
|
|
|
192
226
|
const last = visible[visible.length - 1];
|
|
193
227
|
if (last?.classList.contains('asst-dd-sep')) last.hidden = true;
|
|
194
228
|
let empty = list.querySelector('.asst-dd-empty.asst-dd-noresults');
|
|
195
|
-
const hasItem = rows.some(r => !r.hidden && (r.classList.contains('asst-dd-item') || r.classList.contains('asst-dd-input')));
|
|
229
|
+
const hasItem = rows.some(r => !r.hidden && (r.classList.contains('asst-dd-item') || r.classList.contains('asst-dd-btnrow') || r.classList.contains('asst-dd-input')));
|
|
196
230
|
if (q && !hasItem) {
|
|
197
231
|
if (!empty) { empty = el('div', 'asst-dd-empty asst-dd-noresults', emptyLabel); list.appendChild(empty); }
|
|
198
232
|
empty.hidden = false;
|
|
@@ -204,7 +238,8 @@ const PREFER_BELOW_MIN = 220;
|
|
|
204
238
|
|
|
205
239
|
/**
|
|
206
240
|
* Whether a menu opens above its anchor (pure; sizes in px). `above` / `below`:
|
|
207
|
-
* the room on each side of the anchor
|
|
241
|
+
* the room on each side of the anchor; `anchorTop` / `viewportHeight`: the anchor's
|
|
242
|
+
* top in the box the menu stays inside, and that box's height.
|
|
208
243
|
*/
|
|
209
244
|
export function menuOpensAbove(placement, { anchorTop, anchorHeight, menuHeight, above, below, viewportHeight }) {
|
|
210
245
|
if (placement === 'above') return menuHeight <= above || above >= below;
|
|
@@ -214,37 +249,101 @@ export function menuOpensAbove(placement, { anchorTop, anchorHeight, menuHeight,
|
|
|
214
249
|
return !(menuHeight <= below || below >= above);
|
|
215
250
|
}
|
|
216
251
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
252
|
+
// A menu: never narrower than this while the window allows it; never shorter than these
|
|
253
|
+
// (a bounded menu holds a title and a footer around its list).
|
|
254
|
+
const BOUNDED_MIN_WIDTH = 248;
|
|
255
|
+
const MENU_MIN_HEIGHT = 96;
|
|
256
|
+
const BOUNDED_MIN_HEIGHT = 140;
|
|
257
|
+
|
|
258
|
+
/** The box a menu stays inside: its bounds clipped to the window (the window when it has none). */
|
|
259
|
+
function boundsBox(bounds) {
|
|
220
260
|
const vw = window.innerWidth;
|
|
221
261
|
const vh = window.innerHeight;
|
|
222
|
-
|
|
223
|
-
|
|
262
|
+
let r = null;
|
|
263
|
+
try { r = typeof bounds === 'function' ? bounds() : bounds?.getBoundingClientRect?.(); } catch { r = null; }
|
|
264
|
+
if (!r || !(r.width > 0) || !(r.height > 0)) return { left: 0, top: 0, right: vw, bottom: vh };
|
|
265
|
+
return { left: Math.max(0, r.left), top: Math.max(0, r.top), right: Math.min(vw, r.right), bottom: Math.min(vh, r.bottom) };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Where a menu without `bounds` stays: the Assistant around its anchor (the sidepanel, or a tab's own root). */
|
|
269
|
+
function assistantOf(anchor) {
|
|
270
|
+
return anchor.closest?.('.assistant-panel') || anchor.closest?.('.asst-root') || null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The widest a menu may be in its box (pure; px): the box less the margins, never under the minimum the window allows. */
|
|
274
|
+
export function menuRoomWidth({ box, viewportWidth, margin = 8 }) {
|
|
275
|
+
return Math.round(Math.max(box.right - box.left - margin * 2, Math.min(BOUNDED_MIN_WIDTH, viewportWidth - margin * 2)));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A bounded menu's width (pure; px): `width` at most, its box less the margins, never under the minimum the window allows. */
|
|
279
|
+
export function boundedMenuWidth({ box, viewportWidth, width, margin = 8 }) {
|
|
280
|
+
return Math.round(Math.max(Math.min(width || 320, box.right - box.left - margin * 2), Math.min(BOUNDED_MIN_WIDTH, viewportWidth - margin * 2)));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Where a menu goes (pure; px; rects as getBoundingClientRect gives them): under the anchor or
|
|
285
|
+
* above it as `placement` decides from the room inside the box (menuOpensAbove), as tall as that
|
|
286
|
+
* room; right-aligned with `alignX` (default: the anchor's right edge), or starting at the anchor's
|
|
287
|
+
* left edge with `alignLeft`; always inside the box and the window.
|
|
288
|
+
* → { left, top, maxHeight (null: its own height fits), up }.
|
|
289
|
+
*/
|
|
290
|
+
export function boundedMenuPlacement({ anchor, box, viewportWidth, viewportHeight, width, menuHeight, placement = 'below', alignX = null, alignLeft = false, minHeight = BOUNDED_MIN_HEIGHT, margin = 8, gap = 6 }) {
|
|
291
|
+
const below = box.bottom - margin - (anchor.bottom + gap);
|
|
292
|
+
const above = anchor.top - gap - (box.top + margin);
|
|
293
|
+
const up = menuOpensAbove(placement, { anchorTop: anchor.top - box.top, anchorHeight: anchor.bottom - anchor.top, menuHeight, above, below, viewportHeight: box.bottom - box.top });
|
|
294
|
+
const room = Math.max(minHeight, Math.floor(up ? above : below));
|
|
295
|
+
const height = Math.min(menuHeight, room);
|
|
296
|
+
let top = up ? anchor.top - gap - height : anchor.bottom + gap;
|
|
297
|
+
top = Math.max(margin, Math.min(top, viewportHeight - margin - height));
|
|
298
|
+
let left = alignLeft ? anchor.left : (alignX ?? anchor.right) - width;
|
|
299
|
+
left = Math.min(left, box.right - margin - width);
|
|
300
|
+
left = Math.max(left, box.left + margin);
|
|
301
|
+
left = Math.max(margin, Math.min(left, viewportWidth - margin - width));
|
|
302
|
+
return { left: Math.round(left), top: Math.round(top), maxHeight: menuHeight > room ? room : null, up };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Place a menu inside its box: its width is final before it is measured (the CSS caps read the
|
|
307
|
+
* box's room through --asst-dd-room; a bounded menu is set to its width), so what is shown is what
|
|
308
|
+
* was placed; then boundedMenuPlacement. The list scrolls inside, title and footer stay.
|
|
309
|
+
*/
|
|
310
|
+
function positionMenu(menu, anchor, { placement, bounds, width }) {
|
|
311
|
+
const viewportWidth = window.innerWidth;
|
|
312
|
+
const viewportHeight = window.innerHeight;
|
|
313
|
+
const box = boundsBox(bounds || assistantOf(anchor));
|
|
314
|
+
if (bounds) {
|
|
315
|
+
menu.style.minWidth = '0';
|
|
316
|
+
menu.style.maxWidth = 'none';
|
|
317
|
+
menu.style.width = `${boundedMenuWidth({ box, viewportWidth, width })}px`;
|
|
318
|
+
menu.style.maxHeight = 'none';
|
|
319
|
+
} else {
|
|
320
|
+
menu.style.setProperty('--asst-dd-room', `${menuRoomWidth({ box, viewportWidth })}px`);
|
|
321
|
+
menu.style.maxHeight = '';
|
|
322
|
+
}
|
|
224
323
|
menu.style.left = '0px';
|
|
225
324
|
menu.style.top = '0px';
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
325
|
+
// The sidepanel hugs the right screen edge: its menus grow leftward from the click point (or the
|
|
326
|
+
// anchor's right edge for keyboard opens), as a bounded menu does; elsewhere a menu starts at its anchor.
|
|
327
|
+
const inSidepanel = !!anchor.closest?.('.assistant-panel');
|
|
328
|
+
const clicked = inSidepanel && _lastDown && anchor.contains(_lastDown.target) && performance.now() - _lastDown.at < 1500;
|
|
329
|
+
const spot = boundedMenuPlacement({
|
|
330
|
+
anchor: anchor.getBoundingClientRect(), box, viewportWidth, viewportHeight,
|
|
331
|
+
width: menu.offsetWidth, menuHeight: menu.offsetHeight, placement,
|
|
332
|
+
alignX: clicked ? _lastDown.x : null, alignLeft: !bounds && !inSidepanel,
|
|
333
|
+
minHeight: bounds ? BOUNDED_MIN_HEIGHT : MENU_MIN_HEIGHT,
|
|
334
|
+
});
|
|
335
|
+
if (spot.maxHeight != null) menu.style.maxHeight = `${spot.maxHeight}px`;
|
|
336
|
+
menu.style.left = `${spot.left}px`;
|
|
337
|
+
menu.style.top = `${spot.top}px`;
|
|
338
|
+
menu.dataset.placement = spot.up ? 'above' : 'below';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** A row its menu's width cuts short (one-line label or description) keeps its words in a tooltip. */
|
|
342
|
+
function titleCutRows(menu) {
|
|
343
|
+
for (const btn of menu.querySelectorAll('.asst-dd-item:not([title])')) {
|
|
344
|
+
const parts = [...btn.querySelectorAll('.asst-dd-item-label, .asst-dd-item-desc')];
|
|
345
|
+
if (parts.some(n => n.scrollWidth > n.clientWidth + 1)) btn.title = parts.map(n => n.textContent).filter(Boolean).join(' · ');
|
|
241
346
|
}
|
|
242
|
-
if (left + mw > vw - margin) left = vw - margin - mw;
|
|
243
|
-
left = Math.max(margin, left);
|
|
244
|
-
menu.style.left = `${Math.round(left)}px`;
|
|
245
|
-
menu.style.top = `${Math.round(top)}px`;
|
|
246
|
-
menu.style.maxWidth = `${vw - margin * 2}px`;
|
|
247
|
-
menu.dataset.placement = up ? 'above' : 'below';
|
|
248
347
|
}
|
|
249
348
|
|
|
250
349
|
/**
|
|
@@ -262,8 +361,9 @@ export function openMenu(anchor, opts = {}) {
|
|
|
262
361
|
menu.setAttribute('role', role);
|
|
263
362
|
menu.id = `asst-menu-${++_uid}`;
|
|
264
363
|
menu.tabIndex = -1;
|
|
364
|
+
let heading = null;
|
|
265
365
|
if (title) {
|
|
266
|
-
|
|
366
|
+
heading = el('div', 'asst-dd-menu-title', title);
|
|
267
367
|
heading.id = `${menu.id}-title`;
|
|
268
368
|
menu.setAttribute('aria-labelledby', heading.id);
|
|
269
369
|
menu.appendChild(heading);
|
|
@@ -290,13 +390,25 @@ export function openMenu(anchor, opts = {}) {
|
|
|
290
390
|
};
|
|
291
391
|
if (opts.content) list.appendChild(opts.content);
|
|
292
392
|
else buildItems(list, items, { role, onActivate: activate });
|
|
293
|
-
|
|
294
|
-
|
|
393
|
+
// The footer stays under the list: only the list scrolls.
|
|
394
|
+
let foot = null;
|
|
395
|
+
const fillFooter = (footer) => {
|
|
396
|
+
if (!foot) { foot = el('div', 'asst-dd-foot'); menu.appendChild(foot); }
|
|
397
|
+
foot.replaceChildren();
|
|
398
|
+
if (Array.isArray(footer) && footer.length) buildItems(foot, footer, { role, onActivate: activate });
|
|
399
|
+
foot.hidden = !(Array.isArray(footer) && footer.length);
|
|
400
|
+
};
|
|
401
|
+
if (Array.isArray(opts.footer)) fillFooter(opts.footer);
|
|
402
|
+
// `width`: at least that wide (exactly, with repositionOnResize), never wider than the room positionMenu sets.
|
|
403
|
+
const capped = `min(${width}px, var(--asst-dd-room, calc(100vw - 16px)))`;
|
|
404
|
+
if (width) menu.style.minWidth = capped;
|
|
405
|
+
if (width && opts.repositionOnResize) menu.style.width = capped;
|
|
295
406
|
document.body.appendChild(menu);
|
|
296
|
-
positionMenu(menu, anchor, placement);
|
|
407
|
+
const place = () => { positionMenu(menu, anchor, { placement, bounds: opts.bounds || null, width }); titleCutRows(menu); };
|
|
408
|
+
place();
|
|
297
409
|
|
|
298
410
|
if (filterInput) {
|
|
299
|
-
filterInput.addEventListener('input', () => { applyMenuFilter(list, filterInput.value, opts.filterEmptyLabel);
|
|
411
|
+
filterInput.addEventListener('input', () => { applyMenuFilter(list, filterInput.value, opts.filterEmptyLabel); place(); });
|
|
300
412
|
}
|
|
301
413
|
|
|
302
414
|
const onDocDown = (e) => {
|
|
@@ -311,7 +423,12 @@ export function openMenu(anchor, opts = {}) {
|
|
|
311
423
|
const inInput = target?.classList?.contains('asst-dd-input');
|
|
312
424
|
const nodes = focusables(menu);
|
|
313
425
|
const idx = nodes.indexOf(document.activeElement);
|
|
314
|
-
if (e.key === 'Escape') {
|
|
426
|
+
if (e.key === 'Escape') {
|
|
427
|
+
e.preventDefault(); e.stopPropagation();
|
|
428
|
+
if (typeof opts.onEscape === 'function' && opts.onEscape() === true) return;
|
|
429
|
+
closeMenu({ restoreFocus: true });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
315
432
|
if (role === 'dialog') {
|
|
316
433
|
if (e.key === 'Tab') {
|
|
317
434
|
const fields = [...menu.querySelectorAll('button, input')].filter(node => !node.disabled && !node.hidden);
|
|
@@ -354,7 +471,7 @@ export function openMenu(anchor, opts = {}) {
|
|
|
354
471
|
// Keep single-letter global keybinds out of an open menu.
|
|
355
472
|
e.stopPropagation();
|
|
356
473
|
};
|
|
357
|
-
const onResize = () => opts.repositionOnResize ?
|
|
474
|
+
const onResize = () => (opts.repositionOnResize || opts.bounds ? place() : closeMenu());
|
|
358
475
|
const onBlur = () => closeMenu();
|
|
359
476
|
document.addEventListener('mousedown', onDocDown, true);
|
|
360
477
|
const keyTarget = role === 'dialog' ? window : document;
|
|
@@ -381,7 +498,17 @@ export function openMenu(anchor, opts = {}) {
|
|
|
381
498
|
} else {
|
|
382
499
|
setFocused(menu, selected || first || null);
|
|
383
500
|
}
|
|
384
|
-
|
|
501
|
+
// Redraw in place (another view of the same menu): the anchor, the listeners and the focus stay in it.
|
|
502
|
+
const update = (next = {}) => {
|
|
503
|
+
if (!_open || _open.menu !== menu) return;
|
|
504
|
+
if (typeof next.title === 'string' && heading) heading.textContent = next.title;
|
|
505
|
+
if (Array.isArray(next.items)) { list.replaceChildren(); buildItems(list, next.items, { role, onActivate: activate }); list.scrollTop = 0; }
|
|
506
|
+
if ('footer' in next) fillFooter(next.footer);
|
|
507
|
+
place();
|
|
508
|
+
const wanted = next.focus != null ? [...menu.querySelectorAll('.asst-dd-item')].find(n => n.dataset.id === String(next.focus) && !n.disabled) : null;
|
|
509
|
+
setFocused(menu, wanted || list.querySelector('.asst-dd-item[data-checked="1"]:not([disabled])') || focusables(menu)[0] || null);
|
|
510
|
+
};
|
|
511
|
+
return { el: menu, list, close: closeMenu, reposition: place, update };
|
|
385
512
|
}
|
|
386
513
|
|
|
387
514
|
/** Toggle helper for anchor buttons: closes when this anchor's menu is open. */
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Assistant — what the composer's model chip says
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
// The model as a short human name from its catalog label ("Opus 5.5 (1M
|
|
5
|
+
// context)" → "Opus 5.5", "Default (recommended) — Opus 5.5" → "Opus 5.5"),
|
|
6
|
+
// a context tag for a "[1m]" / "[extended]" variant ("1M"), and the forms the
|
|
7
|
+
// chip falls back to when the composer row has no room, longest first: the tag
|
|
8
|
+
// goes first, then the account cue, then the API badge, then the name shortens
|
|
9
|
+
// by whole words, to a form that still names this model alone when there is one
|
|
10
|
+
// ("Opus 5.5" → "Opus", "GPT-6-Luna" → "Luna"). Never a cut word.
|
|
11
|
+
// asst-brain-picker.js draws the first form that fits. DOM-free: node --test loads it.
|
|
12
|
+
|
|
13
|
+
const VARIANT = /\[(1m|extended)\]$/i;
|
|
14
|
+
const VERSION = /^v?\d+(?:\.\d+)*[a-z]?$/i;
|
|
15
|
+
|
|
16
|
+
function str(v) {
|
|
17
|
+
return v == null ? '' : String(v).trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The id without its provider ("anthropic/…"), legacy ":<window>" and variant suffix. */
|
|
21
|
+
function bareId(id) {
|
|
22
|
+
const tail = str(id).split('/').pop() || '';
|
|
23
|
+
return tail.replace(/:\d+$/, '').replace(VARIANT, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The model's short name: { name, raw }. The label's own words without what the
|
|
28
|
+
* chip says otherwise (parentheses: the context, "recommended"); after a dash, the
|
|
29
|
+
* model it runs as. No label (or the id itself): the bare id, `raw` (shown mono).
|
|
30
|
+
*/
|
|
31
|
+
export function modelChipName(label, id) {
|
|
32
|
+
let text = str(label);
|
|
33
|
+
if (text && text !== str(id)) {
|
|
34
|
+
const runsAs = text.split(/\s+[—–]\s+/).pop();
|
|
35
|
+
text = str(runsAs.replace(/\s*\([^)]*\)/g, '').replace(/\s+/g, ' '));
|
|
36
|
+
if (text) return { name: text, raw: false };
|
|
37
|
+
}
|
|
38
|
+
const name = bareId(id) || str(id);
|
|
39
|
+
return { name, raw: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** "1M", "400K": a context window in tokens, never rounded up. */
|
|
43
|
+
function compactTokens(n) {
|
|
44
|
+
const v = Number(n);
|
|
45
|
+
if (!Number.isFinite(v) || v <= 0) return '';
|
|
46
|
+
if (v >= 1e6) return `${Math.floor(v / 1e5) / 10}M`;
|
|
47
|
+
if (v >= 1e3) return `${Math.floor(v / 1e3)}K`;
|
|
48
|
+
return String(v);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What tells a context variant from its model ("opus[1m]" → "1M"): its window when
|
|
53
|
+
* the catalog knows it, else "1M" for "[1m]", else the label's own word ("extended").
|
|
54
|
+
* '' for a model that is not a variant.
|
|
55
|
+
*/
|
|
56
|
+
export function modelChipTag(id, { contextWindow = null, label = '' } = {}) {
|
|
57
|
+
const m = str(id).match(VARIANT);
|
|
58
|
+
if (!m) return '';
|
|
59
|
+
const tokens = compactTokens(contextWindow);
|
|
60
|
+
if (tokens) return tokens;
|
|
61
|
+
if (m[1].toLowerCase() === '1m') return '1M';
|
|
62
|
+
const note = str(label).match(/\(([^)]*)\)\s*$/);
|
|
63
|
+
return note ? str(note[1].replace(/\bcontext\b/i, '')) : '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// "GPT-6.1-Sol" → ['GPT', '6.1', 'Sol'] with the separators kept: whole words in, whole words out.
|
|
67
|
+
function parts(name) {
|
|
68
|
+
return str(name).match(/[^\s-]+|[\s-]+/g) || [];
|
|
69
|
+
}
|
|
70
|
+
const isSep = (p) => /^[\s-]+$/.test(p);
|
|
71
|
+
const wordsOf = (name) => parts(name).filter(p => !isSep(p)).map(w => w.toLowerCase());
|
|
72
|
+
|
|
73
|
+
/** Whether `words` run in that order, whole, somewhere in `other` ("opus" in "Opus 5.5"). */
|
|
74
|
+
function within(words, other) {
|
|
75
|
+
for (let i = 0; i + words.length <= other.length; i += 1) {
|
|
76
|
+
if (words.every((w, j) => other[i + j] === w)) return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The name's shorter forms, whole words only, longest first: `unique` still names this
|
|
83
|
+
* model alone among `names` (the short names of every model the provider lists: "Opus"
|
|
84
|
+
* for "Opus 5.5", "GPT-6.1" for "GPT-6.1-Sol", "Luna" for "GPT-6-Luna" when every
|
|
85
|
+
* "GPT-6" is another model too), `other` drops trailing words whatever they lose
|
|
86
|
+
* ("Sonnet" for "Sonnet 5.5" beside "Sonnet 5"; "GPT-6"). A form never starts with a
|
|
87
|
+
* version ("5.5"), and a suffix is tried only when no shorter prefix is unique.
|
|
88
|
+
*/
|
|
89
|
+
export function modelChipShortNames(name, names = []) {
|
|
90
|
+
const all = parts(name);
|
|
91
|
+
const own = wordsOf(name);
|
|
92
|
+
const others = [...new Set((Array.isArray(names) ? names : []).map(str).filter(n => n && n.toLowerCase() !== str(name).toLowerCase()))].map(wordsOf);
|
|
93
|
+
const unique = (form) => { const w = wordsOf(form); return !others.some(o => within(w, o)); };
|
|
94
|
+
const prefixes = [];
|
|
95
|
+
const suffixes = [];
|
|
96
|
+
for (let n = own.length - 1; n >= 1; n -= 1) {
|
|
97
|
+
let seen = 0;
|
|
98
|
+
const cut = all.findIndex(p => !isSep(p) && ++seen > n);
|
|
99
|
+
prefixes.push(all.slice(0, cut).join('').replace(/[\s-]+$/, ''));
|
|
100
|
+
let skip = own.length - n;
|
|
101
|
+
const from = all.findIndex(p => !isSep(p) && skip-- === 0);
|
|
102
|
+
if (!VERSION.test(all[from])) suffixes.push(all.slice(from).join(''));
|
|
103
|
+
}
|
|
104
|
+
const lossless = prefixes.filter(unique);
|
|
105
|
+
const picked = lossless.length ? lossless : suffixes.filter(unique);
|
|
106
|
+
return { unique: picked, other: prefixes.filter(p => !picked.includes(p)) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The chip's forms, longest first: [{ name, tag, cue, api }] (tag / cue / api: shown
|
|
111
|
+
* or not). `tag`, `cue` and `api` say whether the chip has one at all; `names` are the
|
|
112
|
+
* provider's models (modelChipShortNames). What goes, in order: the tag, the account
|
|
113
|
+
* cue, the API badge, then the name. A shorter name that still names this model alone
|
|
114
|
+
* loses nothing, so it is tried before the cue and before the badge go; one that
|
|
115
|
+
* loses something only after.
|
|
116
|
+
*/
|
|
117
|
+
export function modelChipForms({ name, names = [], tag = false, cue = false, api = false } = {}) {
|
|
118
|
+
const full = str(name);
|
|
119
|
+
const out = [];
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
const add = (n, t, c, a) => {
|
|
122
|
+
const form = { name: n, tag: !!(t && tag), cue: !!(c && cue), api: !!(a && api) };
|
|
123
|
+
const key = JSON.stringify(form);
|
|
124
|
+
if (!seen.has(key)) { seen.add(key); out.push(form); }
|
|
125
|
+
};
|
|
126
|
+
const short = full ? modelChipShortNames(full, names) : { unique: [], other: [] };
|
|
127
|
+
add(full, true, true, true);
|
|
128
|
+
add(full, false, true, true);
|
|
129
|
+
for (const u of short.unique) add(u, false, true, true);
|
|
130
|
+
add(full, false, false, true);
|
|
131
|
+
for (const u of short.unique) add(u, false, false, true);
|
|
132
|
+
add(full, false, false, false);
|
|
133
|
+
for (const n of [...short.unique, ...short.other]) add(n, false, false, false);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
@@ -64,7 +64,7 @@ import {
|
|
|
64
64
|
import {
|
|
65
65
|
closeAssistantSession, dispatchAssistantRun, escalateAssistantRun, focusAssistantRun, getAssistantRun,
|
|
66
66
|
fetchManageCatalog, getAssistantRunResult, getAssistantSession, getAssistantSessionUsage, getDesktopStatus, getRouting, killAllAssistantRuns, removeAssistantRun, clearFinishedAssistantRuns,
|
|
67
|
-
listAssistantSessions, patchAssistantSession, patchHiddenModels, stopAssistantRun, uploadAssistantAttachment,
|
|
67
|
+
listAssistantSessions, patchAssistantSession, patchHiddenModels, setAdminSandbox, stopAssistantRun, uploadAssistantAttachment,
|
|
68
68
|
} from './asst-api.js';
|
|
69
69
|
|
|
70
70
|
const ICON_SESSIONS = '<svg viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h12"/></svg>';
|
|
@@ -154,8 +154,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
154
154
|
engine: '',
|
|
155
155
|
sdkVersion: '',
|
|
156
156
|
routing: null, // server view { mode, effectiveMode, defaultMode, askBelow }
|
|
157
|
-
features: { routing: null, computer: null, testDensity: null, rename: null }, // null = unknown yet
|
|
157
|
+
features: { routing: null, computer: null, testDensity: null, rename: null, adminSandbox: null }, // null = unknown yet
|
|
158
158
|
testDensityEffective: null, // server view: the level in effect
|
|
159
|
+
adminSandbox: null, // server view: the global Admin sandbox setting (every agent)
|
|
159
160
|
brainCapabilities: null,
|
|
160
161
|
computerEffective: st.computerUse === true,
|
|
161
162
|
desktopSupported: null,
|
|
@@ -583,6 +584,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
583
584
|
// Test density sits right after the permission mode; hidden on a server that does not know it.
|
|
584
585
|
getTestDensity: () => testDensityView(),
|
|
585
586
|
onTestDensity: (level) => setTestDensity(level),
|
|
587
|
+
// The global Admin sandbox switch, in the permission menu; hidden on a server that does not serve it.
|
|
588
|
+
getAdminSandbox: () => adminSandboxView(),
|
|
589
|
+
onAdminSandbox: (enabled) => changeAdminSandbox(enabled),
|
|
586
590
|
// The registered projects arrived: their labels name the project in the fields and the "Fix … in" suggestion.
|
|
587
591
|
onProjects: () => { picker.render(); renderer.refreshEmpty(); },
|
|
588
592
|
});
|
|
@@ -950,7 +954,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
950
954
|
if (typeof meta.features.computer === 'boolean') st.features.computer = meta.features.computer;
|
|
951
955
|
if (typeof meta.features.testDensity === 'boolean') st.features.testDensity = meta.features.testDensity;
|
|
952
956
|
if (typeof meta.features.rename === 'boolean') st.features.rename = meta.features.rename;
|
|
957
|
+
if (typeof meta.features.adminSandbox === 'boolean') st.features.adminSandbox = meta.features.adminSandbox;
|
|
953
958
|
}
|
|
959
|
+
if (typeof meta.adminSandbox === 'boolean') st.adminSandbox = meta.adminSandbox;
|
|
954
960
|
if ('testDensityExplicit' in meta) st.testDensity = normalizeTestDensity(meta.testDensityExplicit, null);
|
|
955
961
|
if (meta.testDensity) st.testDensityEffective = normalizeTestDensity(meta.testDensity);
|
|
956
962
|
if (typeof meta.computerUse === 'boolean') st.computerEffective = meta.computerUse;
|
|
@@ -1066,6 +1072,31 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
1066
1072
|
if (!quiet) renderer.appendStatus(tt('assistant.status.testDensity', 'Test density: {level}', { level: testDensityLabel(next, t) }));
|
|
1067
1073
|
}
|
|
1068
1074
|
|
|
1075
|
+
/** The global Admin sandbox setting (true / false), or null while the server does not serve it. */
|
|
1076
|
+
function adminSandboxView() {
|
|
1077
|
+
if (st.features.adminSandbox !== true || typeof st.adminSandbox !== 'boolean') return null;
|
|
1078
|
+
return st.adminSandbox;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/** Every agent SynaBun starts follows it; every window hears the change (sync:agents:admin-sandbox-changed). */
|
|
1082
|
+
async function changeAdminSandbox(enabled) {
|
|
1083
|
+
const before = st.adminSandbox;
|
|
1084
|
+
st.adminSandbox = enabled === true;
|
|
1085
|
+
picker.render();
|
|
1086
|
+
try {
|
|
1087
|
+
const view = await setAdminSandbox(st.adminSandbox);
|
|
1088
|
+
if (typeof view?.adminSandbox === 'boolean') st.adminSandbox = view.adminSandbox;
|
|
1089
|
+
picker.render();
|
|
1090
|
+
renderer.appendStatus(st.adminSandbox
|
|
1091
|
+
? tt('assistant.adminSandbox.statusOn', 'Admin sandbox: on. On Windows, Codex may ask for administrator approval once per account to set up its sandbox.')
|
|
1092
|
+
: tt('assistant.adminSandbox.statusOff', 'Admin sandbox: off. No agent SynaBun starts asks for administrator approval for a sandbox.'));
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
st.adminSandbox = before;
|
|
1095
|
+
picker.render();
|
|
1096
|
+
toast(err?.message || String(err));
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1069
1100
|
/** Fresh tabs inherit the last-used test density (like the route mode). */
|
|
1070
1101
|
function applyLastUsedTestDensity() {
|
|
1071
1102
|
if (saved || st.features.testDensity !== true || st.testDensity != null) return;
|
|
@@ -1879,6 +1910,12 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
1879
1910
|
st.routing = { ...(st.routing || {}), defaultMode: def, effectiveMode: st.routeMode || def };
|
|
1880
1911
|
routeChip.render();
|
|
1881
1912
|
}));
|
|
1913
|
+
// The global Admin sandbox setting changed in some window (this one included): every panel shows it.
|
|
1914
|
+
unsubs.push(on('sync:agents:admin-sandbox-changed', (msg) => {
|
|
1915
|
+
if (typeof msg?.adminSandbox !== 'boolean' || msg.adminSandbox === st.adminSandbox) return;
|
|
1916
|
+
st.adminSandbox = msg.adminSandbox;
|
|
1917
|
+
picker.render();
|
|
1918
|
+
}));
|
|
1882
1919
|
unsubs.push(on('session:info', () => updateGuest()));
|
|
1883
1920
|
unsubs.push(on('permissions:changed', () => updateGuest()));
|
|
1884
1921
|
|