mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
Improve autocomplete UX (inline + external overlay), add serve mode, and add macOS ARM64 release workflow (#66)
* Update CLAUDE.md to clarify pull request workflow and enhance documentation - Added a note to emphasize the importance of opening pull requests against the upstream repository instead of a fork's default remote, ensuring better collaboration and code management. - Improved the overall clarity of the Git workflow section, contributing to a more streamlined development process. * Refactor REPL session handling and remove deprecated chat methods - Removed the `repl_session_chat` functionality from the schema and related files, streamlining the REPL session management. - Introduced a new `start_chat` method in the web channel provider for handling chat interactions, enhancing the chat system's architecture. - Updated socket handling to improve event emission with alias support, ensuring better communication across the application. - Enhanced error handling and logging for chat operations, providing clearer feedback during interactions. - Consolidated chat-related logic into the new web channel module, improving maintainability and organization. * Refactor apiClient to improve token management and eliminate circular dependencies - Renamed `_getToken` to `authTokenGetterRef` for clarity and to avoid clashing with transpiled private method names. - Updated the `setStoreForApiClient` function to use the new `authTokenGetterRef` variable, ensuring the `apiClient` remains free of direct imports from `store/index`. - Enhanced the `ApiClient` class by renaming the `getToken` method to `resolveAuthToken`, improving readability and consistency in token retrieval logic. - Adjusted comments in `apiClient.ts` and `store/index.ts` to reflect the changes and clarify the purpose of the lazy store accessor. * Enhance API client integration with store for improved token management - Introduced `setStoreForApiClient` in `main.tsx` to bind the Redux store's auth token for API requests, ensuring seamless token access. - Updated comments in `apiClient.ts` to clarify the lazy token accessor's purpose and prevent circular dependencies. - Removed the previous direct call to `setStoreForApiClient` from `store/index.ts`, centralizing the setup in `main.tsx` for better organization. - Enhanced test setup to include the new store binding, ensuring consistent token retrieval during testing. * Refactor web channel event handling and enhance chat functionality - Updated `WebChatSseEvent` structure to include new fields: `tool_name`, `skill_id`, `args`, `output`, `success`, and `round`, improving the granularity of event data. - Refactored event emission logic in `emit_web_chat_event` to utilize the new fields, ensuring more informative payloads for tool calls and results. - Enhanced the `start_chat` and `cancel_chat` functions to accommodate the new event structure, improving chat session management. - Introduced a new function `publish_tool_events_from_history` to streamline the emission of tool call and result events from chat history, enhancing the overall chat experience. * Refactor web channel integration and enhance controller management - Updated references from the deprecated `web_channel` module to the new `channels::providers::web` structure, improving code organization and maintainability. - Refactored the `build_registered_controllers` and `build_declared_controller_schemas` functions to utilize the new web channel controller methods, ensuring consistency across the application. - Removed the obsolete `web_channel` module and its associated files, streamlining the codebase and enhancing clarity in the channel management system. - Introduced new functions for managing web channel events and schemas, improving the overall architecture of the web channel integration. * Refactor main.tsx and test setup for improved organization - Reordered imports in `main.tsx` to enhance clarity and maintainability, ensuring that `setStoreForApiClient` is called with the correct store reference. - Removed unnecessary whitespace in `setup.ts`, streamlining the test setup file for better readability. * Fix service gate false blocking when service is running * Support soft-pass daemon gate and harden macOS service detection * Extend list-files fallback trigger phrases in agent loop * Replace list-files fallback with tool-call repair retry * Log full system prompt and drop tool-call repair flow * Add tracing-log dependency and enhance CLI logging - Introduced `tracing-log` as a dependency to bridge `log` and `tracing` for improved logging capabilities. - Added a `--verbose` flag to the CLI for enhanced logging detail, initializing the logging level based on this flag. - Implemented an HTTP request logging middleware to capture and log request details. - Updated CLI help output to reflect the new `--verbose` option and improved logging messages throughout the application. * Tighten system prompt for native tool-calling * Auto-create missing workspace context markdown files * Seed workspace prompt files from canonical agent prompt templates * Enhance logging with nu-ansi-term and improve CLI output - Added `nu-ansi-term` dependency for colored terminal output in logs. - Implemented a custom logging format for better readability in CLI. - Updated logging initialization to conditionally use ANSI colors based on terminal support. - Added debug logging for agent responses to aid in troubleshooting. * Harden OpenAI-compatible native tool-call parsing * Format provider tool-call parsing updates * Implement inline autocomplete suggestions in Conversations component - Added functionality for inline suggestions based on user input in the Conversations component. - Introduced debounce logic for autocomplete requests to optimize performance. - Enhanced user experience by allowing suggestions to be accepted via the Tab key. - Updated UI to display inline completion suffixes in the input area. - Modified backend to support new autocomplete features and improved error handling. * Add macOS ARM64 build workflow and enhance release process - Introduced a new GitHub Actions workflow for building signed macOS ARM64 bundles. - Updated the release workflow to validate signing prerequisites for macOS builds. - Enhanced the Tauri configuration preparation to include updater settings. - Added necessary secrets to the example secrets file for macOS signing. - Implemented CLI autocomplete functionality for macOS, including options for debounce and process management.
This commit is contained in:
+85
-2
@@ -5,6 +5,7 @@ use std::collections::BTreeMap;
|
||||
use crate::core::all;
|
||||
use crate::core::jsonrpc::{default_state, invoke_method, parse_json_params};
|
||||
use crate::core::{ControllerSchema, TypeSchema};
|
||||
use crate::openhuman::autocomplete::ops::{autocomplete_start_cli, AutocompleteStartCliOptions};
|
||||
|
||||
const CLI_BANNER: &str = r#"
|
||||
|
||||
@@ -37,6 +38,7 @@ pub fn run_from_cli_args(args: &[String]) -> Result<()> {
|
||||
fn run_server_command(args: &[String]) -> Result<()> {
|
||||
let mut port: Option<u16> = None;
|
||||
let mut socketio_enabled = true;
|
||||
let mut verbose = false;
|
||||
let mut i = 0usize;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
@@ -54,14 +56,28 @@ fn run_server_command(args: &[String]) -> Result<()> {
|
||||
socketio_enabled = false;
|
||||
i += 1;
|
||||
}
|
||||
"-v" | "--verbose" => {
|
||||
verbose = true;
|
||||
i += 1;
|
||||
}
|
||||
"-h" | "--help" => {
|
||||
println!("Usage: openhuman run [--port <u16>] [--jsonrpc-only]");
|
||||
println!("Usage: openhuman run [--port <u16>] [--jsonrpc-only] [-v|--verbose]");
|
||||
println!();
|
||||
println!(
|
||||
" --port <u16> Listen address port (default: 7788 or OPENHUMAN_CORE_PORT)"
|
||||
);
|
||||
println!(" --jsonrpc-only HTTP JSON-RPC only; disable Socket.IO");
|
||||
println!(" -v, --verbose Shorthand for RUST_LOG=debug when RUST_LOG is unset");
|
||||
println!();
|
||||
println!("Logging: set RUST_LOG (e.g. RUST_LOG=debug openhuman run). Default level is info.");
|
||||
return Ok(());
|
||||
}
|
||||
other => return Err(anyhow::anyhow!("unknown run arg: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
crate::core::logging::init_for_cli_run(verbose);
|
||||
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
@@ -136,6 +152,22 @@ fn run_namespace_command(
|
||||
));
|
||||
};
|
||||
|
||||
if namespace == "autocomplete" && function == "start" {
|
||||
if args.len() > 1 && is_help(&args[1]) {
|
||||
print_autocomplete_start_help();
|
||||
return Ok(());
|
||||
}
|
||||
let cli_options = parse_autocomplete_start_cli_options(&args[1..])?;
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
let value = rt
|
||||
.block_on(async { autocomplete_start_cli(cli_options).await })
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
println!("{}", serde_json::to_string_pretty(&value)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if args.len() > 1 && is_help(&args[1]) {
|
||||
print_function_help(namespace, &schema);
|
||||
return Ok(());
|
||||
@@ -156,6 +188,57 @@ fn run_namespace_command(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_autocomplete_start_cli_options(args: &[String]) -> Result<AutocompleteStartCliOptions> {
|
||||
let mut debounce_ms: Option<u64> = None;
|
||||
let mut serve = false;
|
||||
let mut spawn = false;
|
||||
let mut i = 0usize;
|
||||
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"--debounce-ms" => {
|
||||
let raw = args
|
||||
.get(i + 1)
|
||||
.ok_or_else(|| anyhow::anyhow!("missing value for --debounce-ms"))?;
|
||||
debounce_ms = Some(
|
||||
raw.parse::<u64>()
|
||||
.map_err(|e| anyhow::anyhow!("invalid --debounce-ms: {e}"))?,
|
||||
);
|
||||
i += 2;
|
||||
}
|
||||
"--serve" => {
|
||||
serve = true;
|
||||
i += 1;
|
||||
}
|
||||
"--spawn" => {
|
||||
spawn = true;
|
||||
i += 1;
|
||||
}
|
||||
other => return Err(anyhow::anyhow!("unknown autocomplete start arg: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
if serve && spawn {
|
||||
return Err(anyhow::anyhow!(
|
||||
"--serve and --spawn are mutually exclusive"
|
||||
));
|
||||
}
|
||||
|
||||
Ok(AutocompleteStartCliOptions {
|
||||
debounce_ms,
|
||||
serve,
|
||||
spawn,
|
||||
})
|
||||
}
|
||||
|
||||
fn print_autocomplete_start_help() {
|
||||
println!("Usage: openhuman autocomplete start [--debounce-ms <u64>] [--serve|--spawn]");
|
||||
println!();
|
||||
println!(" --debounce-ms <u64> Override debounce in milliseconds.");
|
||||
println!(" --serve Run autocomplete loop in the current foreground process.");
|
||||
println!(" --spawn Spawn autocomplete loop as a background process.");
|
||||
}
|
||||
|
||||
fn parse_function_params(
|
||||
schema: &ControllerSchema,
|
||||
args: &[String],
|
||||
@@ -238,7 +321,7 @@ fn grouped_schemas() -> BTreeMap<String, Vec<ControllerSchema>> {
|
||||
fn print_general_help(grouped: &BTreeMap<String, Vec<ControllerSchema>>) {
|
||||
println!("OpenHuman core CLI\n");
|
||||
println!("Usage:");
|
||||
println!(" openhuman run [--port <u16>]");
|
||||
println!(" openhuman run [--port <u16>] [--jsonrpc-only] [--verbose]");
|
||||
println!(" openhuman call --method <name> [--params '<json>']");
|
||||
println!(" openhuman <namespace> <function> [--param value ...]\n");
|
||||
println!("Available namespaces:");
|
||||
|
||||
+29
-3
@@ -95,6 +95,7 @@ pub fn build_core_http_router(socketio_enabled: bool) -> Router {
|
||||
.route("/events", get(events_handler))
|
||||
.route("/rpc", post(rpc_handler))
|
||||
.fallback(not_found_handler)
|
||||
.layer(middleware::from_fn(http_request_log_middleware))
|
||||
.layer(middleware::from_fn(cors_middleware))
|
||||
.with_state(AppState {
|
||||
core_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
@@ -109,6 +110,28 @@ pub fn build_core_http_router(socketio_enabled: bool) -> Router {
|
||||
router
|
||||
}
|
||||
|
||||
async fn http_request_log_middleware(req: Request, next: Next) -> Response {
|
||||
let method = req.method().clone();
|
||||
let path = req.uri().path().to_string();
|
||||
let query_len = req.uri().query().map(str::len).unwrap_or(0);
|
||||
let started = std::time::Instant::now();
|
||||
|
||||
let response = next.run(req).await;
|
||||
|
||||
let status = response.status().as_u16();
|
||||
let ms = started.elapsed().as_millis();
|
||||
log::info!(
|
||||
"[http] {} {}{} -> {} ({}ms)",
|
||||
method,
|
||||
path,
|
||||
if query_len > 0 { "?…" } else { "" },
|
||||
status,
|
||||
ms
|
||||
);
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
async fn cors_middleware(req: Request, next: Next) -> Response {
|
||||
if req.method() == Method::OPTIONS {
|
||||
return with_cors_headers(StatusCode::NO_CONTENT.into_response());
|
||||
@@ -230,10 +253,13 @@ pub async fn run_server(port: Option<u16>, socketio_enabled: bool) -> anyhow::Re
|
||||
|
||||
let app = build_core_http_router(socketio_enabled);
|
||||
|
||||
log::info!("[core] listening on http://{bind_addr}");
|
||||
log::info!("[rpc:http] JSON-RPC server running — POST http://{bind_addr}/rpc (JSON-RPC 2.0)");
|
||||
log::info!(
|
||||
"[core] OpenHuman core is ready — listening on http://{bind_addr} (version {})",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
log::info!("[rpc:http] JSON-RPC — POST http://{bind_addr}/rpc (JSON-RPC 2.0)");
|
||||
if socketio_enabled {
|
||||
log::info!("[rpc:socketio] Socket.IO server running — ws://{bind_addr}/socket.io/");
|
||||
log::info!("[rpc:socketio] Socket.IO — ws://{bind_addr}/socket.io/ (same HTTP server)");
|
||||
} else {
|
||||
log::info!("[rpc:socketio] disabled (--jsonrpc-only)");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
//! Logging for `openhuman run` (and other CLI paths that need stderr output).
|
||||
//!
|
||||
//! Without initializing a subscriber, `log::` and `tracing::` macros are no-ops.
|
||||
|
||||
use std::fmt;
|
||||
use std::io::{self, IsTerminal};
|
||||
use std::sync::Once;
|
||||
|
||||
use nu_ansi_term::{Color, Style};
|
||||
use tracing::{Event, Level};
|
||||
use tracing_subscriber::fmt::format::{FormatEvent, FormatFields, Writer};
|
||||
use tracing_subscriber::fmt::FmtContext;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
/// `14:32:01 <INFO> (jsonrpc) message…` — colors when stderr is a TTY.
|
||||
struct CleanCliFormat;
|
||||
|
||||
impl<S, N> FormatEvent<S, N> for CleanCliFormat
|
||||
where
|
||||
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
{
|
||||
fn format_event(
|
||||
&self,
|
||||
ctx: &FmtContext<'_, S, N>,
|
||||
mut writer: Writer<'_>,
|
||||
event: &Event<'_>,
|
||||
) -> fmt::Result {
|
||||
let meta = event.metadata();
|
||||
let time = chrono::Local::now().format("%H:%M:%S");
|
||||
let level = level_tag(meta.level());
|
||||
let target = short_target(meta.target());
|
||||
|
||||
if writer.has_ansi_escapes() {
|
||||
let time_styled = Style::new().dimmed().paint(time.to_string());
|
||||
write!(writer, "{time_styled} ")?;
|
||||
|
||||
let tag = format!("<{level}>");
|
||||
let level_styled = match *meta.level() {
|
||||
Level::ERROR => Style::new().fg(Color::Red).bold().paint(tag),
|
||||
Level::WARN => Style::new().fg(Color::Yellow).bold().paint(tag),
|
||||
Level::INFO => Style::new().fg(Color::Green).paint(tag),
|
||||
Level::DEBUG => Style::new().fg(Color::Cyan).paint(tag),
|
||||
Level::TRACE => Style::new().fg(Color::Magenta).dimmed().paint(tag),
|
||||
};
|
||||
write!(writer, "{level_styled} ")?;
|
||||
|
||||
let scope = format!("({target})");
|
||||
let scope_styled = Style::new().fg(Color::Fixed(247)).paint(scope);
|
||||
write!(writer, "{scope_styled} ")?;
|
||||
} else {
|
||||
write!(writer, "{time} <{level}> ({target}) ")?;
|
||||
}
|
||||
|
||||
ctx.field_format().format_fields(writer.by_ref(), event)?;
|
||||
writeln!(writer)
|
||||
}
|
||||
}
|
||||
|
||||
fn level_tag(level: &Level) -> &'static str {
|
||||
match *level {
|
||||
Level::ERROR => "ERROR",
|
||||
Level::WARN => "WARN",
|
||||
Level::INFO => "INFO",
|
||||
Level::DEBUG => "DEBUG",
|
||||
Level::TRACE => "TRACE",
|
||||
}
|
||||
}
|
||||
|
||||
fn short_target(target: &str) -> &str {
|
||||
target.rsplit("::").next().unwrap_or(target)
|
||||
}
|
||||
|
||||
/// Initialize `tracing` + bridge the `log` crate so existing `log::info!` calls appear.
|
||||
///
|
||||
/// - If `RUST_LOG` is unset: uses `info`, or `debug` when `verbose` is true.
|
||||
/// - Safe to call once; subsequent calls are ignored.
|
||||
pub fn init_for_cli_run(verbose: bool) {
|
||||
INIT.call_once(|| {
|
||||
if std::env::var_os("RUST_LOG").is_none() {
|
||||
std::env::set_var("RUST_LOG", if verbose { "debug" } else { "info" });
|
||||
}
|
||||
|
||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
tracing_subscriber::EnvFilter::new(if verbose { "debug" } else { "info" })
|
||||
});
|
||||
|
||||
let use_color = io::stderr().is_terminal();
|
||||
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_ansi(use_color)
|
||||
.with_env_filter(filter)
|
||||
.event_format(CleanCliFormat)
|
||||
.try_init();
|
||||
|
||||
let _ = tracing_log::LogTracer::init();
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod all;
|
||||
pub mod cli;
|
||||
pub mod dispatch;
|
||||
pub mod jsonrpc;
|
||||
pub mod logging;
|
||||
pub mod rpc_log;
|
||||
pub mod socketio;
|
||||
pub mod types;
|
||||
|
||||
+10
-7
@@ -59,14 +59,14 @@ struct ChatCancelPayload {
|
||||
pub fn attach_socketio() -> (socketioxide::layer::SocketIoLayer, SocketIo) {
|
||||
let (layer, io) = SocketIo::new_layer();
|
||||
|
||||
log::debug!(
|
||||
"[socketio] configured with req_path={}",
|
||||
log::info!(
|
||||
"[socketio] engine ready (namespace /, path {})",
|
||||
io.config().engine_config.req_path
|
||||
);
|
||||
|
||||
io.ns("/", |socket: SocketRef| {
|
||||
let client_id = socket.id.to_string();
|
||||
log::debug!("[socketio] connect client_id={client_id}");
|
||||
log::info!("[socketio] client connected id={client_id}");
|
||||
let _ = socket.join(client_id.clone());
|
||||
let ready_payload = json!({ "sid": client_id });
|
||||
log::debug!("[socketio] emit event=ready to_client={}", socket.id);
|
||||
@@ -74,11 +74,14 @@ pub fn attach_socketio() -> (socketioxide::layer::SocketIoLayer, SocketIo) {
|
||||
|
||||
socket.on("rpc:request", |socket: SocketRef, Data(payload): Data<SocketRpcRequest>| async move {
|
||||
let client_id = socket.id.to_string();
|
||||
log::debug!(
|
||||
"[socketio] recv event=rpc:request client_id={} id={} method={} params_type={} params_bytes={}",
|
||||
client_id,
|
||||
payload.id,
|
||||
log::info!(
|
||||
"[socketio] rpc:request method={} id={} client={}",
|
||||
payload.method,
|
||||
payload.id,
|
||||
client_id
|
||||
);
|
||||
log::debug!(
|
||||
"[socketio] rpc:request params_type={} params_bytes={}",
|
||||
json_type_name(&payload.params),
|
||||
payload.params.to_string().len()
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user