From 652bb6e2f9b115a6ccbffc4e09444d93e02ee100 Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Sun, 29 Mar 2026 22:09:08 -0700 Subject: [PATCH] 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. --- .github/workflows/macos-arm64-build.yml | 138 ++++++++ .github/workflows/release.yml | 53 ++- Cargo.lock | 20 +- Cargo.toml | 2 + app/src/pages/Conversations.tsx | 111 +++++- scripts/ci-secrets.example.json | 2 + scripts/prepareTauriConfig.js | 28 +- scripts/test-release-act.sh | 10 +- src/core/cli.rs | 87 ++++- src/core/jsonrpc.rs | 32 +- src/core/logging.rs | 100 ++++++ src/core/mod.rs | 1 + src/core/socketio.rs | 17 +- src/openhuman/agent/agent.rs | 1 + src/openhuman/agent/dispatcher.rs | 10 +- src/openhuman/agent/prompt.rs | 69 +++- src/openhuman/autocomplete/core.rs | 452 +++++++++++++++++++++++- src/openhuman/providers/compatible.rs | 220 +++++++++++- 18 files changed, 1279 insertions(+), 74 deletions(-) create mode 100644 .github/workflows/macos-arm64-build.yml create mode 100644 src/core/logging.rs diff --git a/.github/workflows/macos-arm64-build.yml b/.github/workflows/macos-arm64-build.yml new file mode 100644 index 000000000..6282aa895 --- /dev/null +++ b/.github/workflows/macos-arm64-build.yml @@ -0,0 +1,138 @@ +name: macOS ARM64 Build + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: macos-arm64-build-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-signed-macos-arm64: + name: Build signed macOS ARM64 bundle + runs-on: macos-latest + environment: Production + env: + TARGET: aarch64-apple-darwin + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + submodules: true + + - name: Set Xcode version + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Setup Node.js 24.x + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: yarn + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@1.93.0 + with: + targets: aarch64-apple-darwin + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Validate signing prerequisites + shell: bash + env: + UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY || vars.UPDATER_PUBLIC_KEY }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }} + APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + for var in UPDATER_PUBLIC_KEY TAURI_SIGNING_PRIVATE_KEY APPLE_CERTIFICATE_BASE64 APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do + if [ -z "${!var}" ]; then + echo "Missing required secret/variable: $var" + exit 1 + fi + done + + - name: Define Tauri configuration overrides + id: config-overrides + uses: actions/github-script@v7 + env: + BASE_URL: ${{ vars.BASE_URL }} + UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY || vars.UPDATER_PUBLIC_KEY }} + UPDATER_ENDPOINT: ${{ vars.UPDATER_ENDPOINT }} + UPDATER_REPO: tinyhumansai/openhuman + WITH_UPDATER: "true" + with: + script: | + const workspacePath = process.env.GITHUB_WORKSPACE.replace(/\\/g, '/'); + const prefix = workspacePath.startsWith('/') ? 'file://' : 'file:///'; + const moduleUrl = `${prefix}${workspacePath}/scripts/prepareTauriConfig.js`; + const { default: prepareTauriConfig } = await import(moduleUrl); + const config = prepareTauriConfig(); + core.setOutput('json', JSON.stringify(config)); + + - name: Build frontend + run: yarn workspace openhuman-app build + env: + NODE_ENV: production + VITE_BACKEND_URL: ${{ vars.VITE_BACKEND_URL }} + VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }} + VITE_DEBUG: ${{ vars.VITE_DEBUG }} + + - name: Build sidecar core binary + run: cargo build --manifest-path Cargo.toml --release --target "$TARGET" --bin openhuman + + - name: Stage sidecar for Tauri bundler + shell: bash + run: | + mkdir -p app/src-tauri/binaries + cp "target/$TARGET/release/openhuman" "app/src-tauri/binaries/openhuman-$TARGET" + chmod +x "app/src-tauri/binaries/openhuman-$TARGET" + + - name: Build signed app and dmg + working-directory: app + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.UPDATER_PRIVATE_KEY_PASSWORD }} + MACOSX_DEPLOYMENT_TARGET: "10.15" + run: | + yarn tauri build -c '${{ steps.config-overrides.outputs.json }}' --target "$TARGET" --bundles app,dmg -- --bin OpenHuman + + - name: Verify macOS app bundle sidecar layout + shell: bash + run: | + APP_PATH="target/$TARGET/release/bundle/macos/OpenHuman.app" + echo "Inspecting bundle at: $APP_PATH" + ls -la "$APP_PATH/Contents/MacOS" + ls -la "$APP_PATH/Contents/Resources" | grep openhuman || true + if [ -f "$APP_PATH/Contents/MacOS/openhuman" ]; then + echo "Unexpected standalone core binary found in MacOS dir" + exit 1 + fi + if ! ls "$APP_PATH/Contents/Resources"/openhuman-* >/dev/null 2>&1; then + echo "Sidecar core binary missing from app resources" + exit 1 + fi + + - name: Upload signed macOS artifacts + uses: actions/upload-artifact@v4 + with: + name: macos-arm64-signed-bundles + path: | + target/${{ env.TARGET }}/release/bundle/macos/*.app + target/${{ env.TARGET }}/release/bundle/dmg/*.dmg diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 359a0de1b..a1c4f08ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -209,15 +209,14 @@ jobs: fail-fast: false matrix: settings: - # macOS builds are disabled for now to ensure the flow works first - # - platform: macos-latest - # args: --target aarch64-apple-darwin - # target: aarch64-apple-darwin - # artifact_suffix: aarch64-apple-darwin - # - platform: macos-latest - # args: --target x86_64-apple-darwin - # target: x86_64-apple-darwin - # artifact_suffix: x86_64-apple-darwin + - platform: macos-latest + args: --target aarch64-apple-darwin + target: aarch64-apple-darwin + artifact_suffix: aarch64-apple-darwin + - platform: macos-latest + args: --target x86_64-apple-darwin + target: x86_64-apple-darwin + artifact_suffix: x86_64-apple-darwin - platform: ubuntu-22.04 args: --target x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu @@ -275,12 +274,46 @@ jobs: - name: Install dependencies run: yarn install --frozen-lockfile + - name: Validate signing prerequisites + shell: bash + env: + MATRIX_PLATFORM: ${{ matrix.settings.platform }} + UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY || vars.UPDATER_PUBLIC_KEY }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }} + APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + if [ -z "$UPDATER_PUBLIC_KEY" ]; then + echo "Missing UPDATER_PUBLIC_KEY (set as secret or repo/environment variable)." + exit 1 + fi + + if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then + echo "Missing TAURI_SIGNING_PRIVATE_KEY (or fallback UPDATER_PRIVATE_KEY)." + exit 1 + fi + + if [ "$MATRIX_PLATFORM" = "macos-latest" ]; then + for var in APPLE_CERTIFICATE_BASE64 APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do + if [ -z "${!var}" ]; then + echo "Missing required macOS signing secret: $var" + exit 1 + fi + done + fi + - name: Define Tauri configuration overrides id: config-overrides uses: actions/github-script@v7 env: BASE_URL: ${{ vars.BASE_URL }} - UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY }} + UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY || vars.UPDATER_PUBLIC_KEY }} + UPDATER_ENDPOINT: ${{ vars.UPDATER_ENDPOINT }} + UPDATER_REPO: tinyhumansai/openhuman WITH_UPDATER: "true" with: script: | diff --git a/Cargo.lock b/Cargo.lock index 043deb049..7f86fc02d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4753,6 +4753,16 @@ dependencies = [ "nom 8.0.0", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -5070,6 +5080,7 @@ dependencies = [ "log", "mail-parser", "matrix-sdk", + "nu-ansi-term 0.46.0", "nusb 0.2.3", "once_cell", "openssl", @@ -5110,6 +5121,7 @@ dependencies = [ "toml 1.1.0+spec-1.1.0", "tower", "tracing", + "tracing-log", "tracing-subscriber", "url", "urlencoding", @@ -5243,6 +5255,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "pango" version = "0.18.3" @@ -8570,7 +8588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", - "nu-ansi-term", + "nu-ansi-term 0.50.3", "once_cell", "regex-automata", "sharded-slab", diff --git a/Cargo.toml b/Cargo.toml index a2c100e64..cd52499c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ tokio = { version = "1", features = ["full", "sync"] } once_cell = "1.19" parking_lot = "0.12" log = "0.4" +nu-ansi-term = "0.46" env_logger = "0.11" base64 = "0.22" aes-gcm = "0.10" @@ -44,6 +45,7 @@ toml = "1.0" shellexpand = "3.1" schemars = "1.2" tracing = { version = "0.1", default-features = false } +tracing-log = "0.2" tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi", "env-filter"] } prometheus = { version = "0.14", default-features = false } urlencoding = "2.1" diff --git a/app/src/pages/Conversations.tsx b/app/src/pages/Conversations.tsx index 69d47d6d2..97a192cde 100644 --- a/app/src/pages/Conversations.tsx +++ b/app/src/pages/Conversations.tsx @@ -26,13 +26,20 @@ import { setSelectedThread, } from '../store/threadSlice'; import type { ThreadMessage } from '../types/thread'; -import { openhumanLocalAiTranscribeBytes, openhumanLocalAiTts } from '../utils/tauriCommands'; +import { + isTauri, + openhumanAutocompleteAccept, + openhumanAutocompleteCurrent, + openhumanLocalAiTranscribeBytes, + openhumanLocalAiTts, +} from '../utils/tauriCommands'; const DEFAULT_THREAD_ID = 'default-thread'; const DEFAULT_THREAD_TITLE = 'Conversation'; type ToolTimelineEntryStatus = 'running' | 'success' | 'error'; type InputMode = 'text' | 'voice'; type ReplyMode = 'text' | 'voice'; +const AUTOCOMPLETE_POLL_DEBOUNCE_MS = 180; interface ToolTimelineEntry { id: string; @@ -54,6 +61,25 @@ function formatRelativeTime(dateStr: string): string { return `${days}d ago`; } +function getInlineCompletionSuffix(input: string, suggestion: string): string { + const normalizedInput = input; + const normalizedSuggestion = suggestion; + + if (!normalizedInput || !normalizedSuggestion) { + return ''; + } + + if (normalizedSuggestion === normalizedInput) { + return ''; + } + + if (normalizedSuggestion.startsWith(normalizedInput)) { + return normalizedSuggestion.slice(normalizedInput.length); + } + + return ''; +} + const Conversations = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); @@ -76,6 +102,7 @@ const Conversations = () => { const [isTranscribing, setIsTranscribing] = useState(false); const [voiceStatus, setVoiceStatus] = useState(null); const [isPlayingReply, setIsPlayingReply] = useState(false); + const [inlineSuggestionValue, setInlineSuggestionValue] = useState(''); const [availableModels, setAvailableModels] = useState([]); const [selectedModel, setSelectedModel] = useState('neocortex-mk1'); @@ -102,6 +129,8 @@ const Conversations = () => { const audioChunksRef = useRef([]); const replyAudioRef = useRef(null); const lastSpokenMessageIdRef = useRef(null); + const autocompleteDebounceRef = useRef(null); + const autocompleteRequestSeqRef = useRef(0); const getAudioExtension = (mimeType: string): string => { const lower = mimeType.toLowerCase(); @@ -184,6 +213,45 @@ const Conversations = () => { } }, [inputValue, sendError]); + useEffect(() => { + if ( + !isTauri() || + !rustChat || + inputMode !== 'text' || + isSending || + inputValue.trim().length === 0 + ) { + setInlineSuggestionValue(''); + return; + } + + if (autocompleteDebounceRef.current !== null) { + window.clearTimeout(autocompleteDebounceRef.current); + } + + autocompleteDebounceRef.current = window.setTimeout(() => { + const requestSeq = autocompleteRequestSeqRef.current + 1; + autocompleteRequestSeqRef.current = requestSeq; + + void openhumanAutocompleteCurrent({ context: inputValue }) + .then(response => { + if (autocompleteRequestSeqRef.current !== requestSeq) return; + setInlineSuggestionValue(response.result.suggestion?.value ?? ''); + }) + .catch(() => { + if (autocompleteRequestSeqRef.current !== requestSeq) return; + setInlineSuggestionValue(''); + }); + }, AUTOCOMPLETE_POLL_DEBOUNCE_MS); + + return () => { + if (autocompleteDebounceRef.current !== null) { + window.clearTimeout(autocompleteDebounceRef.current); + autocompleteDebounceRef.current = null; + } + }; + }, [inputValue, inputMode, isSending, rustChat]); + useEffect(() => { return () => { mediaRecorderRef.current?.stop(); @@ -508,6 +576,20 @@ const Conversations = () => { }, [messages, replyMode, rustChat]); const handleInputKeyDown = (e: React.KeyboardEvent) => { + const inlineSuffix = getInlineCompletionSuffix(inputValue, inlineSuggestionValue); + + if (e.key === 'Tab' && inlineSuffix.length > 0) { + e.preventDefault(); + setInputValue(prev => prev + inlineSuffix); + setInlineSuggestionValue(''); + if (isTauri()) { + void openhumanAutocompleteAccept({ suggestion: inputValue + inlineSuffix }).catch(() => { + // Keep local UX smooth even if accept RPC fails. + }); + } + return; + } + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void handleSendMessage(); @@ -528,6 +610,7 @@ const Conversations = () => { const selectedThreadToolTimeline = selectedThreadId ? (toolTimelineByThread[selectedThreadId] ?? []) : []; + const inlineCompletionSuffix = getInlineCompletionSuffix(inputValue, inlineSuggestionValue); return (
@@ -903,15 +986,23 @@ const Conversations = () => { {inputMode === 'text' ? (
-