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:
Steven Enamakel
2026-03-29 22:09:08 -07:00
committed by GitHub
parent 22435ed631
commit 652bb6e2f9
18 changed files with 1279 additions and 74 deletions
+85 -2
View File
@@ -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
View File
@@ -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)");
}
+100
View File
@@ -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();
});
}
+1
View File
@@ -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
View File
@@ -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()
);