Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
45
crates/codegen/xai-grok-pager/src/voice/auth.rs
Normal file
45
crates/codegen/xai-grok-pager/src/voice/auth.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//! Bridge the shell's `AuthManager` onto the voice crate's bearer provider.
|
||||
//!
|
||||
//! voice-api accepts both API keys and OAuth2 tokens directly at `api.x.ai`
|
||||
//! and attributes per-user billing for OAuth, so the voice channel just reuses
|
||||
//! the same bearer the agent uses for chat — no separate env var.
|
||||
//!
|
||||
//! Resolved per request: the agent's refreshing manager in direct-spawn mode,
|
||||
//! or a non-refreshing one that adopts the agent's rotated `auth.json` token
|
||||
//! under the file lock in leader mode (see [`crate::acp`]).
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use xai_grok_tools::types::SharedApiKeyProvider;
|
||||
use xai_grok_voice::{SharedVoiceAuth, VoiceAuthProvider};
|
||||
|
||||
/// Adapts the shell's `ApiKeyProvider` onto [`VoiceAuthProvider`].
|
||||
///
|
||||
/// Resolves a token per request (never a static snapshot) so a long session
|
||||
/// follows the underlying `AuthManager` instead of pinning a token that 401s.
|
||||
struct AuthManagerVoiceAuth(SharedApiKeyProvider);
|
||||
|
||||
impl std::fmt::Debug for AuthManagerVoiceAuth {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("AuthManagerVoiceAuth")
|
||||
}
|
||||
}
|
||||
|
||||
impl VoiceAuthProvider for AuthManagerVoiceAuth {
|
||||
fn bearer(&self) -> Pin<Box<dyn Future<Output = Option<String>> + Send + '_>> {
|
||||
let provider = self.0.clone();
|
||||
Box::pin(async move { provider.current_api_key_async().await })
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the voice bearer provider from the connection's `AuthManager`.
|
||||
///
|
||||
/// Works for every auth method: OAuth / grok.com / OIDC session tokens and
|
||||
/// `XAI_API_KEY` / per-model BYOK keys.
|
||||
pub fn build_voice_auth(auth_manager: Arc<xai_grok_shell::auth::AuthManager>) -> SharedVoiceAuth {
|
||||
Arc::new(AuthManagerVoiceAuth(
|
||||
xai_grok_shell::auth::shared_api_key_provider(auth_manager),
|
||||
))
|
||||
}
|
||||
89
crates/codegen/xai-grok-pager/src/voice/handle.rs
Normal file
89
crates/codegen/xai-grok-pager/src/voice/handle.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
//! Map pipeline [`VoiceEvent`]s onto prompt-box dictation state.
|
||||
|
||||
use xai_grok_voice::VoiceEvent;
|
||||
|
||||
use crate::app::app_view::{AppView, VoiceTarget};
|
||||
|
||||
/// Append finalized text to whichever prompt started capture
|
||||
/// (`voice_recording_target`) — the agent prompt or the dashboard dispatch input
|
||||
/// — not necessarily the active view, so a late final after a view switch still
|
||||
/// lands in the right place. Inserts a single separating space unless the prompt
|
||||
/// is empty or already ends in whitespace (preserves trailing newlines).
|
||||
fn append_voice_text_to_prompt(app: &mut AppView, text: &str) {
|
||||
let combine = |existing: &str| -> String {
|
||||
if existing.trim().is_empty() {
|
||||
text.to_string()
|
||||
} else if existing.ends_with(char::is_whitespace) {
|
||||
format!("{existing}{text}")
|
||||
} else {
|
||||
format!("{existing} {text}")
|
||||
}
|
||||
};
|
||||
match app.voice_recording_target() {
|
||||
Some(VoiceTarget::Agent(id)) => {
|
||||
let Some(agent) = app.agents.get_mut(&id) else {
|
||||
return;
|
||||
};
|
||||
let combined = combine(agent.prompt.text());
|
||||
agent.prompt.set_text(&combined);
|
||||
agent.prompt.set_cursor(combined.len());
|
||||
}
|
||||
Some(target @ (VoiceTarget::DashboardDispatch | VoiceTarget::DashboardPeekReply(_))) => {
|
||||
let Some(dashboard) = app.dashboard.as_mut() else {
|
||||
return;
|
||||
};
|
||||
// Route to the box bound at capture start. The dispatch box is stable,
|
||||
// but the peek reply widget is *shared* across rows and reassigned when
|
||||
// the peeked row changes. While listening `enforce_voice_session_bound`
|
||||
// stops capture on a row change, but after an explicit stop the target
|
||||
// is kept for the trailing final and that guard no longer runs — so
|
||||
// re-check the bound row here, or a final would land in (and send from)
|
||||
// another agent's reply.
|
||||
let prompt = match target {
|
||||
VoiceTarget::DashboardPeekReply(rec) => {
|
||||
let peeked = match dashboard.peek.as_ref().map(|p| &p.row) {
|
||||
Some(crate::views::dashboard::DashboardRowId::TopLevel(id)) => Some(*id),
|
||||
_ => None,
|
||||
};
|
||||
if peeked != Some(rec) {
|
||||
return;
|
||||
}
|
||||
&mut dashboard.peek_reply
|
||||
}
|
||||
_ => &mut dashboard.dispatch,
|
||||
};
|
||||
let combined = combine(prompt.text());
|
||||
prompt.set_text(&combined);
|
||||
prompt.set_cursor(combined.len());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a voice event to app state. Returns whether the frame should redraw.
|
||||
pub fn handle_voice_event(app: &mut AppView, event: VoiceEvent) -> bool {
|
||||
match event {
|
||||
VoiceEvent::InterimTranscript { text } => {
|
||||
// No-op unless recording, so a late interim after a stop can't
|
||||
// repopulate the overlay.
|
||||
app.voice_set_interim(text)
|
||||
}
|
||||
VoiceEvent::UtteranceFinal { text } => {
|
||||
app.voice_clear_interim();
|
||||
// Keep the mic open across pauses; user stops explicitly, then Enter to send.
|
||||
// The bound target survives a stop (`Stopping`), so a trailing final
|
||||
// after an explicit stop still lands.
|
||||
if !text.trim().is_empty() {
|
||||
append_voice_text_to_prompt(app, text.trim());
|
||||
}
|
||||
true
|
||||
}
|
||||
VoiceEvent::Error { message } => {
|
||||
// Tear down the session; `voice_reset` releases the mic when recording
|
||||
// (the pipeline reader usually has already on error).
|
||||
app.voice_reset();
|
||||
app.show_toast(&format!("Voice: {message}"));
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
27
crates/codegen/xai-grok-pager/src/voice/mod.rs
Normal file
27
crates/codegen/xai-grok-pager/src/voice/mod.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//! Voice input: STT pipeline integration and prompt-box dictation.
|
||||
//!
|
||||
//! Layering (pager-owned):
|
||||
//! - **Voice gate** — GA default **on**. Remote `voice_mode_enabled: false` is
|
||||
//! a kill switch (every voice surface unavailable and silent — no toast).
|
||||
//! Absent remote falls through to on. `GROK_VOICE_MODE` overrides for local
|
||||
//! dev (env > remote > default on). Free/X Basic still get SuperGrok upsell
|
||||
//! via tier gates (not this flag).
|
||||
//! - **Session mode** (`voice_ui_active`) — this CLI run only; shows the mic.
|
||||
//! - **Capture chord** — `/voice` or `Ctrl+Space` start dictation (Esc/Enter
|
||||
//! stop). `Ctrl+Space` decodes identically on every terminal, so the
|
||||
//! cheatsheet shows it whenever voice is enabled.
|
||||
//! - **Hold-to-talk** — `Ctrl+Space`: hold to record, release to stop, on
|
||||
//! terminals that report key releases (Kitty protocol); elsewhere the same
|
||||
//! chord toggles instead (press starts, press again stops). Handled in
|
||||
//! `app::event_loop`.
|
||||
//!
|
||||
//! Finals append to the recording target's prompt — the agent prompt or the
|
||||
//! dashboard's dispatch (new-agent) input, captured at start via
|
||||
//! [`crate::app::app_view::VoiceTarget`] — while capture stays open across
|
||||
//! speech pauses. The user always submits with Enter; nothing is auto-sent.
|
||||
|
||||
mod auth;
|
||||
mod handle;
|
||||
|
||||
pub use auth::build_voice_auth;
|
||||
pub use handle::handle_voice_event;
|
||||
Loading…
Reference in a new issue