feat(nexus): mount phase 14 providers + drain voice queue in assistant

main.tsx adds VoiceProvider and CommandPaletteProvider to the provider
stack, placed above BrowserRouter so both the route tree and the
document-level Cmd+K listener see a stable context.

PersonalAssistant gains a single-shot effect that, on mount, drains
VoiceContext.queue via drainQueue() and feeds each transcript through
the existing handleSend pipeline. This implements spec section 5.5
(voice captured on non-Assistant routes streams through on arrival).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nexus Dev 2026-04-11 13:23:30 +00:00
parent 2a950dedd0
commit 9b772aa1bd
2 changed files with 45 additions and 23 deletions

View file

@ -14,6 +14,8 @@ import { SidebarProvider } from "./context/SidebarContext";
import { DialogProvider } from "./context/DialogContext";
import { ToastProvider } from "./context/ToastContext";
import { ThemeProvider } from "./context/ThemeContext";
import { VoiceProvider } from "./context/VoiceContext";
import { CommandPaletteProvider } from "./context/CommandPaletteContext";
import { TooltipProvider } from "@/components/ui/tooltip";
import { initPluginBridge } from "./plugins/bridge-init";
import { PluginLauncherProvider } from "./plugins/launchers";
@ -41,29 +43,33 @@ createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<BrowserRouter>
<CompanyProvider>
<ToastProvider>
<LiveUpdatesProvider>
<TooltipProvider>
<BreadcrumbProvider>
<SidebarProvider>
<PanelProvider>
<ChatPanelProvider>
<PluginLauncherProvider>
<DialogProvider>
<App />
</DialogProvider>
</PluginLauncherProvider>
</ChatPanelProvider>
</PanelProvider>
</SidebarProvider>
</BreadcrumbProvider>
</TooltipProvider>
</LiveUpdatesProvider>
</ToastProvider>
</CompanyProvider>
</BrowserRouter>
<VoiceProvider>
<CommandPaletteProvider>
<BrowserRouter>
<CompanyProvider>
<ToastProvider>
<LiveUpdatesProvider>
<TooltipProvider>
<BreadcrumbProvider>
<SidebarProvider>
<PanelProvider>
<ChatPanelProvider>
<PluginLauncherProvider>
<DialogProvider>
<App />
</DialogProvider>
</PluginLauncherProvider>
</ChatPanelProvider>
</PanelProvider>
</SidebarProvider>
</BreadcrumbProvider>
</TooltipProvider>
</LiveUpdatesProvider>
</ToastProvider>
</CompanyProvider>
</BrowserRouter>
</CommandPaletteProvider>
</VoiceProvider>
</ThemeProvider>
</QueryClientProvider>
</StrictMode>

View file

@ -43,6 +43,7 @@ export function PersonalAssistant() {
const queryClient = useQueryClient();
const { pushToast } = useToast();
const { activeConversationId, setActiveConversationId } = useChatPanel();
const voice = useVoice();
const companyId = selectedCompany?.id ?? null;
const companyPrefix = useMemo(
@ -195,6 +196,21 @@ export function PersonalAssistant() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [promote.state.kind]);
// Phase 14 — drain any voice transcripts captured from non-Assistant
// routes into the chat pipeline. Runs once per mount; if the queue is
// empty the effect is a no-op.
const voiceDrainedRef = useRef(false);
useEffect(() => {
if (voiceDrainedRef.current) return;
if (!companyId) return;
if (voice.queue.length === 0) return;
voiceDrainedRef.current = true;
const drained = voice.drainQueue();
for (const text of drained) {
handleSend(text);
}
}, [companyId, voice, handleSend]);
const handlePromote = useCallback(() => {
if (!selectedConvId || !canPromote) return;
promote.startPrompting();