@agentprojectcontext/apx 1.25.0 → 1.27.2
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/package.json +42 -12
- package/skills/apx/SKILL.md +50 -119
- package/skills/apx-agency-agents/SKILL.md +141 -0
- package/skills/apx-agent/SKILL.md +100 -0
- package/skills/apx-mcp/SKILL.md +114 -0
- package/skills/apx-mcp-builder/SKILL.md +186 -0
- package/skills/apx-project/SKILL.md +100 -0
- package/skills/apx-routine/SKILL.md +138 -0
- package/skills/apx-runtime/SKILL.md +115 -0
- package/skills/apx-sessions/SKILL.md +281 -0
- package/skills/apx-skill-builder/SKILL.md +149 -0
- package/skills/apx-task/SKILL.md +95 -0
- package/skills/apx-telegram/SKILL.md +115 -0
- package/skills/apx-voice/SKILL.md +135 -0
- package/src/core/agent/constants.js +3 -0
- package/src/core/agent/ghost-guard.js +24 -0
- package/src/core/agent/index.js +35 -0
- package/src/core/agent/model-router.js +257 -0
- package/src/core/agent/prompt-builder.js +313 -0
- package/src/core/agent/prompts/channels/api.md +7 -0
- package/src/core/agent/prompts/channels/cli.md +6 -0
- package/src/core/agent/prompts/channels/code.md +20 -0
- package/src/core/agent/prompts/channels/deck.md +6 -0
- package/src/core/agent/prompts/channels/desktop.md +24 -0
- package/src/core/agent/prompts/channels/routine.md +9 -0
- package/src/core/agent/prompts/channels/telegram.md +9 -0
- package/src/core/agent/prompts/channels/terminal.md +16 -0
- package/src/core/agent/prompts/channels/web.md +7 -0
- package/src/core/agent/prompts/channels/web_sidebar.md +7 -0
- package/src/core/agent/prompts/modes/voice.md +4 -0
- package/src/core/agent/prompts/super-agent-base.md +42 -0
- package/src/core/agent/pseudo-tools.js +40 -0
- package/src/core/agent/retry.js +85 -0
- package/src/core/agent/run-agent.js +423 -0
- package/src/core/agent/self-memory.js +155 -0
- package/src/{daemon → core/agent}/tool-call-parser.js +83 -10
- package/src/core/agent/tools-overlap.js +66 -0
- package/src/core/apc-skill-sync.js +97 -0
- package/src/core/code-sessions-store.js +150 -0
- package/src/core/config.js +473 -11
- package/src/core/desktop/autostart.js +162 -0
- package/src/core/engines/_health.js +63 -0
- package/src/{daemon → core}/engines/anthropic.js +16 -0
- package/src/core/engines/gemini.js +168 -0
- package/src/core/engines/groq.js +8 -0
- package/src/{daemon → core}/engines/index.js +3 -1
- package/src/{daemon → core}/engines/mock.js +22 -0
- package/src/{daemon → core}/engines/ollama.js +35 -0
- package/src/core/engines/openai-compatible.js +145 -0
- package/src/core/engines/openai.js +8 -0
- package/src/core/engines/openrouter.js +8 -0
- package/src/core/git-baseline.js +147 -0
- package/src/core/identity.js +21 -0
- package/src/core/logging.js +1 -1
- package/src/core/mcp/index.js +14 -0
- package/src/{daemon/mcp-runner.js → core/mcp/runner.js} +18 -6
- package/src/core/mcp/sources.js +246 -0
- package/src/core/memory/active-threads.js +124 -0
- package/src/core/memory/broker.js +144 -0
- package/src/core/memory/compactor.js +186 -0
- package/src/core/memory/embed-engines/gemini.js +62 -0
- package/src/core/memory/embed-engines/index.js +148 -0
- package/src/core/memory/embed-engines/ollama.js +55 -0
- package/src/core/memory/embed-engines/openai.js +64 -0
- package/src/core/memory/embed-engines/tf.js +20 -0
- package/src/core/memory/embeddings.js +132 -0
- package/src/core/memory/index.js +161 -0
- package/src/core/memory/indexer.js +257 -0
- package/src/core/memory/store.js +231 -0
- package/src/core/messages-store.js +143 -25
- package/src/core/parser.js +78 -16
- package/src/core/scaffold.js +175 -79
- package/src/core/tasks-store.js +264 -0
- package/src/core/telegram-identity.js +126 -0
- package/src/core/tools/index.js +6 -0
- package/src/core/voice/engines/elevenlabs.js +96 -0
- package/src/core/voice/engines/gemini.js +148 -0
- package/src/core/voice/engines/index.js +144 -0
- package/src/core/voice/engines/mock.js +59 -0
- package/src/core/voice/engines/openai.js +82 -0
- package/src/core/voice/engines/piper.js +93 -0
- package/src/core/voice/index.js +3 -0
- package/src/core/voice/tts.js +89 -0
- package/src/{daemon → host/daemon}/apc-runtime-context.js +1 -1
- package/src/host/daemon/api/admin-config.js +159 -0
- package/src/host/daemon/api/admin.js +72 -0
- package/src/host/daemon/api/agents.js +284 -0
- package/src/host/daemon/api/artifacts.js +52 -0
- package/src/host/daemon/api/code.js +351 -0
- package/src/host/daemon/api/config.js +104 -0
- package/src/host/daemon/api/connections.js +42 -0
- package/src/host/daemon/api/conversations.js +161 -0
- package/src/host/daemon/api/deck.js +511 -0
- package/src/host/daemon/api/desktop.js +71 -0
- package/src/host/daemon/api/embeddings.js +65 -0
- package/src/host/daemon/api/engines.js +80 -0
- package/src/host/daemon/api/exec.js +193 -0
- package/src/host/daemon/api/health.js +10 -0
- package/src/host/daemon/api/identity.js +36 -0
- package/src/host/daemon/api/mcps.js +205 -0
- package/src/host/daemon/api/messages.js +83 -0
- package/src/host/daemon/api/pairing.js +194 -0
- package/src/host/daemon/api/plugins.js +19 -0
- package/src/host/daemon/api/projects.js +35 -0
- package/src/host/daemon/api/routines.js +84 -0
- package/src/host/daemon/api/run.js +55 -0
- package/src/host/daemon/api/runtimes.js +219 -0
- package/src/host/daemon/api/sessions-search.js +177 -0
- package/src/host/daemon/api/sessions.js +115 -0
- package/src/host/daemon/api/shared.js +217 -0
- package/src/host/daemon/api/super-agent.js +208 -0
- package/src/host/daemon/api/tasks.js +118 -0
- package/src/host/daemon/api/telegram.js +325 -0
- package/src/host/daemon/api/tools.js +21 -0
- package/src/host/daemon/api/top-level.js +131 -0
- package/src/host/daemon/api/transcribe.js +35 -0
- package/src/host/daemon/api/tts.js +44 -0
- package/src/host/daemon/api/voice.js +519 -0
- package/src/host/daemon/api/web.js +123 -0
- package/src/host/daemon/api.js +152 -0
- package/src/{daemon → host/daemon}/compact.js +1 -1
- package/src/{daemon → host/daemon}/db.js +19 -5
- package/src/{daemon/overlay-ws.js → host/daemon/desktop-ws.js} +7 -7
- package/src/host/daemon/engine-sessions.js +245 -0
- package/src/{daemon → host/daemon}/index.js +27 -10
- package/src/{daemon/plugins/overlay.js → host/daemon/plugins/desktop.js} +36 -28
- package/src/{daemon → host/daemon}/plugins/index.js +2 -2
- package/src/{daemon → host/daemon}/plugins/telegram.js +165 -33
- package/src/{daemon → host/daemon}/project-config.js +31 -7
- package/src/{daemon → host/daemon}/routines.js +27 -8
- package/src/{daemon → host/daemon}/skills-loader.js +4 -2
- package/src/{daemon → host/daemon}/smoke.js +9 -5
- package/src/{daemon → host/daemon}/super-agent-tools/helpers.js +1 -1
- package/src/host/daemon/super-agent-tools/index.js +157 -0
- package/src/{daemon → host/daemon}/super-agent-tools/registry-bridge.js +1 -1
- package/src/{daemon → host/daemon}/super-agent-tools/tools/add-project.js +2 -2
- package/src/{daemon → host/daemon}/super-agent-tools/tools/ask-questions.js +4 -0
- package/src/{daemon → host/daemon}/super-agent-tools/tools/call-agent.js +5 -2
- package/src/{daemon → host/daemon}/super-agent-tools/tools/call-runtime.js +117 -8
- package/src/host/daemon/super-agent-tools/tools/create-task.js +52 -0
- package/src/{daemon → host/daemon}/super-agent-tools/tools/import-agent.js +2 -3
- package/src/{daemon → host/daemon}/super-agent-tools/tools/list-agents.js +1 -1
- package/src/host/daemon/super-agent-tools/tools/list-tasks.js +52 -0
- package/src/{daemon → host/daemon}/super-agent-tools/tools/list-vault-agents.js +1 -1
- package/src/host/daemon/super-agent-tools/tools/read-self-memory.js +21 -0
- package/src/host/daemon/super-agent-tools/tools/remember.js +40 -0
- package/src/{daemon → host/daemon}/super-agent-tools/tools/search-messages.js +1 -1
- package/src/host/daemon/super-agent-tools/tools/search-sessions.js +134 -0
- package/src/{daemon → host/daemon}/super-agent-tools/tools/send-telegram.js +2 -2
- package/src/{daemon → host/daemon}/super-agent-tools/tools/set-identity.js +1 -1
- package/src/{daemon → host/daemon}/super-agent-tools/tools/set-permission-mode.js +1 -1
- package/src/{daemon → host/daemon}/super-agent-tools/tools/tail-messages.js +1 -1
- package/src/host/daemon/super-agent.js +129 -0
- package/src/host/daemon/token-store.js +118 -0
- package/src/host/daemon/tool-call-parser.js +2 -0
- package/src/{daemon → host/daemon}/transcription.js +1 -1
- package/src/{daemon → host/daemon}/wakeup.js +2 -2
- package/src/interfaces/cli/claude-permissions.js +33 -0
- package/src/{cli → interfaces/cli}/commands/agent.js +43 -8
- package/src/{cli → interfaces/cli}/commands/command.js +1 -1
- package/src/{cli → interfaces/cli}/commands/config.js +1 -1
- package/src/{cli → interfaces/cli}/commands/daemon.js +67 -0
- package/src/interfaces/cli/commands/desktop.js +335 -0
- package/src/interfaces/cli/commands/exec.js +92 -0
- package/src/{cli → interfaces/cli}/commands/identity.js +6 -63
- package/src/{cli → interfaces/cli}/commands/init.js +1 -1
- package/src/{cli → interfaces/cli}/commands/mcp.js +69 -10
- package/src/{cli → interfaces/cli}/commands/memory.js +2 -2
- package/src/interfaces/cli/commands/model.js +136 -0
- package/src/interfaces/cli/commands/pair.js +170 -0
- package/src/interfaces/cli/commands/project-config.js +131 -0
- package/src/{cli → interfaces/cli}/commands/project.js +1 -1
- package/src/{cli → interfaces/cli}/commands/search.js +1 -1
- package/src/interfaces/cli/commands/session.js +892 -0
- package/src/interfaces/cli/commands/sessions.js +997 -0
- package/src/{cli → interfaces/cli}/commands/setup.js +98 -4
- package/src/{cli → interfaces/cli}/commands/skills.js +117 -9
- package/src/{cli → interfaces/cli}/commands/status.js +9 -1
- package/src/{cli → interfaces/cli}/commands/sys.js +96 -17
- package/src/interfaces/cli/commands/task.js +179 -0
- package/src/interfaces/cli/commands/telegram.js +366 -0
- package/src/{cli → interfaces/cli}/commands/update.js +1 -1
- package/src/interfaces/cli/commands/voice.js +258 -0
- package/src/{cli → interfaces/cli}/http.js +6 -2
- package/src/{cli → interfaces/cli}/index.js +955 -63
- package/src/interfaces/cli/postinstall.js +34 -0
- package/src/interfaces/desktop/assets/app-icon-180.png +0 -0
- package/src/interfaces/desktop/assets/app-icon-32.png +0 -0
- package/src/interfaces/desktop/assets/app-icon.png +0 -0
- package/src/interfaces/desktop/assets/apx-logo.png +0 -0
- package/src/interfaces/desktop/assets/superagent.png +0 -0
- package/src/interfaces/desktop/assets/tray-icon.png +0 -0
- package/src/interfaces/desktop/index.html +18 -0
- package/src/interfaces/desktop/main.js +652 -0
- package/src/interfaces/desktop/preload.js +48 -0
- package/src/interfaces/desktop/renderer.js +1006 -0
- package/src/interfaces/desktop/style.css +400 -0
- package/src/{mcp → interfaces/mcp-server}/index.js +2 -2
- package/src/interfaces/tui/_shims/util-which.ts +53 -0
- package/src/{tui → interfaces/tui}/app.tsx +2 -2
- package/src/{tui → interfaces/tui}/component/prompt/index.tsx +4 -1
- package/src/{tui → interfaces/tui}/context/sdk-apx.tsx +84 -16
- package/src/interfaces/tui/context/sync-apx.tsx +398 -0
- package/src/interfaces/tui/routes/session/index.tsx +368 -0
- package/src/interfaces/tui/routes/session/message-actions.tsx +58 -0
- package/src/interfaces/tui/routes/session/sidebar-apx.tsx +114 -0
- package/src/{tui → interfaces/tui}/tsconfig.json +1 -0
- package/src/{tui → interfaces/tui}/util/clipboard.ts +1 -1
- package/src/interfaces/web/README.md +102 -0
- package/src/interfaces/web/coming-soon.html +65 -0
- package/src/interfaces/web/components.json +25 -0
- package/src/interfaces/web/dist/assets/index-BDUsA6L6.css +1 -0
- package/src/interfaces/web/dist/assets/index-CfWyjPBa.js +548 -0
- package/src/interfaces/web/dist/assets/index-CfWyjPBa.js.map +1 -0
- package/src/interfaces/web/dist/favicon/dark/android-chrome-192x192.png +0 -0
- package/src/interfaces/web/dist/favicon/dark/android-chrome-512x512.png +0 -0
- package/src/interfaces/web/dist/favicon/dark/apple-touch-icon.png +0 -0
- package/src/interfaces/web/dist/favicon/dark/favicon-16x16.png +0 -0
- package/src/interfaces/web/dist/favicon/dark/favicon-32x32.png +0 -0
- package/src/interfaces/web/dist/favicon/dark/favicon-48x48.png +0 -0
- package/src/interfaces/web/dist/favicon/dark/favicon.ico +0 -0
- package/src/interfaces/web/dist/favicon/dark/favicon.webp +0 -0
- package/src/interfaces/web/dist/favicon/dark/site.webmanifest +18 -0
- package/src/interfaces/web/dist/favicon/white/android-chrome-192x192.png +0 -0
- package/src/interfaces/web/dist/favicon/white/android-chrome-512x512.png +0 -0
- package/src/interfaces/web/dist/favicon/white/apple-touch-icon.png +0 -0
- package/src/interfaces/web/dist/favicon/white/favicon-16x16.png +0 -0
- package/src/interfaces/web/dist/favicon/white/favicon-32x32.png +0 -0
- package/src/interfaces/web/dist/favicon/white/favicon-48x48.png +0 -0
- package/src/interfaces/web/dist/favicon/white/favicon.ico +0 -0
- package/src/interfaces/web/dist/favicon/white/favicon.webp +0 -0
- package/src/interfaces/web/dist/favicon/white/site.webmanifest +18 -0
- package/src/interfaces/web/dist/index.html +27 -0
- package/src/interfaces/web/dist/logo/logo_dark.webp +0 -0
- package/src/interfaces/web/dist/logo/logo_only_dark.webp +0 -0
- package/src/interfaces/web/dist/logo/logo_only_white.webp +0 -0
- package/src/interfaces/web/dist/logo/logo_vertical_dark.webp +0 -0
- package/src/interfaces/web/dist/logo/logo_vertical_white.webp +0 -0
- package/src/interfaces/web/dist/logo/logo_white.webp +0 -0
- package/src/interfaces/web/dist/modules/superagent.png +0 -0
- package/src/interfaces/web/index.html +26 -0
- package/src/interfaces/web/package-lock.json +4253 -0
- package/src/interfaces/web/package.json +55 -0
- package/src/interfaces/web/playwright.config.ts +45 -0
- package/src/interfaces/web/pnpm-lock.yaml +2946 -0
- package/src/interfaces/web/public/favicon/dark/android-chrome-192x192.png +0 -0
- package/src/interfaces/web/public/favicon/dark/android-chrome-512x512.png +0 -0
- package/src/interfaces/web/public/favicon/dark/apple-touch-icon.png +0 -0
- package/src/interfaces/web/public/favicon/dark/favicon-16x16.png +0 -0
- package/src/interfaces/web/public/favicon/dark/favicon-32x32.png +0 -0
- package/src/interfaces/web/public/favicon/dark/favicon-48x48.png +0 -0
- package/src/interfaces/web/public/favicon/dark/favicon.ico +0 -0
- package/src/interfaces/web/public/favicon/dark/favicon.webp +0 -0
- package/src/interfaces/web/public/favicon/dark/site.webmanifest +18 -0
- package/src/interfaces/web/public/favicon/white/android-chrome-192x192.png +0 -0
- package/src/interfaces/web/public/favicon/white/android-chrome-512x512.png +0 -0
- package/src/interfaces/web/public/favicon/white/apple-touch-icon.png +0 -0
- package/src/interfaces/web/public/favicon/white/favicon-16x16.png +0 -0
- package/src/interfaces/web/public/favicon/white/favicon-32x32.png +0 -0
- package/src/interfaces/web/public/favicon/white/favicon-48x48.png +0 -0
- package/src/interfaces/web/public/favicon/white/favicon.ico +0 -0
- package/src/interfaces/web/public/favicon/white/favicon.webp +0 -0
- package/src/interfaces/web/public/favicon/white/site.webmanifest +18 -0
- package/src/interfaces/web/public/logo/logo_dark.webp +0 -0
- package/src/interfaces/web/public/logo/logo_only_dark.webp +0 -0
- package/src/interfaces/web/public/logo/logo_only_white.webp +0 -0
- package/src/interfaces/web/public/logo/logo_vertical_dark.webp +0 -0
- package/src/interfaces/web/public/logo/logo_vertical_white.webp +0 -0
- package/src/interfaces/web/public/logo/logo_white.webp +0 -0
- package/src/interfaces/web/public/modules/superagent.png +0 -0
- package/src/interfaces/web/src/App.tsx +199 -0
- package/src/interfaces/web/src/components/AddProjectDialog.tsx +121 -0
- package/src/interfaces/web/src/components/ModelCombobox.tsx +96 -0
- package/src/interfaces/web/src/components/RobyBubble.tsx +213 -0
- package/src/interfaces/web/src/components/Section.tsx +44 -0
- package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +97 -0
- package/src/interfaces/web/src/components/TelegramSendDialog.tsx +48 -0
- package/src/interfaces/web/src/components/Toast.tsx +84 -0
- package/src/interfaces/web/src/components/UiSelect.tsx +74 -0
- package/src/interfaces/web/src/components/chat/Composer.tsx +43 -0
- package/src/interfaces/web/src/components/chat/ContextBar.tsx +111 -0
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +95 -0
- package/src/interfaces/web/src/components/chat/MessageList.tsx +35 -0
- package/src/interfaces/web/src/components/chat/ModelPicker.tsx +145 -0
- package/src/interfaces/web/src/components/chat/ToolCall.tsx +141 -0
- package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +87 -0
- package/src/interfaces/web/src/components/code/CodeComposer.tsx +87 -0
- package/src/interfaces/web/src/components/code/CodeContextTab.tsx +83 -0
- package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +39 -0
- package/src/interfaces/web/src/components/code/CodeSessionList.tsx +97 -0
- package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +44 -0
- package/src/interfaces/web/src/components/code/CodeToolTrail.tsx +29 -0
- package/src/interfaces/web/src/components/code/DiffView.tsx +67 -0
- package/src/interfaces/web/src/components/common/Qr.tsx +27 -0
- package/src/interfaces/web/src/components/common/TabLayout.tsx +46 -0
- package/src/interfaces/web/src/components/common/TabNav.tsx +113 -0
- package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +202 -0
- package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +42 -0
- package/src/interfaces/web/src/components/config/global-config-sections.ts +60 -0
- package/src/interfaces/web/src/components/config/project-config-sections.ts +58 -0
- package/src/interfaces/web/src/components/deck/DaemonCard.tsx +58 -0
- package/src/interfaces/web/src/components/deck/DesktopGroup.tsx +33 -0
- package/src/interfaces/web/src/components/deck/WidgetRow.tsx +100 -0
- package/src/interfaces/web/src/components/layout/Logo.tsx +59 -0
- package/src/interfaces/web/src/components/layout/ProjectAvatar.tsx +116 -0
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +151 -0
- package/src/interfaces/web/src/components/settings/AdvancedPanel.tsx +45 -0
- package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +72 -0
- package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +232 -0
- package/src/interfaces/web/src/components/settings/DevicesPanel.tsx +60 -0
- package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +127 -0
- package/src/interfaces/web/src/components/settings/IdentityPanel.tsx +69 -0
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +226 -0
- package/src/interfaces/web/src/components/settings/PairDeviceDialog.tsx +175 -0
- package/src/interfaces/web/src/components/settings/SuperAgentPanel.tsx +93 -0
- package/src/interfaces/web/src/components/settings/TelegramChannelsPanel.tsx +90 -0
- package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +101 -0
- package/src/interfaces/web/src/components/settings/TelegramGlobalPanel.tsx +100 -0
- package/src/interfaces/web/src/components/settings/TelegramRolesPanel.tsx +108 -0
- package/src/interfaces/web/src/components/settings/TelegramSettingsTabs.tsx +55 -0
- package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +95 -0
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +405 -0
- package/src/interfaces/web/src/components/settings/providers/typeStyles.ts +155 -0
- package/src/interfaces/web/src/components/settings/providers/types.ts +26 -0
- package/src/interfaces/web/src/components/ui/accordion.tsx +72 -0
- package/src/interfaces/web/src/components/ui/alert-dialog.tsx +187 -0
- package/src/interfaces/web/src/components/ui/alert.tsx +76 -0
- package/src/interfaces/web/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/interfaces/web/src/components/ui/avatar.tsx +107 -0
- package/src/interfaces/web/src/components/ui/badge.tsx +52 -0
- package/src/interfaces/web/src/components/ui/breadcrumb.tsx +125 -0
- package/src/interfaces/web/src/components/ui/button-group.tsx +87 -0
- package/src/interfaces/web/src/components/ui/button.tsx +58 -0
- package/src/interfaces/web/src/components/ui/calendar.tsx +221 -0
- package/src/interfaces/web/src/components/ui/card.tsx +103 -0
- package/src/interfaces/web/src/components/ui/carousel.tsx +242 -0
- package/src/interfaces/web/src/components/ui/chart.tsx +371 -0
- package/src/interfaces/web/src/components/ui/chat-input.tsx +122 -0
- package/src/interfaces/web/src/components/ui/checkbox.tsx +29 -0
- package/src/interfaces/web/src/components/ui/collapsible.tsx +19 -0
- package/src/interfaces/web/src/components/ui/combobox.tsx +295 -0
- package/src/interfaces/web/src/components/ui/command.tsx +196 -0
- package/src/interfaces/web/src/components/ui/context-menu.tsx +271 -0
- package/src/interfaces/web/src/components/ui/dialog.tsx +158 -0
- package/src/interfaces/web/src/components/ui/direction.tsx +4 -0
- package/src/interfaces/web/src/components/ui/drawer.tsx +134 -0
- package/src/interfaces/web/src/components/ui/dropdown-menu.tsx +266 -0
- package/src/interfaces/web/src/components/ui/empty.tsx +104 -0
- package/src/interfaces/web/src/components/ui/field.tsx +236 -0
- package/src/interfaces/web/src/components/ui/hover-card.tsx +51 -0
- package/src/interfaces/web/src/components/ui/input-group.tsx +158 -0
- package/src/interfaces/web/src/components/ui/input-otp.tsx +85 -0
- package/src/interfaces/web/src/components/ui/input.tsx +20 -0
- package/src/interfaces/web/src/components/ui/item.tsx +201 -0
- package/src/interfaces/web/src/components/ui/kbd.tsx +26 -0
- package/src/interfaces/web/src/components/ui/label.tsx +20 -0
- package/src/interfaces/web/src/components/ui/menubar.tsx +280 -0
- package/src/interfaces/web/src/components/ui/native-select.tsx +61 -0
- package/src/interfaces/web/src/components/ui/navigation-menu.tsx +168 -0
- package/src/interfaces/web/src/components/ui/pagination.tsx +130 -0
- package/src/interfaces/web/src/components/ui/popover.tsx +88 -0
- package/src/interfaces/web/src/components/ui/progress.tsx +83 -0
- package/src/interfaces/web/src/components/ui/radio-group.tsx +36 -0
- package/src/interfaces/web/src/components/ui/resizable.tsx +50 -0
- package/src/interfaces/web/src/components/ui/scroll-area.tsx +53 -0
- package/src/interfaces/web/src/components/ui/select.tsx +201 -0
- package/src/interfaces/web/src/components/ui/separator.tsx +23 -0
- package/src/interfaces/web/src/components/ui/sheet.tsx +138 -0
- package/src/interfaces/web/src/components/ui/sidebar.tsx +723 -0
- package/src/interfaces/web/src/components/ui/skeleton.tsx +13 -0
- package/src/interfaces/web/src/components/ui/slider.tsx +52 -0
- package/src/interfaces/web/src/components/ui/sonner.tsx +49 -0
- package/src/interfaces/web/src/components/ui/spinner.tsx +10 -0
- package/src/interfaces/web/src/components/ui/switch.tsx +30 -0
- package/src/interfaces/web/src/components/ui/table.tsx +116 -0
- package/src/interfaces/web/src/components/ui/tabs.tsx +72 -0
- package/src/interfaces/web/src/components/ui/textarea.tsx +18 -0
- package/src/interfaces/web/src/components/ui/tip.tsx +21 -0
- package/src/interfaces/web/src/components/ui/toggle-group.tsx +87 -0
- package/src/interfaces/web/src/components/ui/toggle.tsx +45 -0
- package/src/interfaces/web/src/components/ui/tooltip.tsx +64 -0
- package/src/interfaces/web/src/components/ui.tsx +211 -0
- package/src/interfaces/web/src/components/voice/VoiceProviderList.tsx +197 -0
- package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +213 -0
- package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +72 -0
- package/src/interfaces/web/src/components/voice/VoiceTestCard.tsx +112 -0
- package/src/interfaces/web/src/components/voice/useTtsPlayer.ts +59 -0
- package/src/interfaces/web/src/constants/index.ts +91 -0
- package/src/interfaces/web/src/hooks/use-mobile.ts +19 -0
- package/src/interfaces/web/src/hooks/useChat.ts +276 -0
- package/src/interfaces/web/src/hooks/useDaemonStatus.ts +12 -0
- package/src/interfaces/web/src/hooks/useDevices.ts +12 -0
- package/src/interfaces/web/src/hooks/useEngines.ts +10 -0
- package/src/interfaces/web/src/hooks/useGlobalConfig.ts +24 -0
- package/src/interfaces/web/src/hooks/useIdentity.ts +16 -0
- package/src/interfaces/web/src/hooks/useProjects.ts +27 -0
- package/src/interfaces/web/src/hooks/useTelegram.ts +35 -0
- package/src/interfaces/web/src/hooks/useTheme.tsx +57 -0
- package/src/interfaces/web/src/hooks/useTokenBootstrap.ts +122 -0
- package/src/interfaces/web/src/i18n/en.ts +767 -0
- package/src/interfaces/web/src/i18n/es.ts +770 -0
- package/src/interfaces/web/src/i18n/index.ts +86 -0
- package/src/interfaces/web/src/lib/api/admin.ts +30 -0
- package/src/interfaces/web/src/lib/api/agents.ts +46 -0
- package/src/interfaces/web/src/lib/api/code.ts +122 -0
- package/src/interfaces/web/src/lib/api/conversations.ts +16 -0
- package/src/interfaces/web/src/lib/api/deck.ts +106 -0
- package/src/interfaces/web/src/lib/api/desktop.ts +54 -0
- package/src/interfaces/web/src/lib/api/embeddings.ts +44 -0
- package/src/interfaces/web/src/lib/api/engines.ts +17 -0
- package/src/interfaces/web/src/lib/api/filesystem.ts +12 -0
- package/src/interfaces/web/src/lib/api/health.ts +6 -0
- package/src/interfaces/web/src/lib/api/identity.ts +7 -0
- package/src/interfaces/web/src/lib/api/mcps.ts +29 -0
- package/src/interfaces/web/src/lib/api/messages.ts +24 -0
- package/src/interfaces/web/src/lib/api/projects.ts +29 -0
- package/src/interfaces/web/src/lib/api/routines.ts +14 -0
- package/src/interfaces/web/src/lib/api/sessions.ts +16 -0
- package/src/interfaces/web/src/lib/api/super_agent.ts +29 -0
- package/src/interfaces/web/src/lib/api/tasks.ts +19 -0
- package/src/interfaces/web/src/lib/api/telegram.ts +57 -0
- package/src/interfaces/web/src/lib/api/tools.ts +13 -0
- package/src/interfaces/web/src/lib/api/voice.ts +169 -0
- package/src/interfaces/web/src/lib/api.ts +48 -0
- package/src/interfaces/web/src/lib/cn.ts +6 -0
- package/src/interfaces/web/src/lib/code-context.ts +83 -0
- package/src/interfaces/web/src/lib/config-values.ts +29 -0
- package/src/interfaces/web/src/lib/device.ts +10 -0
- package/src/interfaces/web/src/lib/http.ts +104 -0
- package/src/interfaces/web/src/lib/secrets.ts +15 -0
- package/src/interfaces/web/src/lib/utils.ts +6 -0
- package/src/interfaces/web/src/main.tsx +16 -0
- package/src/interfaces/web/src/screens/ApxAdminScreen.tsx +174 -0
- package/src/interfaces/web/src/screens/PairingScreen.tsx +105 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +178 -0
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +111 -0
- package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +274 -0
- package/src/interfaces/web/src/screens/base/ComingSoon.tsx +16 -0
- package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +53 -0
- package/src/interfaces/web/src/screens/base/LogsTab.tsx +188 -0
- package/src/interfaces/web/src/screens/base/ModelsTab.tsx +13 -0
- package/src/interfaces/web/src/screens/base/SessionsTab.tsx +58 -0
- package/src/interfaces/web/src/screens/base/WorkspacesTab.tsx +49 -0
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +295 -0
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +173 -0
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +304 -0
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +174 -0
- package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +152 -0
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +455 -0
- package/src/interfaces/web/src/screens/project/AgentsTab.tsx +364 -0
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +198 -0
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +94 -0
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +149 -0
- package/src/interfaces/web/src/screens/project/MemoriesTab.tsx +134 -0
- package/src/interfaces/web/src/screens/project/Overview.tsx +37 -0
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +386 -0
- package/src/interfaces/web/src/screens/project/TasksTab.tsx +116 -0
- package/src/interfaces/web/src/screens/project/TelegramTab.tsx +126 -0
- package/src/interfaces/web/src/screens/project/ThreadsTab.tsx +100 -0
- package/src/interfaces/web/src/styles.css +128 -0
- package/src/interfaces/web/src/types/daemon.ts +289 -0
- package/src/interfaces/web/tailwind.config.js +53 -0
- package/src/interfaces/web/tsconfig.json +24 -0
- package/src/interfaces/web/vite.config.ts +50 -0
- package/src/cli/commands/exec.js +0 -56
- package/src/cli/commands/overlay.js +0 -253
- package/src/cli/commands/session.js +0 -395
- package/src/cli/commands/sessions.js +0 -517
- package/src/cli/commands/telegram.js +0 -77
- package/src/cli/postinstall.js +0 -75
- package/src/cli-ts/commands/agent.ts +0 -173
- package/src/cli-ts/commands/chat.ts +0 -119
- package/src/cli-ts/commands/daemon.ts +0 -112
- package/src/cli-ts/commands/exec.ts +0 -109
- package/src/cli-ts/commands/mcp.ts +0 -235
- package/src/cli-ts/commands/session.ts +0 -224
- package/src/cli-ts/commands/status.ts +0 -61
- package/src/cli-ts/http.ts +0 -36
- package/src/cli-ts/index.ts +0 -73
- package/src/cli-ts/ui.ts +0 -107
- package/src/daemon/api.js +0 -1558
- package/src/daemon/engines/gemini.js +0 -56
- package/src/daemon/engines/openai.js +0 -79
- package/src/daemon/mcp-sources.js +0 -114
- package/src/daemon/super-agent-tools/index.js +0 -84
- package/src/daemon/super-agent-tools.js +0 -1
- package/src/daemon/super-agent.js +0 -541
- package/src/overlay/index.html +0 -44
- package/src/overlay/main.js +0 -480
- package/src/overlay/preload.js +0 -34
- package/src/overlay/renderer.js +0 -371
- package/src/overlay/style.css +0 -250
- package/src/tui/context/sync-apx.tsx +0 -284
- package/src/tui/routes/session/index.tsx +0 -274
- package/src/tui/routes/session/sidebar-apx.tsx +0 -90
- /package/src/{daemon → core}/tools/browser.js +0 -0
- /package/src/{daemon → core}/tools/fetch.js +0 -0
- /package/src/{daemon → core}/tools/glob.js +0 -0
- /package/src/{daemon → core}/tools/grep.js +0 -0
- /package/src/{daemon → core}/tools/registry.js +0 -0
- /package/src/{daemon → core}/tools/search.js +0 -0
- /package/src/{daemon → host/daemon}/conversations.js +0 -0
- /package/src/{daemon → host/daemon}/env-detect.js +0 -0
- /package/src/{daemon → host/daemon}/runtimes/_spawn.js +0 -0
- /package/src/{daemon → host/daemon}/runtimes/aider.js +0 -0
- /package/src/{daemon → host/daemon}/runtimes/claude-code.js +0 -0
- /package/src/{daemon → host/daemon}/runtimes/codex.js +0 -0
- /package/src/{daemon → host/daemon}/runtimes/cursor-agent.js +0 -0
- /package/src/{daemon → host/daemon}/runtimes/gemini-cli.js +0 -0
- /package/src/{daemon → host/daemon}/runtimes/index.js +0 -0
- /package/src/{daemon → host/daemon}/runtimes/opencode.js +0 -0
- /package/src/{daemon → host/daemon}/runtimes/qwen-code.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/call-mcp.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/edit-file.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/list-files.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/list-mcps.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/list-projects.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/list-skills.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/load-skill.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/read-agent-memory.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/read-file.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/run-shell.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/search-files.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/transcribe-audio.js +0 -0
- /package/src/{daemon → host/daemon}/super-agent-tools/tools/write-file.js +0 -0
- /package/src/{daemon → host/daemon}/thinking.js +0 -0
- /package/src/{daemon → host/daemon}/whisper-server.py +0 -0
- /package/src/{daemon → host/daemon}/whisper-transcribe.py +0 -0
- /package/src/{cli → interfaces/cli}/commands/a2a.js +0 -0
- /package/src/{cli → interfaces/cli}/commands/artifact.js +0 -0
- /package/src/{cli → interfaces/cli}/commands/chat.js +0 -0
- /package/src/{cli → interfaces/cli}/commands/log.js +0 -0
- /package/src/{cli → interfaces/cli}/commands/messages.js +0 -0
- /package/src/{cli → interfaces/cli}/commands/plugins.js +0 -0
- /package/src/{cli → interfaces/cli}/commands/routine.js +0 -0
- /package/src/{cli → interfaces/cli}/commands/runtime.js +0 -0
- /package/src/{cli → interfaces/cli}/terminal-chat/renderer.js +0 -0
- /package/src/{overlay → interfaces/desktop}/package.json +0 -0
- /package/src/{tui → interfaces/tui}/_shims/cli-error.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/cli-logo.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/cli-ui.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/config-console-state.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/core-any.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/core-binary.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/core-flag.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/core-log.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/lsp-language.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/opencode-any.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/opencode-sdk-v2.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/plugin-tui.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/prompt-display.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/provider-provider.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/session-retry.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/session-schema.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/session-session.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/snapshot.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/tool-any.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/util-error.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/util-filesystem.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/util-format.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/util-iife.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/util-locale.ts +0 -0
- /package/src/{tui → interfaces/tui}/_shims/util-process.ts +0 -0
- /package/src/{tui → interfaces/tui}/asset/charge.wav +0 -0
- /package/src/{tui → interfaces/tui}/asset/pulse-a.wav +0 -0
- /package/src/{tui → interfaces/tui}/asset/pulse-b.wav +0 -0
- /package/src/{tui → interfaces/tui}/asset/pulse-c.wav +0 -0
- /package/src/{tui → interfaces/tui}/attach.ts +0 -0
- /package/src/{tui → interfaces/tui}/component/bg-pulse-render.ts +0 -0
- /package/src/{tui → interfaces/tui}/component/bg-pulse.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/border.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-agent.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-console-org.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-mcp.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-model.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-provider.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-retry-action.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-session-delete-failed.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-session-list.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-session-rename.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-skill.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-stash.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-status.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-tag.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-theme-list.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-variant.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-workspace-create.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-workspace-file-changes.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/dialog-workspace-unavailable.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/error-component.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/logo.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/plugin-route-missing.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/prompt/autocomplete.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/prompt/cwd.ts +0 -0
- /package/src/{tui → interfaces/tui}/component/prompt/frecency.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/prompt/history.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/prompt/part.ts +0 -0
- /package/src/{tui → interfaces/tui}/component/prompt/stash.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/prompt/traits.ts +0 -0
- /package/src/{tui → interfaces/tui}/component/spinner.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/startup-loading.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/todo-item.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/use-connected.tsx +0 -0
- /package/src/{tui → interfaces/tui}/component/workspace-label.tsx +0 -0
- /package/src/{tui → interfaces/tui}/config/cwd.ts +0 -0
- /package/src/{tui → interfaces/tui}/config/keybind.ts +0 -0
- /package/src/{tui → interfaces/tui}/config/tui-migrate.ts +0 -0
- /package/src/{tui → interfaces/tui}/config/tui-schema.ts +0 -0
- /package/src/{tui → interfaces/tui}/config/tui.ts +0 -0
- /package/src/{tui → interfaces/tui}/context/aggregate-failures.ts +0 -0
- /package/src/{tui → interfaces/tui}/context/args.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/command-palette.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/directory.ts +0 -0
- /package/src/{tui → interfaces/tui}/context/editor-zed.ts +0 -0
- /package/src/{tui → interfaces/tui}/context/editor.ts +0 -0
- /package/src/{tui → interfaces/tui}/context/event-apx.ts +0 -0
- /package/src/{tui → interfaces/tui}/context/event.ts +0 -0
- /package/src/{tui → interfaces/tui}/context/exit.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/helper.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/kv.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/local.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/path-format.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/project-apx.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/project.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/prompt.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/route.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/sdk.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/sync-v2.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/sync.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/aura.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/ayu.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/carbonfox.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/catppuccin-frappe.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/catppuccin-macchiato.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/catppuccin.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/cobalt2.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/cursor.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/dracula.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/everforest.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/flexoki.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/github.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/gruvbox.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/kanagawa.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/lucent-orng.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/material.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/matrix.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/mercury.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/monokai.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/nightowl.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/nord.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/one-dark.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/opencode.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/orng.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/osaka-jade.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/palenight.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/rosepine.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/solarized.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/synthwave84.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/tokyonight.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/vercel.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/vesper.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme/zenburn.json +0 -0
- /package/src/{tui → interfaces/tui}/context/theme.tsx +0 -0
- /package/src/{tui → interfaces/tui}/context/tui-config.tsx +0 -0
- /package/src/{tui → interfaces/tui}/event.ts +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/home/footer.tsx +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/home/tips-view.tsx +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/home/tips.tsx +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/sidebar/context.tsx +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/sidebar/files.tsx +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/sidebar/footer.tsx +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/sidebar/lsp.tsx +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/sidebar/mcp.tsx +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/sidebar/todo.tsx +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/system/plugins.tsx +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/system/session-v2.tsx +0 -0
- /package/src/{tui → interfaces/tui}/feature-plugins/system/which-key.tsx +0 -0
- /package/src/{tui → interfaces/tui}/keymap.tsx +0 -0
- /package/src/{tui → interfaces/tui}/layer.ts +0 -0
- /package/src/{tui → interfaces/tui}/plugin/api.tsx +0 -0
- /package/src/{tui → interfaces/tui}/plugin/command-shim.ts +0 -0
- /package/src/{tui → interfaces/tui}/plugin/internal.ts +0 -0
- /package/src/{tui → interfaces/tui}/plugin/runtime.ts +0 -0
- /package/src/{tui → interfaces/tui}/plugin/slots.tsx +0 -0
- /package/src/{tui → interfaces/tui}/routes/home.tsx +0 -0
- /package/src/{tui → interfaces/tui}/routes/session/dialog-fork-from-timeline.tsx +0 -0
- /package/src/{tui → interfaces/tui}/routes/session/dialog-message.tsx +0 -0
- /package/src/{tui → interfaces/tui}/routes/session/dialog-subagent.tsx +0 -0
- /package/src/{tui → interfaces/tui}/routes/session/dialog-timeline.tsx +0 -0
- /package/src/{tui → interfaces/tui}/routes/session/footer.tsx +0 -0
- /package/src/{tui → interfaces/tui}/routes/session/permission.tsx +0 -0
- /package/src/{tui → interfaces/tui}/routes/session/question.tsx +0 -0
- /package/src/{tui → interfaces/tui}/routes/session/sidebar.tsx +0 -0
- /package/src/{tui → interfaces/tui}/routes/session/subagent-footer.tsx +0 -0
- /package/src/{tui → interfaces/tui}/run.ts +0 -0
- /package/src/{tui → interfaces/tui}/thread.ts +0 -0
- /package/src/{tui → interfaces/tui}/ui/dialog-alert.tsx +0 -0
- /package/src/{tui → interfaces/tui}/ui/dialog-confirm.tsx +0 -0
- /package/src/{tui → interfaces/tui}/ui/dialog-export-options.tsx +0 -0
- /package/src/{tui → interfaces/tui}/ui/dialog-help.tsx +0 -0
- /package/src/{tui → interfaces/tui}/ui/dialog-prompt.tsx +0 -0
- /package/src/{tui → interfaces/tui}/ui/dialog-select.tsx +0 -0
- /package/src/{tui → interfaces/tui}/ui/dialog.tsx +0 -0
- /package/src/{tui → interfaces/tui}/ui/link.tsx +0 -0
- /package/src/{tui → interfaces/tui}/ui/spinner.ts +0 -0
- /package/src/{tui → interfaces/tui}/ui/toast.tsx +0 -0
- /package/src/{tui → interfaces/tui}/util/editor.ts +0 -0
- /package/src/{tui → interfaces/tui}/util/model.ts +0 -0
- /package/src/{tui → interfaces/tui}/util/provider-origin.ts +0 -0
- /package/src/{tui → interfaces/tui}/util/revert-diff.ts +0 -0
- /package/src/{tui → interfaces/tui}/util/scroll.ts +0 -0
- /package/src/{tui → interfaces/tui}/util/selection.ts +0 -0
- /package/src/{tui → interfaces/tui}/util/signal.ts +0 -0
- /package/src/{tui → interfaces/tui}/util/sound.ts +0 -0
- /package/src/{tui → interfaces/tui}/util/transcript.ts +0 -0
- /package/src/{tui → interfaces/tui}/validate-session.ts +0 -0
- /package/src/{tui → interfaces/tui}/win32.ts +0 -0
- /package/src/{tui → interfaces/tui}/worker.ts +0 -0
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
// APX Desktop renderer — vanilla port of the v2 floating-capsule design.
|
|
2
|
+
//
|
|
3
|
+
// The capsule is always visible (a Siri/Spotlight-style bar). When there is
|
|
4
|
+
// a conversation in flight (or any past turn) a glass card appears below it
|
|
5
|
+
// with the transcript, and a session bar below that. State machine:
|
|
6
|
+
//
|
|
7
|
+
// idle → input ready, mic button, no live wave
|
|
8
|
+
// listening → mic recording, capsule shows live wave + cancel/send
|
|
9
|
+
// transcribing → blob being decoded, status "Transcribiendo…"
|
|
10
|
+
// thinking → super-agent producing tokens, status "Pensando…"
|
|
11
|
+
// speaking → TTS playing back, status "Superagente está hablando…"
|
|
12
|
+
//
|
|
13
|
+
// MediaRecorder webm chunks are buffered and the CUMULATIVE blob is sent on
|
|
14
|
+
// every tick (live partial) and again on stop (authoritative) — single chunks
|
|
15
|
+
// lack the EBML header and are undecodable on their own.
|
|
16
|
+
|
|
17
|
+
(() => {
|
|
18
|
+
"use strict";
|
|
19
|
+
|
|
20
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
21
|
+
let mode = "idle"; // idle | listening | transcribing | thinking | speaking
|
|
22
|
+
let messages = []; // [{id, role:'user'|'agent', text, t, via, dur?, audio?}]
|
|
23
|
+
let nextId = 1;
|
|
24
|
+
let pendingUserText = ""; // live partial during transcribing
|
|
25
|
+
let isCancelled = false;
|
|
26
|
+
|
|
27
|
+
let mediaRecorder = null;
|
|
28
|
+
let audioStream = null;
|
|
29
|
+
let recordedChunks = [];
|
|
30
|
+
let recorderMime = "";
|
|
31
|
+
let recorderFormat = "webm";
|
|
32
|
+
let liveBusy = false;
|
|
33
|
+
|
|
34
|
+
// Web Audio analyser — drives the live capsule wave from real mic amplitude
|
|
35
|
+
let audioCtx = null;
|
|
36
|
+
let analyser = null;
|
|
37
|
+
let freqData = null;
|
|
38
|
+
let waveRaf = null;
|
|
39
|
+
|
|
40
|
+
let streamingAgentEntry = null; // { id, role:'agent', el, ... } during thinking/speaking
|
|
41
|
+
let toolPillsByName = {}; // active tool pills inside the streaming bubble row
|
|
42
|
+
let ttsAudio = null; // <audio> playing the agent reply
|
|
43
|
+
|
|
44
|
+
let history = []; // [{role:'user'|'assistant', content}] sent to daemon for context
|
|
45
|
+
let theme = "light";
|
|
46
|
+
let position = "right";
|
|
47
|
+
let agentName = "Superagente"; // overwritten from config on first render
|
|
48
|
+
|
|
49
|
+
// Guard so a duplicate `done` event from the daemon never spawns a second
|
|
50
|
+
// requestTts / second finalize on the same in-flight bubble.
|
|
51
|
+
let doneHandled = false;
|
|
52
|
+
let ttsTimer = null;
|
|
53
|
+
// Which agent turn (by message id) is waiting for its TTS audio to attach.
|
|
54
|
+
// We finalize the bubble immediately on `done`, then post-attach the
|
|
55
|
+
// scrubber when (or if) tts-ready arrives.
|
|
56
|
+
let pendingTtsTurnId = null;
|
|
57
|
+
|
|
58
|
+
// ── Inline SVG icons (mirrors the design's I.* set) ──────────────────────
|
|
59
|
+
const SVG = (path, attrs = {}) => {
|
|
60
|
+
const a = Object.entries(attrs).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
61
|
+
return `<svg ${a} fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${path}</svg>`;
|
|
62
|
+
};
|
|
63
|
+
const ICON = {
|
|
64
|
+
mic: () => SVG('<rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/>', { width: 18, height: 18, viewBox: "0 0 24 24" }),
|
|
65
|
+
text: () => SVG('<path d="M5 7h14M5 12h14M5 17h9"/>', { width: 17, height: 17, viewBox: "0 0 24 24" }),
|
|
66
|
+
send: () => SVG('<path d="M5 12h13M12 5l7 7-7 7"/>', { width: 18, height: 18, viewBox: "0 0 24 24" }),
|
|
67
|
+
x: () => SVG('<path d="M6 6l12 12M18 6L6 18"/>', { width: 17, height: 17, viewBox: "0 0 24 24", "stroke-width": "1.9" }),
|
|
68
|
+
close: () => SVG('<path d="M6 6l12 12M18 6L6 18"/>', { width: 15, height: 15, viewBox: "0 0 24 24" }),
|
|
69
|
+
plus: () => SVG('<path d="M12 5v14M5 12h14"/>', { width: 14, height: 14, viewBox: "0 0 24 24", "stroke-width": "1.9" }),
|
|
70
|
+
person: () => SVG('<circle cx="12" cy="8" r="3.6"/><path d="M5.5 20c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6"/>', { width: 12, height: 12, viewBox: "0 0 24 24" }),
|
|
71
|
+
refresh:() => SVG('<path d="M3 12a9 9 0 0 1 15.5-6.2L21 8M21 3v5h-5M21 12a9 9 0 0 1-15.5 6.2L3 16M3 21v-5h5"/>', { width: 13, height: 13, viewBox: "0 0 24 24", "stroke-width": "1.9" }),
|
|
72
|
+
copy: () => SVG('<rect x="9" y="9" width="11" height="11" rx="2.5"/><path d="M5 15V5a2 2 0 0 1 2-2h8"/>', { width: 13, height: 13, viewBox: "0 0 24 24" }),
|
|
73
|
+
check: () => SVG('<path d="M5 12l4.5 4.5L19 7"/>', { width: 13, height: 13, viewBox: "0 0 24 24", "stroke-width": "2" }),
|
|
74
|
+
play: () => `<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5.2v13.6c0 .8.9 1.3 1.6.9l10.5-6.8c.6-.4.6-1.3 0-1.7L9.6 4.3C8.9 3.9 8 4.4 8 5.2z"/></svg>`,
|
|
75
|
+
pause: () => `<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.3"/><rect x="13.5" y="5" width="4" height="14" rx="1.3"/></svg>`,
|
|
76
|
+
stop: () => `<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="3"/></svg>`,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ── DOM scaffolding (built once) ─────────────────────────────────────────
|
|
80
|
+
const $root = document.getElementById("root");
|
|
81
|
+
const $connBadge = document.getElementById("conn-badge");
|
|
82
|
+
|
|
83
|
+
$root.className = "float-root enter pos-right";
|
|
84
|
+
$root.innerHTML = `
|
|
85
|
+
<div class="cap" id="cap">
|
|
86
|
+
<div class="cap-badge" id="cap-badge" style="display:none">
|
|
87
|
+
<span class="g mic" id="badge-mic"></span>
|
|
88
|
+
<span class="g txt" id="badge-txt"></span>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="center" id="cap-center"></div>
|
|
91
|
+
<div class="cap-actions" id="cap-actions"></div>
|
|
92
|
+
</div>
|
|
93
|
+
<div id="caption-slot"></div>
|
|
94
|
+
<div id="conv-slot"></div>
|
|
95
|
+
<div id="session-slot"></div>
|
|
96
|
+
`;
|
|
97
|
+
const $cap = $root.querySelector("#cap");
|
|
98
|
+
const $capBadge = $root.querySelector("#cap-badge");
|
|
99
|
+
const $badgeMic = $root.querySelector("#badge-mic");
|
|
100
|
+
const $badgeTxt = $root.querySelector("#badge-txt");
|
|
101
|
+
const $capCenter = $root.querySelector("#cap-center");
|
|
102
|
+
const $capActions = $root.querySelector("#cap-actions");
|
|
103
|
+
const $captionSlot = $root.querySelector("#caption-slot");
|
|
104
|
+
const $convSlot = $root.querySelector("#conv-slot");
|
|
105
|
+
const $sessionSlot = $root.querySelector("#session-slot");
|
|
106
|
+
|
|
107
|
+
$badgeMic.innerHTML = ICON.mic();
|
|
108
|
+
$badgeTxt.innerHTML = ICON.text();
|
|
109
|
+
$capActions.style.cssText = "display:flex; align-items:center; gap:6px;";
|
|
110
|
+
|
|
111
|
+
// ── Initial config from main (theme, position, shortcut, agent name) ─────
|
|
112
|
+
// Don't render() before this resolves — otherwise the first paint creates
|
|
113
|
+
// the input with the default "Hablá o escribí a Superagente…" placeholder
|
|
114
|
+
// and the render() guard ("if (!existingInput)") never recreates it, so
|
|
115
|
+
// the agent name stays wrong until the user changes mode.
|
|
116
|
+
let configReady = false;
|
|
117
|
+
Promise.all([
|
|
118
|
+
window.apx?.getTheme?.() ?? "light",
|
|
119
|
+
window.apx?.getPosition?.() ?? "right",
|
|
120
|
+
window.apx?.getShortcut?.() ?? "CommandOrControl+G",
|
|
121
|
+
window.apx?.getAgentName?.() ?? "Superagente",
|
|
122
|
+
]).then(([th, pos, shortcut, name]) => {
|
|
123
|
+
theme = th || "light";
|
|
124
|
+
position = pos || "right";
|
|
125
|
+
agentName = (name && String(name).trim()) || "Superagente";
|
|
126
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
127
|
+
setPosition(position);
|
|
128
|
+
initialCaption(shortcut);
|
|
129
|
+
configReady = true;
|
|
130
|
+
// If render() already fired the bootstrap paint (because the IPC was
|
|
131
|
+
// slow), the existing input has the stale placeholder. Patch it in
|
|
132
|
+
// place so the user sees the real agent name on the very first frame
|
|
133
|
+
// they can interact with.
|
|
134
|
+
const input = $capCenter.querySelector("input");
|
|
135
|
+
if (input) input.placeholder = `Hablá o escribí a ${agentName}…`;
|
|
136
|
+
render();
|
|
137
|
+
}).catch(() => {
|
|
138
|
+
document.documentElement.setAttribute("data-theme", "light");
|
|
139
|
+
setPosition("right");
|
|
140
|
+
initialCaption("CommandOrControl+G");
|
|
141
|
+
configReady = true;
|
|
142
|
+
render();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
function setPosition(p) {
|
|
146
|
+
$root.classList.remove("pos-left", "pos-center", "pos-right");
|
|
147
|
+
$root.classList.add("pos-" + p);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatShortcut(s) {
|
|
151
|
+
if (!s) return "⌘G";
|
|
152
|
+
const isMac = (window.apx?.platform || (navigator.platform || "").toLowerCase()).indexOf("mac") >= 0
|
|
153
|
+
|| (window.apx?.platform === "darwin");
|
|
154
|
+
return s
|
|
155
|
+
.replace("CommandOrControl", isMac ? "⌘" : "Ctrl")
|
|
156
|
+
.replace("Command", "⌘")
|
|
157
|
+
.replace("Control", "Ctrl")
|
|
158
|
+
.replace("Shift", "⇧")
|
|
159
|
+
.replace("Option", "⌥")
|
|
160
|
+
.replace("Alt", "⌥")
|
|
161
|
+
.replace(/\+/g, "");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function initialCaption(shortcut) {
|
|
165
|
+
const sc = formatShortcut(shortcut);
|
|
166
|
+
$captionSlot.innerHTML = `
|
|
167
|
+
<div class="caption">Mantené <span class="kbd">${sc}</span> para hablar
|
|
168
|
+
<span class="kbd">⌥ /</span> para escribir</div>
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Render: capsule center + actions vary by mode ────────────────────────
|
|
173
|
+
//
|
|
174
|
+
// CRITICAL: the idle <input> must NOT be re-created on every keystroke,
|
|
175
|
+
// or it loses focus. We keep the input element across re-renders and only
|
|
176
|
+
// tear it down when mode changes. Badge + right-side actions rebuild
|
|
177
|
+
// freely (they have no focus state to preserve).
|
|
178
|
+
function render() {
|
|
179
|
+
$cap.classList.toggle("listening", mode === "listening");
|
|
180
|
+
$cap.classList.toggle("busy", mode === "transcribing" || mode === "thinking" || mode === "speaking");
|
|
181
|
+
const existingInput = $capCenter.querySelector("input");
|
|
182
|
+
const currentInputText = existingInput ? existingInput.value : "";
|
|
183
|
+
|
|
184
|
+
// badge visibility: shown while typing or listening
|
|
185
|
+
const typing = mode === "idle" && currentInputText.trim() !== "";
|
|
186
|
+
const showBadge = typing || mode === "listening";
|
|
187
|
+
$capBadge.style.display = showBadge ? "" : "none";
|
|
188
|
+
if (showBadge) {
|
|
189
|
+
$badgeMic.classList.toggle("show", !typing);
|
|
190
|
+
$badgeMic.classList.toggle("hide", typing);
|
|
191
|
+
$badgeTxt.classList.toggle("show", typing);
|
|
192
|
+
$badgeTxt.classList.toggle("hide", !typing);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// center — only rebuild when there's a real change
|
|
196
|
+
if (mode === "idle") {
|
|
197
|
+
if (!existingInput) {
|
|
198
|
+
// Mode just transitioned to idle (or first render). Create the input
|
|
199
|
+
// once; subsequent renders will hit the `existingInput` branch and
|
|
200
|
+
// leave focus/selection alone.
|
|
201
|
+
$capCenter.innerHTML = "";
|
|
202
|
+
const el = document.createElement("input");
|
|
203
|
+
el.type = "text";
|
|
204
|
+
el.placeholder = `Hablá o escribí a ${agentName}…`;
|
|
205
|
+
el.addEventListener("input", () => render());
|
|
206
|
+
el.addEventListener("keydown", (e) => {
|
|
207
|
+
if (e.key === "Enter" && el.value.trim()) {
|
|
208
|
+
const text = el.value.trim();
|
|
209
|
+
el.value = "";
|
|
210
|
+
sendText(text);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
$capCenter.appendChild(el);
|
|
214
|
+
if (window._focusOnNext) {
|
|
215
|
+
window._focusOnNext = false;
|
|
216
|
+
setTimeout(() => el.focus(), 30);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// else: input already there → leave it alone (preserves focus + caret)
|
|
220
|
+
} else if (mode === "listening") {
|
|
221
|
+
// Only rebuild the wave if it's not already there (avoids restarting
|
|
222
|
+
// CSS animations / Web Audio binding every render).
|
|
223
|
+
let wave = $capCenter.querySelector(".cap-wave");
|
|
224
|
+
if (!wave) {
|
|
225
|
+
$capCenter.innerHTML = "";
|
|
226
|
+
wave = document.createElement("div");
|
|
227
|
+
wave.className = "cap-wave reactive"; // JS drives the bar heights from analyser
|
|
228
|
+
for (let i = 0; i < 26; i++) {
|
|
229
|
+
const b = document.createElement("i");
|
|
230
|
+
b.style.height = "4px";
|
|
231
|
+
wave.appendChild(b);
|
|
232
|
+
}
|
|
233
|
+
$capCenter.appendChild(wave);
|
|
234
|
+
}
|
|
235
|
+
} else if (mode === "transcribing" || mode === "thinking" || mode === "speaking") {
|
|
236
|
+
// Only swap innerHTML when the rendered mode actually changes — keeps
|
|
237
|
+
// the shimmer/dots animations from restarting on every render() call.
|
|
238
|
+
if ($capCenter.dataset.mode !== mode) {
|
|
239
|
+
$capCenter.dataset.mode = mode;
|
|
240
|
+
if (mode === "transcribing") {
|
|
241
|
+
$capCenter.innerHTML = `<span class="status"><span class="dots"><i></i><i></i><i></i></span><span class="shimmer">Transcribiendo…</span></span>`;
|
|
242
|
+
} else if (mode === "thinking") {
|
|
243
|
+
$capCenter.innerHTML = `<span class="status"><span class="dots"><i></i><i></i><i></i></span><span class="shimmer">Pensando…</span></span>`;
|
|
244
|
+
} else if (mode === "speaking") {
|
|
245
|
+
$capCenter.innerHTML = `<span class="status"><img class="sa-glyph" src="assets/superagent.png" alt=""/><span class="shimmer">${escapeHtml(agentName)} está hablando…</span></span>`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Clear data-mode when we're back to idle/listening so a future busy mode
|
|
250
|
+
// re-renders correctly.
|
|
251
|
+
if (mode === "idle" || mode === "listening") $capCenter.dataset.mode = "";
|
|
252
|
+
|
|
253
|
+
// actions
|
|
254
|
+
$capActions.innerHTML = "";
|
|
255
|
+
const addBtn = (cls, label, icon, onClick) => {
|
|
256
|
+
const b = document.createElement("button");
|
|
257
|
+
b.className = "act" + (cls ? " " + cls : "");
|
|
258
|
+
b.setAttribute("aria-label", label);
|
|
259
|
+
b.title = label;
|
|
260
|
+
b.innerHTML = icon;
|
|
261
|
+
b.addEventListener("click", onClick);
|
|
262
|
+
$capActions.appendChild(b);
|
|
263
|
+
return b;
|
|
264
|
+
};
|
|
265
|
+
if (mode === "idle") {
|
|
266
|
+
if (currentInputText.trim()) {
|
|
267
|
+
addBtn("", "Enviar", ICON.send(), () => {
|
|
268
|
+
const text = $capCenter.querySelector("input")?.value.trim();
|
|
269
|
+
if (text) { $capCenter.querySelector("input").value = ""; sendText(text); }
|
|
270
|
+
});
|
|
271
|
+
} else {
|
|
272
|
+
addBtn("", "Hablar", ICON.mic(), () => startListening());
|
|
273
|
+
}
|
|
274
|
+
} else if (mode === "listening") {
|
|
275
|
+
addBtn("ghost", "Cancelar", ICON.x(), () => cancel());
|
|
276
|
+
addBtn("", "Enviar", ICON.send(), () => stopListening(/* commit */ true));
|
|
277
|
+
} else if (mode === "transcribing") {
|
|
278
|
+
addBtn("ghost", "Cancelar", ICON.x(), () => cancel());
|
|
279
|
+
} else if (mode === "thinking") {
|
|
280
|
+
addBtn("ghost", "Cancelar", ICON.x(), () => cancel());
|
|
281
|
+
} else if (mode === "speaking") {
|
|
282
|
+
addBtn("ghost", "Detener", ICON.stop(), () => stopSpeaking());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// caption visible only when idle AND no messages yet
|
|
286
|
+
$captionSlot.style.display = (mode === "idle" && messages.length === 0) ? "" : "none";
|
|
287
|
+
|
|
288
|
+
// session bar visible when there are messages
|
|
289
|
+
if (messages.length > 0 || mode !== "idle") {
|
|
290
|
+
renderSessionBar();
|
|
291
|
+
} else {
|
|
292
|
+
$sessionSlot.innerHTML = "";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// conv card visible when there's any content
|
|
296
|
+
const wantConv = messages.length > 0 || mode === "transcribing" || mode === "thinking" || mode === "speaking";
|
|
297
|
+
if (wantConv) ensureConv();
|
|
298
|
+
else $convSlot.innerHTML = "";
|
|
299
|
+
|
|
300
|
+
requestWindowResize();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function renderSessionBar() {
|
|
304
|
+
if ($sessionSlot.querySelector(".session-bar")) return; // keep DOM stable
|
|
305
|
+
$sessionSlot.innerHTML = `
|
|
306
|
+
<div class="session-bar">
|
|
307
|
+
<button class="sbtn new" id="btn-new"><span class="ic">${ICON.plus()}</span> Nueva sesión</button>
|
|
308
|
+
<button class="sbtn close" id="btn-close"><span class="ic">${ICON.close()}</span> Cerrar</button>
|
|
309
|
+
</div>
|
|
310
|
+
`;
|
|
311
|
+
$sessionSlot.querySelector("#btn-new").addEventListener("click", newSession);
|
|
312
|
+
$sessionSlot.querySelector("#btn-close").addEventListener("click", closeWindow);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Conversation card ────────────────────────────────────────────────────
|
|
316
|
+
let $convScroll = null;
|
|
317
|
+
function ensureConv() {
|
|
318
|
+
if (!$convSlot.firstChild) {
|
|
319
|
+
$convSlot.innerHTML = `<div class="conv"><div class="conv-scroll" id="conv-scroll"></div></div>`;
|
|
320
|
+
$convScroll = $convSlot.querySelector("#conv-scroll");
|
|
321
|
+
// Re-render all existing turns
|
|
322
|
+
messages.forEach((m, i) => appendTurn(m, i === messages.length - 1));
|
|
323
|
+
if (mode === "transcribing") renderPendingUserPartial();
|
|
324
|
+
if (mode === "thinking" || mode === "speaking") ensureStreamingAgentBubble();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function appendTurn(m, isLast) {
|
|
329
|
+
if (!$convScroll) return;
|
|
330
|
+
if (isLast) clearLastClass();
|
|
331
|
+
const t = document.createElement("div");
|
|
332
|
+
t.className = "turn" + (isLast ? " last" : "");
|
|
333
|
+
t.dataset.id = m.id;
|
|
334
|
+
if (m.role === "user") {
|
|
335
|
+
const viaIcon = m.via === "voice" ? `<span class="via-mic" title="Mensaje de voz">${ICON.mic()}</span>` : "";
|
|
336
|
+
t.innerHTML = `
|
|
337
|
+
<div class="role user">
|
|
338
|
+
<span class="ava">${ICON.person()}</span>
|
|
339
|
+
<span class="who">Vos</span>
|
|
340
|
+
<span class="time">${m.t}</span>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="bubble-user">${escapeHtml(m.text)}${viaIcon}</div>
|
|
343
|
+
`;
|
|
344
|
+
} else {
|
|
345
|
+
t.innerHTML = `
|
|
346
|
+
<div class="role agent">
|
|
347
|
+
<span class="ava sa"><img src="assets/superagent.png" alt=""/></span>
|
|
348
|
+
<span class="who">${escapeHtml(agentName)}</span>
|
|
349
|
+
<span class="time">${m.t || ""}</span>
|
|
350
|
+
</div>
|
|
351
|
+
<div class="msg-agent">${formatWordsHtml(m.text)}</div>
|
|
352
|
+
${m.audio ? "" /* scrubber added separately */ : ""}
|
|
353
|
+
<div class="turn-actions">
|
|
354
|
+
<button class="chip btn-regen">${ICON.refresh()} Regenerar</button>
|
|
355
|
+
<button class="chip btn-copy">${ICON.copy()} Copiar</button>
|
|
356
|
+
</div>
|
|
357
|
+
`;
|
|
358
|
+
if (m.audio && m.dur) {
|
|
359
|
+
// Insert scrubber before turn-actions
|
|
360
|
+
const scrubberHtml = buildScrubberHtml(m);
|
|
361
|
+
const actions = t.querySelector(".turn-actions");
|
|
362
|
+
actions.insertAdjacentHTML("beforebegin", scrubberHtml);
|
|
363
|
+
wireScrubber(t, m);
|
|
364
|
+
}
|
|
365
|
+
// copy
|
|
366
|
+
t.querySelector(".btn-copy")?.addEventListener("click", (e) => {
|
|
367
|
+
navigator.clipboard?.writeText(m.text).catch(() => {});
|
|
368
|
+
const btn = e.currentTarget;
|
|
369
|
+
btn.classList.add("done");
|
|
370
|
+
btn.innerHTML = `${ICON.check()} Copiado`;
|
|
371
|
+
setTimeout(() => { btn.classList.remove("done"); btn.innerHTML = `${ICON.copy()} Copiar`; }, 1400);
|
|
372
|
+
});
|
|
373
|
+
// regen: only the LAST agent turn can be regenerated. Past turns
|
|
374
|
+
// can't because we'd have to re-issue the user prompt that came right
|
|
375
|
+
// before THEM, then re-thread the entire suffix of the conversation,
|
|
376
|
+
// and that gets semantically confusing fast. CSS hides .btn-regen on
|
|
377
|
+
// older turns; this guard catches anyone who routes around the CSS.
|
|
378
|
+
t.querySelector(".btn-regen")?.addEventListener("click", () => {
|
|
379
|
+
const lastMsg = messages[messages.length - 1];
|
|
380
|
+
if (!lastMsg || lastMsg.id !== m.id) {
|
|
381
|
+
console.warn("desktop renderer: regen only works on the last turn");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const lastUser = [...messages].reverse().find((x) => x.role === "user");
|
|
385
|
+
if (!lastUser) return;
|
|
386
|
+
// Drop the matching assistant entry from `history` so the daemon
|
|
387
|
+
// gets the same conversation it had right before producing `m`.
|
|
388
|
+
if (history.length && history[history.length - 1].role === "assistant") {
|
|
389
|
+
history.pop();
|
|
390
|
+
}
|
|
391
|
+
messages = messages.filter((x) => x.id !== m.id);
|
|
392
|
+
rebuildConvFromState();
|
|
393
|
+
startAgentTurn();
|
|
394
|
+
sendToDaemon(lastUser.text);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
$convScroll.appendChild(t);
|
|
398
|
+
scrollConvToBottom();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Strip the `last` modifier from every turn currently in the scroll. Called
|
|
402
|
+
// right before we mount a new "last" turn so the previous one stops being
|
|
403
|
+
// styled as the freshest reply (and its Regenerate button hides).
|
|
404
|
+
function clearLastClass() {
|
|
405
|
+
if (!$convScroll) return;
|
|
406
|
+
$convScroll.querySelectorAll(".turn.last").forEach((el) => el.classList.remove("last"));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function rebuildConvFromState() {
|
|
410
|
+
if (!$convScroll) return;
|
|
411
|
+
$convScroll.innerHTML = "";
|
|
412
|
+
streamingAgentEntry = null;
|
|
413
|
+
toolPillsByName = {};
|
|
414
|
+
messages.forEach((m, i) => appendTurn(m, i === messages.length - 1));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function renderPendingUserPartial() {
|
|
418
|
+
if (!$convScroll) return;
|
|
419
|
+
const existing = $convScroll.querySelector("[data-pending='user']");
|
|
420
|
+
if (existing) {
|
|
421
|
+
const bub = existing.querySelector(".bubble-user");
|
|
422
|
+
bub.innerHTML = `${escapeHtml(pendingUserText)}<span class="caret"></span>`;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const t = document.createElement("div");
|
|
426
|
+
t.className = "turn last";
|
|
427
|
+
t.dataset.pending = "user";
|
|
428
|
+
t.innerHTML = `
|
|
429
|
+
<div class="role user">
|
|
430
|
+
<span class="ava">${ICON.person()}</span>
|
|
431
|
+
<span class="who">Vos</span>
|
|
432
|
+
<span class="time">${nowHHMM()}</span>
|
|
433
|
+
</div>
|
|
434
|
+
<div class="bubble-user">${escapeHtml(pendingUserText)}<span class="caret"></span></div>
|
|
435
|
+
`;
|
|
436
|
+
$convScroll.appendChild(t);
|
|
437
|
+
scrollConvToBottom();
|
|
438
|
+
}
|
|
439
|
+
function removePendingUserPartial() {
|
|
440
|
+
$convScroll?.querySelector("[data-pending='user']")?.remove();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function ensureStreamingAgentBubble() {
|
|
444
|
+
if (!$convScroll) return;
|
|
445
|
+
if (streamingAgentEntry?.el && document.body.contains(streamingAgentEntry.el)) return;
|
|
446
|
+
clearLastClass();
|
|
447
|
+
const id = nextId++;
|
|
448
|
+
const t = document.createElement("div");
|
|
449
|
+
t.className = "turn last";
|
|
450
|
+
t.dataset.id = id;
|
|
451
|
+
// Placeholder while we wait for the first token: just the dots — the
|
|
452
|
+
// word "Pensando" already appears in the capsule, no need to repeat it
|
|
453
|
+
// inside the bubble. See feedback on doble-"Pensando" in 2026-05-30.
|
|
454
|
+
t.innerHTML = `
|
|
455
|
+
<div class="role agent">
|
|
456
|
+
<span class="ava sa"><img src="assets/superagent.png" alt=""/></span>
|
|
457
|
+
<span class="who">${escapeHtml(agentName)}</span>
|
|
458
|
+
</div>
|
|
459
|
+
<div class="msg-agent">
|
|
460
|
+
<span class="status-line"><span class="dots"><i></i><i></i><i></i></span></span>
|
|
461
|
+
</div>
|
|
462
|
+
`;
|
|
463
|
+
$convScroll.appendChild(t);
|
|
464
|
+
streamingAgentEntry = { id, el: t, msgEl: t.querySelector(".msg-agent"), text: "", started: false };
|
|
465
|
+
scrollConvToBottom();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function appendStreamingToken(chunk) {
|
|
469
|
+
ensureStreamingAgentBubble();
|
|
470
|
+
if (!streamingAgentEntry.started) {
|
|
471
|
+
streamingAgentEntry.started = true;
|
|
472
|
+
streamingAgentEntry.msgEl.innerHTML = ""; // clear the dots placeholder
|
|
473
|
+
}
|
|
474
|
+
streamingAgentEntry.text += chunk;
|
|
475
|
+
// Re-render with the word-in animation: split into spans on word boundaries.
|
|
476
|
+
streamingAgentEntry.msgEl.innerHTML = formatWordsHtml(streamingAgentEntry.text) + `<span class="caret"></span>`;
|
|
477
|
+
scrollConvToBottom();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function finalizeStreamingAgent({ audio, dur } = {}) {
|
|
481
|
+
if (!streamingAgentEntry) return;
|
|
482
|
+
const m = {
|
|
483
|
+
id: streamingAgentEntry.id,
|
|
484
|
+
role: "agent",
|
|
485
|
+
text: streamingAgentEntry.text || "",
|
|
486
|
+
t: nowHHMM(),
|
|
487
|
+
audio: audio || null,
|
|
488
|
+
dur: dur || null,
|
|
489
|
+
fresh: true,
|
|
490
|
+
};
|
|
491
|
+
messages.push(m);
|
|
492
|
+
history.push({ role: "assistant", content: m.text });
|
|
493
|
+
// Replace the streaming placeholder with the finished turn
|
|
494
|
+
streamingAgentEntry.el.remove();
|
|
495
|
+
streamingAgentEntry = null;
|
|
496
|
+
appendTurn(m, true);
|
|
497
|
+
// Force resize so the window grows to fit the agent reply right away.
|
|
498
|
+
requestWindowResize();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function addToolPill(name) {
|
|
502
|
+
ensureStreamingAgentBubble();
|
|
503
|
+
if (toolPillsByName[name]) return;
|
|
504
|
+
const pill = document.createElement("div");
|
|
505
|
+
pill.className = "tool-pill";
|
|
506
|
+
pill.innerHTML = `<div class="spinner"></div><span>${escapeHtml(name)}</span>`;
|
|
507
|
+
$convScroll.insertBefore(pill, streamingAgentEntry.el);
|
|
508
|
+
toolPillsByName[name] = pill;
|
|
509
|
+
scrollConvToBottom();
|
|
510
|
+
}
|
|
511
|
+
function updateToolPill(name) {
|
|
512
|
+
const pill = toolPillsByName[name];
|
|
513
|
+
if (!pill) return;
|
|
514
|
+
pill.innerHTML = `<span class="check">✓</span><span>${escapeHtml(name)}</span>`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ── Audio scrubber ───────────────────────────────────────────────────────
|
|
518
|
+
function buildScrubberHtml(m) {
|
|
519
|
+
const N = 38;
|
|
520
|
+
const bars = waveShape(N, 13);
|
|
521
|
+
const dur = m.dur || 0;
|
|
522
|
+
const fmt = (s) => `0:${String(Math.round(s)).padStart(2, "0")}`;
|
|
523
|
+
return `
|
|
524
|
+
<div class="audio" data-bars="${N}">
|
|
525
|
+
<button class="play" aria-label="Reproducir respuesta">${ICON.play()}</button>
|
|
526
|
+
<div class="wavebar">
|
|
527
|
+
${bars.map((h) => `<i style="height:${Math.round(h * 24)}px"></i>`).join("")}
|
|
528
|
+
</div>
|
|
529
|
+
<span class="dur">${fmt(dur)}</span>
|
|
530
|
+
</div>
|
|
531
|
+
`;
|
|
532
|
+
}
|
|
533
|
+
function wireScrubber(turnEl, m) {
|
|
534
|
+
const N = 38;
|
|
535
|
+
const audioEl = turnEl.querySelector(".audio");
|
|
536
|
+
if (!audioEl || !m.audio) return;
|
|
537
|
+
const $play = audioEl.querySelector(".play");
|
|
538
|
+
const $bar = audioEl.querySelector(".wavebar");
|
|
539
|
+
const bars = $bar.querySelectorAll("i");
|
|
540
|
+
const $dur = audioEl.querySelector(".dur");
|
|
541
|
+
const dur = m.dur || 1;
|
|
542
|
+
const fmt = (s) => `0:${String(Math.round(s)).padStart(2, "0")}`;
|
|
543
|
+
const audio = new Audio(m.audio);
|
|
544
|
+
let raf = null;
|
|
545
|
+
let progress = 0;
|
|
546
|
+
|
|
547
|
+
const setProgress = (p) => {
|
|
548
|
+
progress = Math.max(0, Math.min(1, p));
|
|
549
|
+
const cur = Math.floor(progress * N);
|
|
550
|
+
bars.forEach((b, i) => {
|
|
551
|
+
b.classList.toggle("on", i <= cur);
|
|
552
|
+
b.classList.toggle("cur", i === cur && !audio.paused);
|
|
553
|
+
});
|
|
554
|
+
$dur.textContent = progress > 0 || !audio.paused ? fmt(progress * dur) : fmt(dur);
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const tick = () => {
|
|
558
|
+
if (audio.duration > 0) setProgress(audio.currentTime / audio.duration);
|
|
559
|
+
raf = requestAnimationFrame(tick);
|
|
560
|
+
};
|
|
561
|
+
audio.addEventListener("play", () => { $play.innerHTML = ICON.pause(); raf = requestAnimationFrame(tick); mode = "speaking"; render(); });
|
|
562
|
+
audio.addEventListener("pause", () => { $play.innerHTML = ICON.play(); if (raf) cancelAnimationFrame(raf); if (mode === "speaking") { mode = "idle"; render(); } });
|
|
563
|
+
audio.addEventListener("ended", () => { setProgress(1); if (mode === "speaking") { mode = "idle"; render(); } });
|
|
564
|
+
|
|
565
|
+
$play.addEventListener("click", () => audio.paused ? audio.play() : audio.pause());
|
|
566
|
+
$bar.addEventListener("click", (e) => {
|
|
567
|
+
const r = $bar.getBoundingClientRect();
|
|
568
|
+
const p = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
|
|
569
|
+
if (audio.duration > 0) audio.currentTime = p * audio.duration;
|
|
570
|
+
setProgress(p);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// If the audio errors out (404, decode error, autoplay block, etc) make
|
|
574
|
+
// sure the capsule doesn't stay stuck in "está hablando…".
|
|
575
|
+
audio.addEventListener("error", () => {
|
|
576
|
+
if (mode === "speaking") { mode = "idle"; render(); }
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// autoplay if it's the fresh reply
|
|
580
|
+
if (m.fresh) {
|
|
581
|
+
m.fresh = false;
|
|
582
|
+
ttsAudio?.pause?.();
|
|
583
|
+
ttsAudio = audio;
|
|
584
|
+
audio.play().catch(() => {
|
|
585
|
+
// Autoplay block (rare in Electron with user-gesture but possible
|
|
586
|
+
// when the window has never been focused). Bail out so the capsule
|
|
587
|
+
// returns to idle and the user can still tap "play" on the scrubber.
|
|
588
|
+
if (mode === "speaking" || mode === "thinking") { mode = "idle"; render(); }
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Post-finalize hook: add a scrubber to an already-rendered agent turn
|
|
594
|
+
// when its TTS audio arrives. Called from the `tts-ready` daemon event.
|
|
595
|
+
function attachAudioToTurn(turnId, { url, dur }) {
|
|
596
|
+
const m = messages.find((x) => x.id === turnId);
|
|
597
|
+
if (!m) return;
|
|
598
|
+
m.audio = url;
|
|
599
|
+
m.dur = dur || 0;
|
|
600
|
+
m.fresh = true; // autoplay the freshly-arrived reply
|
|
601
|
+
const turnEl = $convScroll?.querySelector(`[data-id="${turnId}"]`);
|
|
602
|
+
if (!turnEl) return;
|
|
603
|
+
// Insert the scrubber HTML just before turn-actions (matches appendTurn order).
|
|
604
|
+
const actions = turnEl.querySelector(".turn-actions");
|
|
605
|
+
const html = buildScrubberHtml(m);
|
|
606
|
+
if (actions) actions.insertAdjacentHTML("beforebegin", html);
|
|
607
|
+
else turnEl.insertAdjacentHTML("beforeend", html);
|
|
608
|
+
wireScrubber(turnEl, m);
|
|
609
|
+
scrollConvToBottom();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function waveShape(n, seed = 7) {
|
|
613
|
+
const out = []; let s = seed;
|
|
614
|
+
for (let i = 0; i < n; i++) {
|
|
615
|
+
s = (s * 9301 + 49297) % 233280;
|
|
616
|
+
const r = s / 233280;
|
|
617
|
+
const env = Math.sin((i / n) * Math.PI);
|
|
618
|
+
out.push(0.25 + (0.35 + r * 0.65) * (0.55 + env * 0.6));
|
|
619
|
+
}
|
|
620
|
+
return out;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ── Recording flow ───────────────────────────────────────────────────────
|
|
624
|
+
function startListening() {
|
|
625
|
+
if (mode !== "idle") return;
|
|
626
|
+
isCancelled = false;
|
|
627
|
+
mode = "listening";
|
|
628
|
+
render();
|
|
629
|
+
startMic();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// commit=true → stop and send the recording; false → cancel
|
|
633
|
+
function stopListening(commit) {
|
|
634
|
+
if (mode !== "listening") return;
|
|
635
|
+
isCancelled = !commit;
|
|
636
|
+
if (!commit) {
|
|
637
|
+
mode = "idle";
|
|
638
|
+
stopMic();
|
|
639
|
+
render();
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
mode = "transcribing";
|
|
643
|
+
render();
|
|
644
|
+
stopMic(); // onstop will resolve, send to daemon
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function cancel() {
|
|
648
|
+
isCancelled = true;
|
|
649
|
+
if (mode === "listening") { stopMic(); }
|
|
650
|
+
if (mode === "thinking" || mode === "speaking") { window.apx?.cancel?.(); }
|
|
651
|
+
removePendingUserPartial();
|
|
652
|
+
if (streamingAgentEntry) {
|
|
653
|
+
streamingAgentEntry.el.remove();
|
|
654
|
+
streamingAgentEntry = null;
|
|
655
|
+
}
|
|
656
|
+
mode = "idle";
|
|
657
|
+
render();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function stopSpeaking() {
|
|
661
|
+
try { ttsAudio?.pause?.(); } catch {}
|
|
662
|
+
if (mode === "speaking") { mode = "idle"; render(); }
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ── Mic capture (buffer chunks, cumulative blob = always has header) ─────
|
|
666
|
+
async function startMic() {
|
|
667
|
+
try {
|
|
668
|
+
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
|
669
|
+
|
|
670
|
+
// Web Audio analyser → real-time amplitude for the capsule wave.
|
|
671
|
+
try {
|
|
672
|
+
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
673
|
+
const src = audioCtx.createMediaStreamSource(audioStream);
|
|
674
|
+
analyser = audioCtx.createAnalyser();
|
|
675
|
+
analyser.fftSize = 128; // → 64 frequency bins
|
|
676
|
+
analyser.smoothingTimeConstant = 0.72; // analyser-side temporal smoothing
|
|
677
|
+
analyser.minDecibels = -85; // floor (silence)
|
|
678
|
+
analyser.maxDecibels = -15; // ceiling (loud speech)
|
|
679
|
+
src.connect(analyser);
|
|
680
|
+
freqData = new Uint8Array(analyser.frequencyBinCount);
|
|
681
|
+
startWaveLoop();
|
|
682
|
+
} catch (e) {
|
|
683
|
+
console.warn("desktop renderer: AnalyserNode init failed", e);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus")
|
|
687
|
+
? "audio/webm;codecs=opus"
|
|
688
|
+
: "audio/ogg;codecs=opus";
|
|
689
|
+
recorderFormat = mimeType.includes("webm") ? "webm" : "ogg";
|
|
690
|
+
recorderMime = mimeType;
|
|
691
|
+
recordedChunks = [];
|
|
692
|
+
mediaRecorder = new MediaRecorder(audioStream, { mimeType, audioBitsPerSecond: 32000 });
|
|
693
|
+
mediaRecorder.ondataavailable = (e) => {
|
|
694
|
+
if (e.data && e.data.size > 0) recordedChunks.push(e.data);
|
|
695
|
+
runLivePartial();
|
|
696
|
+
};
|
|
697
|
+
mediaRecorder.onstop = async () => {
|
|
698
|
+
if (isCancelled) { recordedChunks = []; if (mode !== "idle") { mode = "idle"; render(); } return; }
|
|
699
|
+
const raw = await transcribeBuffered();
|
|
700
|
+
const text = (raw || "").trim();
|
|
701
|
+
recordedChunks = [];
|
|
702
|
+
// Guard with .trim() — whisper occasionally returns a single space or
|
|
703
|
+
// newline for very short clips, which used to commit an empty bubble.
|
|
704
|
+
if (!text || isCancelled) {
|
|
705
|
+
mode = "idle";
|
|
706
|
+
pendingUserText = "";
|
|
707
|
+
removePendingUserPartial();
|
|
708
|
+
render();
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
pendingUserText = text;
|
|
712
|
+
commitUserMessage(text, /* via */ "voice");
|
|
713
|
+
};
|
|
714
|
+
mediaRecorder.start(2000);
|
|
715
|
+
} catch (e) {
|
|
716
|
+
console.error("desktop renderer: mic error", e);
|
|
717
|
+
mode = "idle";
|
|
718
|
+
render();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function stopMic() {
|
|
722
|
+
try { mediaRecorder?.stop(); } catch {}
|
|
723
|
+
try { audioStream?.getTracks().forEach((t) => t.stop()); } catch {}
|
|
724
|
+
mediaRecorder = null;
|
|
725
|
+
audioStream = null;
|
|
726
|
+
stopWaveLoop();
|
|
727
|
+
try { audioCtx?.close(); } catch {}
|
|
728
|
+
audioCtx = null;
|
|
729
|
+
analyser = null;
|
|
730
|
+
freqData = null;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ── Reactive wave: amplitude-driven bar heights (runs while mode === listening)
|
|
734
|
+
function startWaveLoop() {
|
|
735
|
+
stopWaveLoop();
|
|
736
|
+
// Per-bar smoothed amplitude so heights don't twitch frame-to-frame.
|
|
737
|
+
let smoothed = null;
|
|
738
|
+
const tick = () => {
|
|
739
|
+
if (mode !== "listening" || !analyser) { waveRaf = null; return; }
|
|
740
|
+
analyser.getByteFrequencyData(freqData);
|
|
741
|
+
const wave = $capCenter.querySelector(".cap-wave");
|
|
742
|
+
if (wave) {
|
|
743
|
+
const bars = wave.children;
|
|
744
|
+
const n = bars.length;
|
|
745
|
+
if (!smoothed || smoothed.length !== n) smoothed = new Float32Array(n);
|
|
746
|
+
// Only use the lower ~60% of the spectrum — voice energy lives there;
|
|
747
|
+
// upper bins are mostly noise hiss that would make bars jiggle uselessly.
|
|
748
|
+
const usable = Math.floor(freqData.length * 0.6);
|
|
749
|
+
const binsPerBar = Math.max(1, Math.floor(usable / n));
|
|
750
|
+
for (let i = 0; i < n; i++) {
|
|
751
|
+
let sum = 0;
|
|
752
|
+
const start = i * binsPerBar;
|
|
753
|
+
const end = Math.min(start + binsPerBar, usable);
|
|
754
|
+
for (let j = start; j < end; j++) sum += freqData[j];
|
|
755
|
+
const raw = sum / Math.max(1, (end - start)) / 255; // 0..1
|
|
756
|
+
// ease curve — small inputs stay small, loud inputs reach ~1
|
|
757
|
+
const v = Math.pow(raw, 0.65);
|
|
758
|
+
// exponential smoothing: snappy attack, slower decay (feels punchier)
|
|
759
|
+
const k = v > smoothed[i] ? 0.55 : 0.18;
|
|
760
|
+
smoothed[i] = smoothed[i] + (v - smoothed[i]) * k;
|
|
761
|
+
// map to height: 3px floor (silence) → 28px peak
|
|
762
|
+
const h = 3 + smoothed[i] * 25;
|
|
763
|
+
bars[i].style.height = h.toFixed(1) + "px";
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
waveRaf = requestAnimationFrame(tick);
|
|
767
|
+
};
|
|
768
|
+
waveRaf = requestAnimationFrame(tick);
|
|
769
|
+
}
|
|
770
|
+
function stopWaveLoop() {
|
|
771
|
+
if (waveRaf != null) cancelAnimationFrame(waveRaf);
|
|
772
|
+
waveRaf = null;
|
|
773
|
+
}
|
|
774
|
+
async function transcribeBuffered() {
|
|
775
|
+
if (!recordedChunks.length) return "";
|
|
776
|
+
const blob = new Blob(recordedChunks, { type: recorderMime });
|
|
777
|
+
const buf = await blob.arrayBuffer();
|
|
778
|
+
try {
|
|
779
|
+
const r = await window.apx.transcribeChunk(buf, recorderFormat, "auto");
|
|
780
|
+
if (r?.ok && r.text?.trim()) return r.text.trim();
|
|
781
|
+
} catch {}
|
|
782
|
+
return "";
|
|
783
|
+
}
|
|
784
|
+
async function runLivePartial() {
|
|
785
|
+
if (liveBusy || mode !== "listening" || !recordedChunks.length) return;
|
|
786
|
+
liveBusy = true;
|
|
787
|
+
try {
|
|
788
|
+
const text = await transcribeBuffered();
|
|
789
|
+
if (text && mode === "listening") {
|
|
790
|
+
pendingUserText = text;
|
|
791
|
+
// No visible live preview in the capsule wave mode; update is mostly
|
|
792
|
+
// useful for the conv pending-user partial during transcribing.
|
|
793
|
+
}
|
|
794
|
+
} finally { liveBusy = false; }
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ── Send: text path + post-transcription commit path ─────────────────────
|
|
798
|
+
function sendText(text) {
|
|
799
|
+
const t = (text || "").trim();
|
|
800
|
+
if (!t) return;
|
|
801
|
+
commitUserMessage(t, /* via */ "text");
|
|
802
|
+
}
|
|
803
|
+
function commitUserMessage(text, via) {
|
|
804
|
+
const clean = (text || "").trim();
|
|
805
|
+
if (!clean) { console.warn("desktop renderer: refused to commit empty user message"); return; }
|
|
806
|
+
const m = { id: nextId++, role: "user", text: clean, t: nowHHMM(), via };
|
|
807
|
+
messages.push(m);
|
|
808
|
+
history.push({ role: "user", content: clean });
|
|
809
|
+
pendingUserText = "";
|
|
810
|
+
removePendingUserPartial();
|
|
811
|
+
ensureConv();
|
|
812
|
+
appendTurn(m, true);
|
|
813
|
+
startAgentTurn();
|
|
814
|
+
sendToDaemon(clean);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Begin a fresh agent turn: reset per-turn flags, switch to thinking,
|
|
818
|
+
// mount the placeholder bubble, and ask main to grow the window now (not
|
|
819
|
+
// one ResizeObserver tick later). Shared by commitUserMessage + regen so
|
|
820
|
+
// both paths set up the daemon-event pipeline identically.
|
|
821
|
+
function startAgentTurn() {
|
|
822
|
+
doneHandled = false;
|
|
823
|
+
pendingTtsTurnId = null;
|
|
824
|
+
if (ttsTimer) { clearTimeout(ttsTimer); ttsTimer = null; }
|
|
825
|
+
mode = "thinking";
|
|
826
|
+
render();
|
|
827
|
+
ensureStreamingAgentBubble();
|
|
828
|
+
requestWindowResize();
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function sendToDaemon(text) {
|
|
832
|
+
const prev = history.slice(0, -1).slice(-20);
|
|
833
|
+
window.apx?.sendMessage?.(text, prev).catch?.((e) => {
|
|
834
|
+
finalizeStreamingAgentError(e?.message || String(e));
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function finalizeStreamingAgentError(message) {
|
|
839
|
+
if (!streamingAgentEntry) ensureStreamingAgentBubble();
|
|
840
|
+
streamingAgentEntry.text = "Error: " + message;
|
|
841
|
+
streamingAgentEntry.msgEl.innerHTML = `<span style="color:oklch(0.6 0.2 22)">${escapeHtml(streamingAgentEntry.text)}</span>`;
|
|
842
|
+
finalizeStreamingAgent();
|
|
843
|
+
mode = "idle"; render();
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function newSession() {
|
|
847
|
+
cancel();
|
|
848
|
+
messages = [];
|
|
849
|
+
history = [];
|
|
850
|
+
streamingAgentEntry = null;
|
|
851
|
+
toolPillsByName = {};
|
|
852
|
+
pendingUserText = "";
|
|
853
|
+
$convSlot.innerHTML = "";
|
|
854
|
+
$sessionSlot.innerHTML = "";
|
|
855
|
+
mode = "idle";
|
|
856
|
+
render();
|
|
857
|
+
}
|
|
858
|
+
function closeWindow() { window.apx?.close?.(); }
|
|
859
|
+
|
|
860
|
+
// ── Daemon event router ──────────────────────────────────────────────────
|
|
861
|
+
window.apx?.onDaemonEvent?.((msg) => {
|
|
862
|
+
switch (msg.type) {
|
|
863
|
+
case "thinking":
|
|
864
|
+
if (mode !== "thinking" && mode !== "speaking") { mode = "thinking"; render(); }
|
|
865
|
+
ensureStreamingAgentBubble();
|
|
866
|
+
break;
|
|
867
|
+
case "token":
|
|
868
|
+
appendStreamingToken(msg.text || "");
|
|
869
|
+
break;
|
|
870
|
+
case "tool_start": addToolPill(msg.name); break;
|
|
871
|
+
case "tool_done": updateToolPill(msg.name); break;
|
|
872
|
+
case "done": {
|
|
873
|
+
// Daemon may emit `done` twice (retry/race). Process only once per turn.
|
|
874
|
+
if (doneHandled) break;
|
|
875
|
+
doneHandled = true;
|
|
876
|
+
const finalText = msg.text || streamingAgentEntry?.text || "";
|
|
877
|
+
// CRITICAL: many models (gemini-flash, groq-fast tier) don't stream
|
|
878
|
+
// tokens — they send the whole reply in `done`. Without this branch
|
|
879
|
+
// the bubble stays with just the dots placeholder until TTS resolves
|
|
880
|
+
// (or 6s timeout), which feels broken. Inject the text NOW so the
|
|
881
|
+
// user sees the reply immediately.
|
|
882
|
+
if (streamingAgentEntry) {
|
|
883
|
+
streamingAgentEntry.text = finalText;
|
|
884
|
+
if (!streamingAgentEntry.started && finalText) {
|
|
885
|
+
streamingAgentEntry.started = true;
|
|
886
|
+
streamingAgentEntry.msgEl.innerHTML = formatWordsHtml(finalText);
|
|
887
|
+
scrollConvToBottom();
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
// Finalize and return to idle right away so the capsule frees up.
|
|
891
|
+
// TTS runs in the background; tts-ready will attach the scrubber to
|
|
892
|
+
// the already-rendered turn (see attachAudioToLastAgentTurn below).
|
|
893
|
+
const finalizedTurnId = streamingAgentEntry?.id;
|
|
894
|
+
finalizeStreamingAgent();
|
|
895
|
+
mode = "idle"; render();
|
|
896
|
+
// Fire-and-forget TTS request. If it returns audio, attach it to
|
|
897
|
+
// the turn we just rendered; if it errors / times out / never replies,
|
|
898
|
+
// no big deal — the user already has the text. Guard with a 6s soft
|
|
899
|
+
// timeout so a stuck request doesn't hold ttsTimer state.
|
|
900
|
+
const handled = window.apx?.requestTts?.(finalText);
|
|
901
|
+
if (handled) {
|
|
902
|
+
if (ttsTimer) clearTimeout(ttsTimer);
|
|
903
|
+
ttsTimer = setTimeout(() => { ttsTimer = null; }, 6000);
|
|
904
|
+
// Remember which turn the next tts-ready/failed belongs to.
|
|
905
|
+
pendingTtsTurnId = finalizedTurnId || null;
|
|
906
|
+
}
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
case "tts-ready": {
|
|
910
|
+
if (ttsTimer) { clearTimeout(ttsTimer); ttsTimer = null; }
|
|
911
|
+
if (pendingTtsTurnId != null) {
|
|
912
|
+
attachAudioToTurn(pendingTtsTurnId, { url: msg.url, dur: msg.duration });
|
|
913
|
+
pendingTtsTurnId = null;
|
|
914
|
+
}
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
case "tts-failed":
|
|
918
|
+
// The text is already on screen; just clean up the timer + pending id.
|
|
919
|
+
if (ttsTimer) { clearTimeout(ttsTimer); ttsTimer = null; }
|
|
920
|
+
pendingTtsTurnId = null;
|
|
921
|
+
break;
|
|
922
|
+
case "error":
|
|
923
|
+
finalizeStreamingAgentError(msg.message || "Unknown error");
|
|
924
|
+
break;
|
|
925
|
+
case "cancelled":
|
|
926
|
+
if (streamingAgentEntry) {
|
|
927
|
+
if (!streamingAgentEntry.text) streamingAgentEntry.el.remove();
|
|
928
|
+
else finalizeStreamingAgent();
|
|
929
|
+
streamingAgentEntry = null;
|
|
930
|
+
}
|
|
931
|
+
mode = "idle"; render();
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
window.apx?.onDaemonConnected?.(() => $connBadge.classList.remove("show"));
|
|
937
|
+
window.apx?.onDaemonDisconnected?.(() => $connBadge.classList.add("show"));
|
|
938
|
+
|
|
939
|
+
// ── Main-process IPC: recording-start/stop (global hotkey), focus-input ──
|
|
940
|
+
window.apx?.onRecordingStart?.(() => { if (mode === "idle") startListening(); });
|
|
941
|
+
window.apx?.onRecordingStop?.(() => { if (mode === "listening") stopListening(true); });
|
|
942
|
+
window.apx?.onFocusInput?.(() => {
|
|
943
|
+
if (mode !== "idle") return;
|
|
944
|
+
window._focusOnNext = true;
|
|
945
|
+
render();
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// ── Keyboard ─────────────────────────────────────────────────────────────
|
|
949
|
+
document.addEventListener("keydown", (e) => {
|
|
950
|
+
if (e.key === "Escape") {
|
|
951
|
+
e.preventDefault();
|
|
952
|
+
if (mode === "listening" || mode === "transcribing" || mode === "thinking" || mode === "speaking") cancel();
|
|
953
|
+
else closeWindow();
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// ── Window-size hint to main (collapse to capsule when empty) ────────────
|
|
958
|
+
// ResizeObserver fires whenever the rendered height of #root changes —
|
|
959
|
+
// more reliable than a setTimeout poll (used to under-report by one frame
|
|
960
|
+
// and clip the session bar). 24px bottom padding so the buttons breathe.
|
|
961
|
+
let lastH = 0;
|
|
962
|
+
function requestWindowResize() {
|
|
963
|
+
if (!$root) return;
|
|
964
|
+
const h = Math.ceil($root.getBoundingClientRect().height) + 24;
|
|
965
|
+
if (h !== lastH) {
|
|
966
|
+
lastH = h;
|
|
967
|
+
window.apx?.resize?.(h);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
const ro = new ResizeObserver(() => requestWindowResize());
|
|
972
|
+
ro.observe($root);
|
|
973
|
+
} catch {
|
|
974
|
+
// Older runtimes without ResizeObserver: fall back to a 250ms poll.
|
|
975
|
+
setInterval(requestWindowResize, 250);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
979
|
+
function nowHHMM() {
|
|
980
|
+
const d = new Date();
|
|
981
|
+
return d.getHours() + ":" + String(d.getMinutes()).padStart(2, "0");
|
|
982
|
+
}
|
|
983
|
+
function escapeHtml(s) {
|
|
984
|
+
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
985
|
+
}
|
|
986
|
+
// Wrap each word in a span.w so wordIn animates fresh tokens
|
|
987
|
+
function formatWordsHtml(text) {
|
|
988
|
+
const escaped = escapeHtml(text);
|
|
989
|
+
// Split on spaces but keep newlines as <br>
|
|
990
|
+
return escaped
|
|
991
|
+
.split("\n")
|
|
992
|
+
.map((line) => line.split(/(\s+)/).map((tok) => tok.match(/^\s+$/) ? tok : `<span class="w">${tok}</span>`).join(""))
|
|
993
|
+
.join("<br>");
|
|
994
|
+
}
|
|
995
|
+
function scrollConvToBottom() {
|
|
996
|
+
if (!$convScroll) return;
|
|
997
|
+
requestAnimationFrame(() => { $convScroll.scrollTop = $convScroll.scrollHeight; });
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ── First paint ──────────────────────────────────────────────────────────
|
|
1001
|
+
// Wait briefly for the config Promise (theme/position/shortcut/agentName).
|
|
1002
|
+
// If it doesn't resolve in 400ms, paint anyway so the window doesn't stay
|
|
1003
|
+
// blank on a wedged daemon — the .then() will patch the placeholder when
|
|
1004
|
+
// it finally resolves.
|
|
1005
|
+
setTimeout(() => { if (!configReady) render(); }, 400);
|
|
1006
|
+
})();
|