44a0fe0359aee336b38c6553077378b6777839d8
1110 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2d38c33da3 |
fix(tui): Ctrl+C shows the same partial reply the session saves
interruptTurn seals bufRef at the keypress and drops later message.delta frames, but the agent keeps streaming until it notices the interrupt and persists every delta it delivered. Deltas already in flight were saved to state.db (and replayed next turn) without ever being drawn. The interrupted message.complete carries that persisted partial; when it extends the sealed text, the interrupted bubble now takes it (or a bubble is added when nothing had been shown yet). |
||
|
|
aae6c2044e |
fmt(js): npm run fix on merge (#123390)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
fd602278c7 |
fix(tui-gateway): report Fast only where the route accepts the priority tier
A profile-wide agent.service_tier: fast reaches every new session, including local llama.cpp ones. The request builders already drop the fast parameter on routes that do not bill for it, but session.info reported fast: true whenever the tier was priority. The desktop then kept the Fast switch visible so it could be turned off, tagged the model row Fast and appended "· Fast" to the pill for a local model. session.info now asks resolve_fast_mode_overrides, the gate the request builders use, before it reports fast. While a model switch is pending, the new pick decides, because the agent's base URL still belongs to the old route. service_tier is still reported as stored. The TUI status bar reads info.fast alone. |
||
|
|
87d1c5b0c3 | refactor(tui): drop redundant native-mode guards | ||
|
|
7e35460ac2 | refactor(tui): simplify native mode paths | ||
|
|
7ecd29c293 | perf(tui): add native history benchmark | ||
|
|
d03e3aa390 | feat(tui): render native mode in terminal flow | ||
|
|
0f65d698d0 | Merge remote-tracking branch 'origin/main' into ethie/pm-clean | ||
|
|
c502cd026d |
refactor(hermes-ink): reuse one hasMultipleCodepoints and drop the pre-clear SGR reset
Export the surrogate-aware helper from termio/parser.ts instead of a near-identical copy in log-update.ts. The combining-grapheme pre-clear no longer resets style/hyperlink before its space (the glyph overwrites it immediately), keeping SGR/OSC8 coalescing across runs. The isEmptyCellAt guard stays as the last condition, so a fresh multi-codepoint draw onto an empty cell writes only the glyph (no leading space or cursor move). Co-authored-by: tynamite <35367599+tynamite@users.noreply.github.com> |
||
|
|
0eecdb02ec |
fix(ui-tui): only flush the streamed tail when message.complete text lacks it
The #61520 flush ran on ordinary tool turns where the buffered tail IS the final answer, attaching the tool shelf to the answer and moving the trail below it. Skip the flush when completion text already contains the tail; cover both the text and #16391 no-text paths. Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com> |
||
|
|
c1648af8f7 | test(hermes-ink): trim combining-grapheme pre-clear tests to two invariants | ||
|
|
5590e30b77 |
docs(ui-tui): document recordError tail semantics next to the #61520 flush
Sibling-site check for the message.complete tail fix: recordError drops the whole unsealed turn and interruptTurn preserves the tail as `partial`, so neither needs the flush; record that contract in place. Co-authored-by: liuhao1024 <sunsky.lau@gmail.com> |
||
|
|
caae35a005 |
fix(tui): clear combining graphemes before redraw
(cherry picked from commit 87502e4455bd79d4b86cecfa993eabc289613db5) |
||
|
|
051d888e55 |
fix(ui-tui): flush the buffered streaming tail at message.complete
`TurnController.recordMessageComplete` was the only turn-end site in the class that never called `flushStreamingSegment()`. Every other site that ends or seals a stream does — `pushInlineDiffSegment`, the interim seal, `recordToolStart`, `recordInlineDiffToolComplete` — and `interruptTurn` preserves the buffer explicitly. The consequence: when the gateway supplies its own final answer via `payload.text` (or `payload.rendered`), `this.bufRef` is not the source of that answer, so any text streamed after the last flush — typically the narration between a tool call and the end of the turn — was still sitting in the buffer when `idle()` cleared it. The user watched that block render live and then saw it vanish from the transcript at turn end. Flush the buffer as a segment instead of appending it to `finalMessages`. Because the segment spread already sits ahead of the final assistant text, stream order is preserved structurally rather than by hand-ordering a push, and `finalTail()` strips the tail out of `finalText` when the completion payload already contains it, so no duplicate is rendered. Pending tool rows attach to the flushed segment through the same `pushSegment` path used at every other flush site, so they still appear exactly once. The flush is skipped when neither `payload.text` nor `payload.rendered` is present: in that case `rawText` IS the buffer (the #16391 fallback), and flushing would publish the final answer as a segment instead of the final assistant message. (cherry picked from commit 8a88c06033f524e1915778e1a8e93388604de34f) |
||
|
|
cb7b18431a |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts: # apps/desktop/src/app/settings/connections-registry.tsx # scripts/install.ps1 # scripts/install.sh # tests/hermes_cli/test_update_autostash.py |
||
|
|
0641ee1dcd |
fix(tui-gateway): never park an idle /steer on the agent
With no live turn, AIAgent.steer() still accepted the text and the gateway answered 'Steer queued'. Nothing drained it until the next turn's pre-API drain spliced it after the newest tool row, possibly one from an earlier turn (misplaced, cache-breaking), or it was requeued. - command.dispatch /steer: idle sends the text as the next turn with a notice (CLI parity) instead of calling agent.steer(). - session.steer: idle answers 'rejected' without touching the agent, so clients fall back to their next-turn queue. - TUI /steer: a rejected steer is queued for the next turn, not dropped. Fixes #64578 |
||
|
|
97de4b4ef8 |
Merge origin/main into ethie/pm-clean
Conflict resolutions and semantic fixups: - utils.py / hermes_yaml.py: main widened ruamel's round-trip emitter so a long double-quoted scalar is never folded after an escaped backslash. pm-clean builds every rt emitter through hermes_yaml.roundtrip_yaml(), so the width lives there (ROUNDTRIP_YAML_WIDTH moves with it); xai_retirement imports it from hermes_yaml. - hermes_cli/banner.py: keep pm-clean's removal of the banner update check. Main's GIT_NO_LAZY_FETCH fix for it applies to its replacement, source_check: every read-only probe (source_git_env) now refuses promisor lazy fetches, and the partial-clone test targets that probe (red without the flag). - .github/workflows/tests.yml: keep setup-pm; main's uv pin bump does not apply. Main's WAL-capable SQLite gates are kept, run against $HERMES_PYTHON (the PM-pinned interpreter, SQLite 3.53.1). The e2e step takes main's --include-integration invocation. - apps/desktop: package.json has no build block here, so main's macOS locale-marker restore joins the darwin branch of the existing after-pack.mjs, and its test loads the hook from electron-builder.config.cjs and imports PlatformPackager from app-builder-lib's root (electron-builder 27 exports no ./out paths). The win32 row is dropped: this hook sanitizes and signs PE trees on win32 by design. - reconciliation.ts: main's rowId hydration (#119326) was merged into the first of pm-clean's split helpers only; the resolver is now one helper both halves use. - en.ts: both sides' keys kept. tests/tools/test_lazy_deps.py stays deleted. - Tests main added with `import yaml` use hermes_yaml, like the rest of the tree. |
||
|
|
65ad5296ea |
fix(cli): make function-key workflows reliable on macOS (#120666)
* fix(tui): preserve function-key events * fix(cli): add portable dock shortcut * test: update dock shortcut expectations |
||
|
|
16652eea18 |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts: # gateway/config.py # gateway/config_loader.py # gateway/readiness.py # hermes_cli/managed_scope.py # hermes_cli/plugin_python_deps.py # hermes_cli/plugins_cmd.py # hermes_cli/update_cmd_maint.py # plugin-catalog/hindsight.yaml # plugins/plugin_loader.py # providers/__init__.py # scripts/run_tests.sh # tests/gateway/test_control_socket_windows_live.py # tests/gateway/test_gateway_streaming_nested_config.py # tests/hermes_cli/test_doctor.py # tests/hermes_cli/test_plan_reconciliation_windows_live.py # tests/hermes_cli/test_update_apply_shallow_count.py # tests/hermes_cli/test_update_concurrent_quarantine.py # tests/hermes_cli/test_update_shim_self_lock.py # tests/hermes_cli/test_verify_console_scripts.py # tests/tools/test_lazy_deps.py # tests/tui_gateway/test_subprocess_encoding.py # tools/lazy_deps.py |
||
|
|
6300f73659 |
test: restore TUI status-rule clock and title guards dropped by #120071
- appChromeBlockedTimers: 'status-chrome timers track the current overlay model' (#12463). Each overlay state is mounted on the real StatusRule: the in-flow prompts (approval, billing, clarify, confirm, secret, subscription, sudo), agents/journey and the ambient dock keep the 1s clocks running, and the occluding set pauses them. Gating clarify/ambient in $isStatusRuleOccluded turns it red. - appChromeBlockedTimers: the existing 're-syncs the elapsed read-outs' test flaked under load (a fixed 20ms flush); it now polls for the reveal frame. - appChromeStatusRule: session title renders without a raw accent background fill (#82465 contrast bug). - appChromeStatusRule: idle-since read-out is shown when idle, hidden while busy and before the first turn completes. |
||
|
|
1753653450 |
feat(tui): goal row above the live-work dock
The Ink TUI already lists queued follow-ups above the composer; a standing
/goal only surfaced as transient status text after each judged turn. A one-
line row above the live-work dock now shows it: "⊙ goal · 3/20 turns ·
<title>", "⏳ goal parked · until 14:32 · <reason>" or "⏸ goal paused ·
<reason>", and leaves once the goal is done or cleared.
State comes from the existing session.control contract: one
session.control.read when the session changes, then the
session.control.update events the backend already publishes after /goal
commands and judged turns. No new polling of state.db.
|
||
|
|
02f1364383 | fix: reconcile Termux TUI and platform tests | ||
|
|
a43f8c2dd7 | Merge remote-tracking branch 'origin/main' into ethie/pm-clean | ||
|
|
890bbbda1f |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts: # apps/desktop/e2e/archived-hidden-session-recoverable.spec.ts # apps/desktop/e2e/bot-chat-message-agent-friendly-name.spec.ts # apps/desktop/e2e/bot-mailbox-unreadable-ticket.spec.ts # apps/desktop/e2e/bot-mode-roster-localized.spec.ts # apps/desktop/e2e/bot-mode-row-click-mirrors-registry.spec.ts # apps/desktop/e2e/bot-mode-tab-shows-bot-name.spec.ts # apps/desktop/e2e/bot-roster-group-row-organisation.spec.ts # apps/desktop/e2e/bot-roster-ignores-infra-dirs.spec.ts # apps/desktop/e2e/bot-roster-timestamp-meta.spec.ts # apps/desktop/e2e/bot-roster-user-sections.spec.ts # apps/desktop/e2e/bot-routines-pane-narrow.spec.ts # apps/desktop/e2e/bot-row-open-recent-session.spec.ts # apps/desktop/e2e/bot-tile-ignores-ambient-composer-model.spec.ts # apps/desktop/e2e/group-composer-auto-grow.spec.ts # apps/desktop/e2e/group-create-gate-remote-roster.spec.ts # apps/desktop/e2e/group-prompt-renamed-primary-handle.spec.ts # apps/desktop/e2e/hosted-room-backend-continuity.spec.ts # apps/desktop/e2e/hosted-room-legacy-store-migration.spec.ts # apps/desktop/e2e/settings-scope-chips-bot-title.spec.ts # apps/desktop/e2e/worktree-branch-status.spec.ts # apps/desktop/electron/backend-probes.test.ts # apps/desktop/electron/connection-apply.test.ts # apps/desktop/electron/desktop-electron-pin.test.ts # apps/desktop/electron/desktop-uninstall.test.ts # apps/desktop/electron/gateway-file-download-transport.test.ts # apps/desktop/electron/gateway-stop-before-update.test.ts # apps/desktop/electron/github-api-auth.test.ts # apps/desktop/electron/registry-primary-profile-scope.test.ts # apps/desktop/electron/update-api-check.test.ts # apps/desktop/electron/update-handoff-marker.test.ts # apps/desktop/electron/venv-blocker-scan.test.ts # apps/desktop/scripts/after-extract.test.mjs # apps/desktop/scripts/local-pack-publish.test.mjs # apps/desktop/scripts/tasks-scroll.test.mjs # apps/desktop/src/app/settings/model-settings.test.tsx # apps/desktop/src/app/updates-overlay.blockers.test.tsx # apps/desktop/src/components/desktop-install-overlay.test.tsx # apps/desktop/src/lib/update-copy.test.ts # scripts/ci/check_os_marker_fakes.py # tests-js/desktop-mac-usage-descriptions.test.ts # tests-js/node-engine-alignment.test.ts # tests/agent/lsp/test_install_and_lint_fixes.py # tests/agent/test_command_token_source.py # tests/agent/test_compression_boundary_hook.py # tests/agent/test_create_openai_client_ssl_verify.py # tests/agent/test_custom_provider_ca_probes.py # tests/agent/test_endpoint_blackhole.py # tests/agent/test_estimator_parity.py # tests/agent/test_in_place_compaction.py # tests/agent/test_moa_loop_mode.py # tests/agent/test_model_metadata.py # tests/agent/test_skill_session_platform_gate.py # tests/agent/test_skill_utils.py # tests/agent/test_ssl_ca_guard.py # tests/computer_use/test_doctor.py # tests/cron/test_codex_execution_paths.py # tests/cron/test_cron_bot_chat_delivery.py # tests/cron/test_cron_script.py # tests/cron/test_media_delivery_parity.py # tests/cron/test_misfire_catchup.py # tests/cron/test_parallel_pool.py # tests/cron/test_recurring_eagain_redispatch.py # tests/gateway/test_choice_picker.py # tests/gateway/test_control_socket_windows_live.py # tests/gateway/test_dingtalk.py # tests/gateway/test_feishu.py # tests/gateway/test_feishu_onboard.py # tests/gateway/test_gateway_shutdown.py # tests/gateway/test_matrix.py # tests/gateway/test_model_command_custom_providers.py # tests/gateway/test_reasoning_command.py # tests/gateway/test_runtime_footer.py # tests/gateway/test_session.py # tests/gateway/test_session_hygiene.py # tests/gateway/test_status.py # tests/gateway/test_teams.py # tests/gateway/test_turn_lease.py # tests/gateway/test_whatsapp_connect.py # tests/hermes_cli/test_approvals_command.py # tests/hermes_cli/test_auth_store_lock_concurrent.py # tests/hermes_cli/test_backup.py # tests/hermes_cli/test_banner_git_state.py # tests/hermes_cli/test_certifi_repair.py # tests/hermes_cli/test_cmd_update.py # tests/hermes_cli/test_compat_manifest_targets.py # tests/hermes_cli/test_computer_use_cli.py # tests/hermes_cli/test_cpr_local_leak.py # tests/hermes_cli/test_dashboard_auth_gate.py # tests/hermes_cli/test_dashboard_procs_kill_grace.py # tests/hermes_cli/test_desktop_lifecycle_windows_live.py # tests/hermes_cli/test_doctor.py # tests/hermes_cli/test_doctor_command_install.py # tests/hermes_cli/test_fleet_config_migration_windows_live.py # tests/hermes_cli/test_gateway.py # tests/hermes_cli/test_gateway_platform_gating.py # tests/hermes_cli/test_gateway_restart_loop.py # tests/hermes_cli/test_gateway_task_probe.py # tests/hermes_cli/test_gateway_wsl.py # tests/hermes_cli/test_gui_command.py # tests/hermes_cli/test_install_cua_driver.py # tests/hermes_cli/test_kanban_db.py # tests/hermes_cli/test_lazy_command_exports.py # tests/hermes_cli/test_lazy_refresh_venv_repair.py # tests/hermes_cli/test_linux_desktop_entry.py # tests/hermes_cli/test_local_runtime.py # tests/hermes_cli/test_local_runtime_updates.py # tests/hermes_cli/test_managed_uv.py # tests/hermes_cli/test_mcp_reload_confirm_gate.py # tests/hermes_cli/test_nous_subscription.py # tests/hermes_cli/test_npm_engine.py # tests/hermes_cli/test_personality_none.py # tests/hermes_cli/test_pet_toggle.py # tests/hermes_cli/test_plan_reconciliation_windows_live.py # tests/hermes_cli/test_plugin_event_bus.py # tests/hermes_cli/test_plugin_manifest_v2.py # tests/hermes_cli/test_plugin_packs.py # tests/hermes_cli/test_plugins_cmd.py # tests/hermes_cli/test_plugins_cmd_enable_disable_nested.py # tests/hermes_cli/test_process_identity.py # tests/hermes_cli/test_profiles.py # tests/hermes_cli/test_profiles_sidebar_cache.py # tests/hermes_cli/test_pty_bridge.py # tests/hermes_cli/test_resolve_turn_limit.py # tests/hermes_cli/test_serve_runtime_inventory.py # tests/hermes_cli/test_session_vacuum_config.py # tests/hermes_cli/test_set_config_value.py # tests/hermes_cli/test_signal_handler_kanban_worker.py # tests/hermes_cli/test_slash_confirm_windows.py # tests/hermes_cli/test_stale_pid_guard.py # tests/hermes_cli/test_startup_fast_guards.py # tests/hermes_cli/test_status.py # tests/hermes_cli/test_telegram_managed_bot.py # tests/hermes_cli/test_tools_config.py # tests/hermes_cli/test_update_apply_shallow_count.py # tests/hermes_cli/test_update_autostash.py # tests/hermes_cli/test_update_concurrent_quarantine.py # tests/hermes_cli/test_update_fetch_failure_classifier.py # tests/hermes_cli/test_update_fleet_probe_resume_token.py # tests/hermes_cli/test_update_handoff_backend_reap.py # tests/hermes_cli/test_update_handoff_desktop_rebuild.py # tests/hermes_cli/test_update_head_moved_gate.py # tests/hermes_cli/test_update_host_obligation.py # tests/hermes_cli/test_update_import_guard.py # tests/hermes_cli/test_update_interrupted_recovery.py # tests/hermes_cli/test_update_inventory.py # tests/hermes_cli/test_update_launchd_unloaded_gateway.py # tests/hermes_cli/test_update_missing_configured_deps.py # tests/hermes_cli/test_update_modified_notice.py # tests/hermes_cli/test_update_multiplex_migration_hook.py # tests/hermes_cli/test_update_no_gateway_restart.py # tests/hermes_cli/test_update_orphan_backend_reap.py # tests/hermes_cli/test_update_parked_branch_guard.py # tests/hermes_cli/test_update_post_pull_syntax_guard.py # tests/hermes_cli/test_update_receipt.py # tests/hermes_cli/test_update_self_lock.py # tests/hermes_cli/test_update_shim_fail_closed.py # tests/hermes_cli/test_update_shim_self_lock.py # tests/hermes_cli/test_update_sqlite_remediation.py # tests/hermes_cli/test_update_stale_dashboard.py # tests/hermes_cli/test_update_stale_virtualenv.py # tests/hermes_cli/test_update_venv_health.py # tests/hermes_cli/test_update_venv_ownership_preflight.py # tests/hermes_cli/test_update_wedged_gateway.py # tests/hermes_cli/test_update_yes_flag.py # tests/hermes_cli/test_update_zip_two_phase.py # tests/hermes_cli/test_urllib_security.py # tests/hermes_cli/test_ux_messages_auth_config.py # tests/hermes_cli/test_ux_messages_startup.py # tests/hermes_cli/test_venv_holder_classifier.py # tests/hermes_cli/test_verify_console_scripts.py # tests/hermes_cli/test_verify_core_dependencies.py # tests/hermes_cli/test_web_server.py # tests/hermes_cli/test_web_server_console_ws.py # tests/hermes_cli/test_web_server_ws_ping.py # tests/hermes_cli/test_web_ui_build.py # tests/hermes_state/test_fts_rebuild_admission.py # tests/hermes_state/test_hermes_state.py # tests/plugins/memory/test_memory_lazy_install.py # tests/plugins/test_google_meet_plugin.py # tests/plugins/test_langfuse_plugin.py # tests/plugins/test_security_guidance_plugin.py # tests/plugins/test_transform_llm_output_hook.py # tests/scripts/desktop_update/test_desktop_update_windows_gateway_flag.py # tests/scripts/desktop_update/test_desktop_update_windows_python_handoff.py # tests/scripts/desktop_update/test_desktop_update_windows_timestamp.py # tests/scripts/install/test_install_clone_throttle_fallback.py # tests/scripts/install/test_install_lockfile_churn.py # tests/scripts/install/test_install_no_initial_commit.py # tests/scripts/install/test_install_sh_browser_install.py # tests/scripts/install/test_install_sh_node_prerelease.py # tests/scripts/install/test_install_sh_symlink_stomp.py # tests/scripts/install/test_install_sh_uv_lock_config.py # tests/scripts/install/test_install_unmerged_index.py # tests/scripts/test_contributor_map.py # tests/scripts/test_run_tests_parallel.py # tests/skills/test_competitor_news_monitor_skill.py # tests/skills/test_document_to_action_items_skill.py # tests/skills/test_google_workspace_setup.py # tests/skills/test_google_workspace_setup_deps.py # tests/skills/test_grounded_citations_skill.py # tests/skills/test_ip_as_logo_skill.py # tests/skills/test_live_dashboard_skill.py # tests/skills/test_mcp_oauth_remote_gateway_skill.py # tests/skills/test_office_document_skills.py # tests/skills/test_openclaw_migration.py # tests/skills/test_product_price_monitor_skill.py # tests/skills/test_scrollcraft_skill.py # tests/skills/test_setup_wizard_generator_skill.py # tests/skills/test_weekly_review_planning_skill.py # tests/test_engines_satisfiable.py # tests/test_fast_safe_load.py # tests/test_hermes_bootstrap.py # tests/test_hermes_constants.py # tests/test_hermes_logging.py # tests/test_managed_runtime_resolution.py # tests/test_model_tools_async_bridge.py # tests/test_packaging_build_guard.py # tests/test_packaging_metadata.py # tests/test_yaml_indent_consistency.py # tests/tools/test_approval_timeout_overflow.py # tests/tools/test_base_environment.py # tests/tools/test_bot_mode_dm.py # tests/tools/test_browser_chromium_check.py # tests/tools/test_browser_hardening.py # tests/tools/test_browser_homebrew_paths.py # tests/tools/test_browser_npx_warmup.py # tests/tools/test_browser_orphan_reaper.py # tests/tools/test_browser_real_profile.py # tests/tools/test_browser_use_cli.py # tests/tools/test_clipboard.py # tests/tools/test_code_execution.py # tests/tools/test_code_execution_modes.py # tests/tools/test_code_execution_windows_env.py # tests/tools/test_computer_use.py # tests/tools/test_delegate_liveness_timeout.py # tests/tools/test_execute_code_approval_cluster.py # tests/tools/test_execution_flag_detection.py # tests/tools/test_fal_common.py # tests/tools/test_file_operations.py # tests/tools/test_file_tools.py # tests/tools/test_file_tools_cwd_resolution.py # tests/tools/test_file_tools_live.py # tests/tools/test_lazy_deps.py # tests/tools/test_lazy_deps_durable_target.py # tests/tools/test_lazy_deps_managed.py # tests/tools/test_local_env_blocklist.py # tests/tools/test_local_tempdir.py # tests/tools/test_macos_protected_search.py # tests/tools/test_mcp_npx_cached_bin.py # tests/tools/test_oneshot_completion_linger.py # tests/tools/test_process_registry.py # tests/tools/test_read_file_schema_gating.py # tests/tools/test_skill_improvements.py # tests/tools/test_skills_sync.py # tests/tools/test_termux_api_detection.py # tests/tools/test_tirith_security.py # tests/tools/test_transcription_tools.py # tests/tools/test_tts_streaming.py # tests/tools/test_wake_word.py # tests/tui_gateway/test_compute_host_borrowed_lease.py # tests/tui_gateway/test_compute_host_turn_protocol.py # tests/tui_gateway/test_isolated_orphan_activity.py # tests/tui_gateway/test_protocol.py # tests/tui_gateway/test_slash_worker_profile_home.py # tests/tui_gateway/test_subprocess_encoding.py # tests/tui_gateway/test_tui_gateway_server.py # ui-tui/src/__tests__/terminalParity.test.ts # ui-tui/src/__tests__/termuxComposerLayout.test.ts # ui-tui/src/__tests__/textInputFastEcho.test.ts |
||
|
|
9329575b4e |
fmt(js): npm run fix on merge (#120098)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
38137d2b6e |
test: purge low-value tests, lane js06 (375 removed)
Change-detectors, tautologies, source-reading tests, redundant duplicates, mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane ledger (category + reason for every removal). |
||
|
|
d9ca819cc4 |
Inspired by Amp: pick the workspace a dashboard chat starts in
A fresh dashboard /chat always spawned the TUI in the dashboard process's launch directory, so from a phone or any browser there was no way to aim a new session at a specific repository. Amp's runners now serve many directories and the web composer offers a picker of the runner's projects and discovered git checkouts; this ports that mechanism onto the surface Hermes already has: the dashboard is the phone/web front, the host's projects.db + repo-discovery cache are the served directories. - GET /api/chat/workspaces: the profile's projects (with folders) and discovered repos (session-derived + scanned), default_cwd, home; ?scan=1 rescans desktop.repo_scan_roots on the host, so headless installs (no Desktop to populate the cache) discover repos too. - /api/pty?cwd=<dir>: validated (existing directory, fail-closed 400 via the PTY error path) and forwarded to the TUI child as HERMES_CWD (self-spawned gateway cwd) + HERMES_TUI_CWD (explicit cwd on session.create for the dashboard's in-memory gateway, whose own cwd is the launch dir). Resumed sessions ignore it. - ui-tui: session.create carries cwd when HERMES_TUI_CWD is set, so /new inside a dashboard chat stays in the picked workspace too. - web: workspace selector in the Chat rail above "New chat" (projects, repos by recency, Other path…, rescan), remembered per profile in localStorage; 17 locales. - docs: web-dashboard.md rail + REST sections. Live E2E: real `hermes dashboard` under a scratch HOME with two git repos under desktop.repo_scan_roots -> /api/chat/workspaces?scan=1 lists both; /api/pty?cwd=repo-a -> TUI status bar shows ~/code/repo-a; /api/pty?cwd=<missing> -> "Working directory does not exist" + close. |
||
|
|
c13287c915 |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts: # apps/desktop/electron/main.ts # hermes_cli/backup.py # hermes_cli/config.py # hermes_cli/plugin_catalog.py # hermes_cli/plugins_cmd.py # hermes_cli/plugins_cmd_catalog.py # hermes_cli/plugins_discovery.py # hermes_cli/profiles.py # hermes_cli/update_cmd_deps.py # pyproject.toml # tests/gateway/test_dm_topics.py # tests/hermes_cli/test_config.py # tests/hermes_cli/test_plugins_cmd.py # tests/hermes_cli/test_update_autostash.py # tests/tools/test_lazy_deps.py # tools/lazy_deps.py # tools/skill_ledger.py # utils.py # website/docs/user-guide/security.md |
||
|
|
70f5dc5f46 |
feat(connectors): the backend API for the desktop Connectors page; connect an app without a chat session (#115191)
* feat(connectors): the backend serves a connector's tool list, cached for 24 hours
The Connectors page opens one app and shows every tool it has. The backend
had no way to read that list.
- `tools/connectors/portal/`: a client for the portal's tool-list route and a
JSON cache under the Hermes home, one file per portal origin and connector.
An entry is fresh for 24 hours. After that the read revalidates with the
stored ETag: 304 keeps the list, 404 deletes the entry, an upstream failure
serves the stored list marked stale, and a 401 never serves the cache.
- `connectors.tools {slug, refresh}`: account-level, routed by `profile`, no
chat session. Errors carry a fixed `reason` from one closed set on the rail.
- Every connector model that is not operation state moves into
`tui_gateway/contracts/connectors.py`. Handlers that no chat session owns
live in `tui_gateway/methods_connectors_account.py`.
The wire model is tolerant: an unknown facet reads as unclassified and one odd
tool never blanks a connector.
* feat(connectors): catalog, accounts and member tool rules by RPC
The Connectors page needs the app catalog, the connected account of one app,
a way to disconnect it, and the member's own on/off rules. None had an RPC.
- `connectors.catalog`: name, description, category and logo of each app.
- `connectors.accounts`, `connectors.accounts.remove`: read the accounts at
the tool gateway and remove one by id.
- `connectors.policy.get`: the rule layers that apply to the member, widest
first. The body is a union on `mode`, so a reader can name who turned a
tool off.
- `connectors.policy.set`: one change, a union on `type` (the tools of one
connector, or one connector on or off), with the revision the user saw. A
stale revision answers `POLICY_CONFLICT`. The backend composes the upstream
write in one pure function, so no renderer learns the upstream rules.
- Bundled MCP manifests can name their hosted twin with `connector:`, so the
page can show one card per app.
* feat(connectors): connect an app without a chat session
Every connector RPC took a `session_id`, and a connect that did not come from
the model's tool call minted a link with no watcher. The Connectors page has
no chat session, and its card must flip to connected by itself.
- `connectors.list`, `connectors.connect`, `connectors.operation.status`,
`connectors.operation.wake` and `connection.respond` take `owner`, a union
on `type`: `session` (today's behaviour and authorization) or `account`
(routed by `profile`, authorized by the live transport like `mcp.*`).
`session_id` is gone from these params; every desktop caller sends `owner`.
- An account connect runs the same operation lifecycle on a background
thread, under the profile's scope, so the watcher reads the account and
settles the operation. A second connect for an app that is already
connecting returns the open operation and mints nothing.
- `connection.update` carries `owner`. An account operation has no session to
address, so its updates go out on the session-less broadcast path.
* feat(mcp-catalog): eighteen more bundled entries name their hosted connector
A bundled MCP entry and a hosted connector for the same app are one card
on the Connectors page only when the manifest names its hosted twin.
Linear and Notion had the field. These entries get it too: airtable,
asana, attio, calendly, dropbox, figma, railway, supabase, todoist,
betterstack, canva, cloudflare, datadog, intercom, neon, sentry, stripe
and vercel. Atlassian maps to two hosted connectors and Prisma Postgres
is not clearly the same app, so both stay without one.
* refactor(connectors): the account handlers share one gate, one params model and one write table
The six account-level handlers each repeated the availability gate, the
auth catch and the catch-all reply. One decorator now owns that, and each
handler validates its params with its contract model instead of a ladder
of isinstance checks. The five connection RPCs share one guard for the
unexpected-failure reply.
The four write composers for the member rules were the same function
with a different list key and polarity. They are one table now.
The owner union lives in contracts/common.py, so the params side and the
event side stop declaring it twice and the import cycle is gone.
An account operation start carries one event and a flag, so the wait for
the sign-in link blocks instead of polling every 50 ms. run_operation
loses its two account-only parameters; drive_operation is the second
entry point.
Tests: four deleted (they exercised pydantic or the mock), three merged
into tables, two added (a client that still sends the old top-level
session_id is refused; all six account RPCs run off the server loop).
The shared reply helper and the HTTP and managed-client fakes move to
one place each. Comments are one line or gone.
* fix(connectors): a missing tool-list route reads as "unavailable", not "connector gone"
The tool-list read treated every 404 as the portal's "this connector is
not in the catalog" answer. It deleted the cache entry and answered
CONNECTOR_NOT_FOUND, so a page would offer to remove an app that is
connected and works. A portal that does not serve the route yet answers
a bare 404 for every app.
Only the portal's own {"error": "connector_not_found"} means the
connector is gone. Any other 404 is now a tool-list outage: the cached
list is served as stale, or the RPC answers TOOLS_UNAVAILABLE.
* fix(connectors): a connect from the page returns to the app after sign-in
The sign-in link carries a return target only when the session's surface
is the desktop. A chat session binds that surface. An account-owned call
has no chat session, so nothing bound it: the link was minted without a
return target and the browser ended on the portal's done page instead of
coming back to Hermes.
Every account-owned call now runs with the process's own surface bound,
next to its profile scope. The operation thread copies that context, so
the first link and every reissued link carry the return target and the
operation id.
* test(connectors): defer the new connector RPC coverage
The tests for the new account RPCs, the portal client, the tool-list cache
and the rule composer leave this PR and come back in one later change, after
the API is settled. The same was done for #111008.
Kept: the edits that existing tests need because the five connection RPCs
now take `owner` instead of `session_id`, and the rename of the managed
client seam.
Removed: six new test files, their two fakes and the gateway conftest, and
the new cases in test_mcp_catalog.py, test_connectors_gateway_client.py,
gateway-rpc.test.ts and notifications.test.ts. Reverting this commit restores
all of them.
* fix(cli): the connection panel hands the tool thread back at once
The classic CLI's connection callback waited on a queue for the user's first
decision. The operation's watcher starts only after the callback returns, and
the watcher is what polls a hosted account, runs the 300-second deadline and
sees Ctrl+C.
For a hosted connector the panel opens on the sign-in link, where the only
key that filled the queue was Cancel. The account was never polled: the user
signed in, the panel never changed, and Esc reported the app as skipped.
Ctrl+C set the interrupt flag but left the thread parked on the queue, so the
turn never ended.
The callback now opens the panel and returns, as the gateway's callback does
for the desktop and the Ink TUI. The panel's actions already reach the
operation through apply_answer on the UI thread, so the queue is removed. An
install with a form still waits for Connect, because the backend starts no
work for a pending row. Ctrl+C now settles the operation as `interrupt`, and
open rows become `not_connected`.
Checked on the e2e rig with the fake tool gateway: hosted connect completes on
the third status read; Ctrl+C ends the turn and the polling stops; an MCP
install with a plain and a secret field still saves config and both values.
* fix(connectors): "run it again" lives in the library, so the classic CLI can use it
Making a new sign-in link for a failed or expired hosted connector was
implemented only in the JSON-RPC layer (`_reissue`). The classic CLI does not
go through JSON-RPC: its Connect button on a failed row called apply_answer,
which does nothing for a hosted operation because it has no MCP runner. The
panel showed "Waiting…" until the deadline.
`tools.connectors.run.reissue(operation, names)` now holds the checks and the
per-kind action, and returns a refusal reason or None. The gateway maps each
reason to the same JSON-RPC error as before. The CLI calls it for a hosted
row; a refusal is shown on the row. MCP rows keep their path, because Connect
on a failed MCP row re-sends the form values.
Checked on the e2e rig: a scripted failed sign-in, then Connect: a second mint
with `reinitiate: true`, a new link with a new connection id, then connected.
* feat(connectors): the account list and disconnect go through the portal
`connectors.accounts` and `connectors.accounts.remove` called the tool
gateway. They now call the portal's account-management routes
(`GET /api/v1/connectors/accounts`, `DELETE /api/v1/connectors/accounts/{id}`),
which apply the organisation membership checks and write the disconnect audit
row. There is no fallback to the gateway when the portal is unavailable, and a
removal is never retried.
The read of ONE account stays on the gateway (`GET v1/connectors/accounts/{id}`):
the portal has no such route, and the operation watcher polls it once per second.
`ConnectorClient.list_accounts` and `delete_account` are removed. The removed
account's reply model carries `connector`, which both services send.
* fix(connectors): the account RPCs answer what the portal really sends
Checked against the portal source and against the staging and production
services.
- Errors are read from the upstream error code, not the HTTP status. A rule
write answered 409 for a stale revision and for a user with no organisation;
both read as "the policy changed". `org_required` is now `ORG_REQUIRED` and
403 `no_access` is `ORG_ACCESS_DENIED` on every account RPC; only a rejected
sign-in is `NEEDS_NOUS_AUTH`. `connectors.list` and `connectors.connect` with
the account owner map these too.
- `connectors.policy.get` and `connectors.policy.set` carry `effective`: the
portal's own result for this user, with its stamp and without provider or
subject ids. Nothing is recomputed locally.
- A rule write needs the revision the user saw: `expected_revision` is required
and must be a revision string; a bad one is refused before any HTTP call.
- A tool row carries `no_auth`; a list without the upstream flag is an invalid
answer, not `false`.
- `connectors.accounts.remove` returns the app of the removed account. An
invalid id is `INVALID_PARAMS`.
- The tool-list cache is per signed-in member (a hash of the token's `sub`),
so two Nous accounts on one profile do not share entries.
- A malformed slug is a local error, not a 404 from a server nobody called.
Live, staging: no revision and a malformed revision refused locally; a good
revision wrote one disabled Gmail tool and returned it in `effective`; the
same revision again answered `POLICY_CONFLICT`; the list row showed the tool;
the restore brought the member rules back to the start. Live, staging and
production, read-only: all 60 tool lists (5483 tools) parse.
* fix(connectors): the operation RPCs match their contract; a settled card cannot start a new link
Found by two adversarial reviews of the RPC layer and its types.
- `connectors.connect` from a chat session with no open operation is refused
(`UNKNOWN_OPERATION`). It used to call `manage_connections` through the tool
registry with no card: it made a link nobody watched, returned a reply
without the required `settled` field, and named an operation that was never
registered. There is one way into an operation: the agent's call, or the
account owner's `connectors.connect`. "Run it again" inside an open
operation is unchanged.
- `connection.update` for a session is routed by session key AND profile; two
profiles with the same key no longer cross-deliver a sign-in link. The event
payload gets the same redaction as the RPC replies.
- `connection.respond` runs on the long-handler pool: an approval can start MCP
OAuth discovery, which blocked every RPC of the gateway while it ran.
- `connectors.list` rows are a closed snake_case model: `connector`, `enabled`,
`connected`, `connection_status`, `status_reason`, `gateway_disabled_tools`.
The last one is display data: the gateway enforces the rules, the backend
only passes the list on. The phantom `name` and `description` are gone, and
the desktop uses the generated types instead of hand-written copies.
- `tools_listing` (model-only data) no longer rides on `connectors.operation.status`.
- `unavailable` is removed from the target states and settle reasons: nothing
produces it. The contract generator now fails when a contract enum and its
domain enum differ.
- `ConnectorErrorReason` is part of the generated TypeScript and OpenRPC.
- The desktop sends `connection.respond` on the socket that holds the session,
as wake and reissue already did.
- Contract violations are logged every time, at error level.
- An account connect whose prepare step is slow returns the live operation
instead of an error while the operation keeps running.
- The MCP-manifest `connector` field leaves this PR (it moves to a later one
on top of the catalog-reader change). `hermes_cli/mcp_catalog.py` and
`optional-mcps/` are untouched by this PR again.
anti-slop: no net-new findings (15 touched files).
* fix(connectors): the model gets no sign-in link wherever a card exists; side agents cannot connect
The flag that tells the model "a connection card exists" was the session
platform (`== "desktop"`). The Ink TUI and the classic CLI also draw a card,
so there a connector call on an unconnected app handed the model the raw
`connect_url` and told it to pass the link to the user.
- The agent turn now declares how a link can reach the user
(`tools/connectors/turn.py`): CARD when the agent was built with a
connection callback, SIDE for a subagent or a background turn, LINK for a
headless run (`-q`, cron, ACP, api_server, messaging). It is set once per
tool batch in the agent loop and read by the connector dispatch path, which
never sees the agent. The session platform decides return-to-app only.
- CARD: the result carries `connect_card_available` and our hint, never the
link and never the gateway's own hint.
- SIDE: subagents (`delegate_tool`), gateway background turns and the classic
CLI `/bg` are built with `side_agent=True`. They hold no `manage_connections`
tool on any path that derives the tool list, and a connector call on an
unconnected app gets no link, only "report this to the main agent".
- LINK is unchanged.
- The hosted path with no card builds a detached operation, as the MCP path
does, so no `connection.update` is emitted for an operation no client asked
for. Names and docstrings that said "off desktop" now say "no card".
- A settled card is dead on the desktop: `reissueConnectionTarget` and
`respondToConnectionRequest` share one guard and send nothing for a settled
or unknown operation.
- The model-facing settled result no longer carries `connection_id`; the model
repeated it to the user.
Shown on the real clients with a real model (rig, fake tool gateway): Ink TUI
and classic CLI get `connect_card_available` and no link, the model opens the
card, the account connects, the retried call succeeds; `-q` still gets the
link; a subagent and a background turn have no `manage_connections` and get
the no-link hint; on the desktop a card settled with Continue has no enabled
control and sends no RPC.
* feat(tools): every call made through tool_search + tool_call shows a real label on all three clients
A bridged call showed as a generic `tool_call` row in the Ink TUI and as
`⚡ tool_call` in the classic CLI, because the display looked the name up in
the tool registry and bridged names are made at run time. The desktop labelled
only batches that were all hosted connector calls, by parsing names itself.
- `tools/tool_labels.py` is the one place that turns a bridged call into a
label: kind, app, action, emoji and text. Hosted: `connectors__gmail__GMAIL_SEND_EMAIL`
→ "Gmail · send email". MCP: "Linear · list issues". A local deferred tool
keeps its own emoji, verb and primary-argument preview. A batch gets exactly
one label per entry, always; an entry with no name gets a generic label.
- Classic CLI: one row per inner call; the duration on the last row; the
failure text on the row of the call that failed. With friendly labels off
it prints what it printed before.
- Gateway: tool start, progress and complete events and stored transcript rows
carry a typed `labels` field. It does not depend on the classic CLI's
display setting. Clients no longer parse tool names.
- Ink TUI: rows from the labels; the verbose trail keeps Args and Result.
- Desktop: `ConnectorExecution` renders hosted, MCP and mixed turns from the
labels, one row per call. The labels reach the row under a key no tool
argument can use. The connect card it drew under a failed tool result is
gone: after `CONNECTION_REQUIRED` the one way in is the agent's own
`manage_connections` call.
- `tool_search` and `tool_describe` rows read "Searching tools · <query>" and
"Reading tool details · N tools".
Shown on the real desktop (video and screenshots), the Ink TUI and the classic
CLI with the rig: hosted rows, MCP rows, a two-entry batch, a failed entry, a
`CONNECTION_REQUIRED` row with no card under it, labels after a reload, and the
desktop rows with the classic CLI setting off.
* fix(connectors): the model can tell "hosted tools unavailable" from "no such tool"; manage_connections routes MCP names correctly
- A failed hosted search or describe used to return nothing, by design, so the
model saw only local tools and told the user that a connected app was
missing. The local results are unchanged; when the hosted leg failed, the
`tool_search` and `tool_describe` results carry
`connectors: {status: "unavailable", reason: "unreachable" | "sign_in_expired"}`
and one hint line. A rejected token is `sign_in_expired`; an entitlement
refusal or a shut gate adds nothing. `tool_describe` no longer lists those
names under `not_found` next to "search again".
- NS-932. The description now says which side a name belongs to: a bare name
is a hosted connector account; `mcp: true` only when the user asks for an MCP
server, a local server or an install, or when the name exists only in the
catalog; connect and reconnect are hosted verbs, install, enable and
authorize are MCP verbs. It names the three clients that draw a card.
- A misrouted target is refused with the call that works. Only when the
gateway does not know the connector (confirmed on that failure path) and the
name is a catalog entry does the target fail with "X is a local MCP server.
Call manage_connections with action install ...". It is a per-target
outcome: other targets of the same call keep their links and their card. A
vendor failure on a name both sides know stays an ordinary failed row. The
MCP side mirrors it, and never for an entry that is only not installed.
- "Do not re-ask after a skip or a timeout" no longer stops the model when the
USER asks for that app again; the description and the settled-result notes
say so. A builder saw the model refuse a direct user request.
Shown on the Ink TUI and the classic CLI with a real model: a dead gateway and
a 401; "connect fxmail" goes hosted; "install the fx-noauth MCP server" goes
MCP; "connect fx-noauth" reaches the MCP install card in one corrective round
with no hosted mint; a two-target call where one is misrouted still connects
the other with exactly one mint.
* fix(tui): the connection card answers every key, shows what is happening, and is dead once settled
Reproduced on the real Ink TUI with the rig, then fixed:
- The keyboard was dead during the sign-in wait: the card kept a `submitting`
flag that the normal OAuth path never cleared, and Esc went through the same
guard. The in-flight state now belongs to the answered row and clears when
that row moves, when any later frame of the operation arrives, or after
five seconds. Esc skips the row in every phase; Ctrl+C interrupts the turn
(the input handler had no branch for this overlay); Shift+arrows scroll the
transcript and the card ignores them; arrow keys no longer move the text
cursor and the field focus at once.
- The card was lost at turn idle: the overlay flag was cleared while the
operation stayed in the store, and a resume dropped the pending card. The
flag survives idle, a resume shows the pending card again, a session switch
clears it.
- States with no branch: `not_connected` and a row with no link fell into the
credential form; `expired` vanished with no note. The title and the row text
now name the action (connect, reconnect, install, enable, authorize); a
failed or expired row with no fields offers Try again / Skip; a failed row
WITH fields reopens the form over the typed draft, with the failure above it.
- A settled card is dead: at settle the overlay closes and one transcript line
per app states the outcome. A settled or dismissed operation id is
remembered, so no replay or resume can reopen its card. Esc in the last
"Finishing…" moment hides the card and still writes the outcome lines.
- A failed `connection.respond` and a browser that did not open are shown on
the card in one sentence.
Also: `tui_gateway/connector_payload.py` redacted the BOOLEAN `secret` flag of
a credential field to the string "[REDACTED]". On the desktop every credential
field therefore rendered as a password and lost its prefilled default. A
boolean is no longer redacted.
* chore(connectors): remove the comments and docstrings this branch added
Deletions only. Kept: tool directives (`# noqa`, `// eslint-disable`, ...),
`// SAFETY:` lines, and the docstrings of the contract models under
`tui_gateway/contracts/`, which become the descriptions in the generated
OpenRPC and TypeScript.
Checked that no code changed: every Python file has the same AST as before
once docstrings and `pass` are ignored (62 files), and every TypeScript file
prints the same with comments stripped by the TypeScript printer (32 files).
The generated contract files are unchanged.
* fix(connectors): a card restored after a reload answers again; every account RPC names auth and org failures
Found by the end-to-end runs on the pushed head.
- Desktop: after a window reload, Continue on the restored card sent nothing.
The answer looked up the backend that holds the session with the runtime
session id, the lookup wants the stored id, and a failed lookup returned
silently. When the lookup gives no owner the answer now goes out on the
window's active socket, which is what main does.
- `connectors.policy.get` answered `POLICY_UNAVAILABLE` for a rejected sign-in,
a refused scope, a non-member and a missing organisation alike: the handler
runs with the gateway's globals and did not import the reason enum, so its
own error mapping raised. `connectors.accounts.remove` caught auth failures
in its generic branch. `org_required` was mapped on `policy.set` only. All
six account RPCs now answer `NEEDS_NOUS_AUTH`, `FORBIDDEN_SCOPE`,
`ORG_ACCESS_DENIED` and `ORG_REQUIRED` for those four upstream answers.
|
||
|
|
efaa819ee9 | merge: refresh desktop fixes without reviving retired update checks | ||
|
|
c7c2df1a53 |
fmt(js): npm run fix on merge (#118435)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
e3c0be3e12 | merge origin/main (npm run fix formatting sweep) into ethie/pm-clean | ||
|
|
bc655bfb40 |
fmt(js): npm run fix on merge (#118250)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|
|
9f2ba1b74d |
merge origin/main (779 commits) into ethie/pm-clean
Branch semantics kept where main and PM disagree: update_cmd_deps.py, constraints-termux.txt, the Electron update-api-check module and the post-swap hand-off test stay deleted; the pending-fleet-restart catch-up and the local_runtime tag/download ladder stay retired (PM owns engines). Ported from main onto the branch's shape: profile_scoped_chore for the auto-archive and plugin-update housekeeping chores, the local-runtime cross-process boot lock and residency cap, the checkpoint tmp_pack sweep, the cua daemon-liveness status probe, the remote-served Desktop update flag (posix.sh / windows.ps1), sign-in for env-pinned remote gateways (urlDisabled on RemoteSetupFields), the uvloop extra split (uvicorn without [standard]), and the umask-scoping spawn test. uv.lock regenerated with pm.build_env --lock-only; new utf-8 reads from main switched to utf-8-sig (check-windows-footguns). |
||
|
|
afc3b7c6f3 |
feat(connectors): one backend-owned connection operation, with a setup card on Desktop, TUI and CLI (#111008)
* feat(connectors): the desktop connects apps through one backend-owned operation Re-based onto main after #109517, #110368, #110574 and #110843 landed as squash merges ( |
||
|
|
1d30aa5571 |
feat(tui): Processes block in the live-work dock and /agents overlay
The Ink dock and /agents overlay only merged subagent.* events with subagent.list. They now poll process.list for the owning session on the same cadence and paint a Processes block under the agents (⚙ command · elapsed · last output; exit verdict for 60 s after exit). The dock budget is split so neither block hides the other; processes alone still surface the dock. Glyphs live in lib/processGlyph.ts so panel and overlay match. |
||
|
|
2c78b9b39e |
feat(process_registry): stamp exited_at and expose it on process.list
The live-work docks retire a finished background process ~60 s after it ends; the registry only knew started_at, so the age of an exit was not observable. _move_to_finished is the single choke point every exit path (reader loop, reconcile, kill) passes through, so the stamp lives there. completion_reason rides along so a killed process can read "killed" instead of "exit -15". |
||
|
|
1cf8a9fb41 |
fix(tui): count streamed frames as heartbeat liveness
The TUI's JsonRpcRequestChannel took the 'response' liveness default, so only a ping-ack or a response to a pending call refreshed the deadline: a socket streaming a delta every second through a 134s turn was declared dead at the 45s deadline and force-closed, splitting sessions that went on to complete server-side (#115251). Pass heartbeatLiveness: 'any-inbound' on the TUI channel — the same contract the desktop/web client already uses — so any inbound frame counts as life. A silent drop still trips the deadline, so true dead-transport detection is unchanged; only the false-kill class goes. The channel's 'response' mode keeps its semantics for callers that explicitly choose it; the shared wiring test pins the TUI construction site so the option cannot silently regress. Fixes #115251 |
||
|
|
d6a69ea5a3 |
fix(tui): treat every ctrl chord as a binding name, never typed text
Widen #115382 from bare C0 bytes to every ctrl-modified keypress: a kitty CSI u / xterm modifyOtherKeys Ctrl+L (`ESC [ 108 ; 5 u`) reaches parseKey with ctrl set and name `l` exactly like the 0x0c redraw byte, so the composer typed an `l` for it too. `isControlChord` is ctrl && !isPasted; bracketed pastes stay text. Test trimmed to two invariants (chord is bindable and refused by the insert gate; typed text and pastes still land). |
||
|
|
addf694901 |
fix(tui): a ctrl chord's control byte is not typed text (stray l after resume)
The dashboard asks a reused PTY's TUI for a full redraw on every re-attach by writing the force-redraw byte Ctrl+L (0x0c, `hermes_cli/pty_session.py` TUI_FORCE_REDRAW) into its stdin (`web_routers/chat_ws.py`: `session.attach(ws, force_redraw=not _created)`). parse-keypress names a C0 control byte after the letter it encodes (0x0c -> 'l') and `parseKey` hands that name to `InputEvent.input` so bindings can match ctrl+<letter> on the raw byte; the composer's insert gate read the name as typed text, so the redraw byte landed in the input box as a solitary `l` that prefixed the next message (#115284 — also on tab switch and window restore, both of which re-attach). Flag the event instead of touching `input` (clearing it would break the composer's own ctrl+a/e/u/k/w/z/y branches and the global ctrl+c/x/o/t pass-through): `InputEvent.isControlByteChord` is true only for a single-byte C0 sequence with ctrl set, so kitty CSI u / xterm modifyOtherKeys chords and bracketed pastes are unaffected. The composer skips both insert sites for it, and still flushes a pending key-burst because a chord is not typing. |
||
|
|
b787fb9128 |
fix(tui): Ctrl+D exits from an empty composer on macOS too
The exit binding matched isAction(key, ch, "d"), which on macOS means Cmd+D: Ghostty consumes Cmd+D for split panes and literal Ctrl+D never matched, so the TUI stayed open. Ctrl+D is the terminal EOF convention, not a Cmd shortcut, so it now also routes through the existing isMacActionFallback seam (target union gains "d"), and on every platform it exits only when the composer holds no text, buffered lines or attachments, matching the classic CLI. Slim redo of #116454. Co-authored-by: Mohamad Kanso <91088196+MohamadKanso@users.noreply.github.com> |
||
|
|
6abbc02228 | fix(tui): no gateway respawn after graceful-exit kill (#114987) | ||
|
|
ce3d042f0c | Merge remote-tracking branch 'origin/main' into ethie/pm-clean | ||
|
|
e1576d06a6 |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
Resolved toward the branch: PM provisions uv/python (main's install.ps1 uv-shim salvage + its test and workflow steps dropped), the shim re-exec stays retired, package.json carries no electron-builder block (afterExtract identity stamp wired into electron-builder.config.cjs instead; after-pack.mjs keeps signing only), Desktop workspace-deps helpers stay retired. Main's scratch-dir bootstrap (export_scratch_tmp_env) is taken and re-run after profile resolution. |
||
|
|
171a1777b5 |
fix(desktop,tui): reasoning pill and effort rows say ultra sends max on this route
The Desktop pill, the catalog row meta and the effort radio row rendered a clamped pick as a plain "Ultra", and the TUI status bar as "ultra" — presenting a Hermes-internal step as a wire level the route does not have, while the CLI's `/reasoning` shows "ultra (sends max on this route)" (#115876). Both surfaces now read `session.info.reasoning_effort_wire`: - Desktop pill / catalog row meta: compact "Ultra→Max", tooltip and aria-label "Effort: Ultra (sends Max on this route)" (new `modelOptions.sendsOnRoute` string in every locale, mirroring the CLI wording). - Effort radio row for the selected clamped level: "Ultra (sends Max on this route)". - TUI status bar: "ultra→max". - Unknown wire ('' — not stamped yet, or an optimistic pick) or a verbatim level makes no claim, so nothing changes on routes that send the level as is. `setCurrentReasoningEffort` and the tile optimistic write clear the wire so a stale clamp never pairs with a new pick until the gateway re-stamps. Docs: one sentence in the Desktop guide. Part of #61634. |
||
|
|
6f6ed01355 |
fix: WAF 403s stop reading as key rejections; anthropic_messages routes send custom_providers extra_headers
Two gaps for custom providers behind a WAF/CDN:
- `build_anthropic_client` never consulted `custom_providers[].extra_headers`,
so a relay in `anthropic_messages` mode that rejects the SDK User-Agent kept
403ing even with `extra_headers: {User-Agent: ...}` configured, while the
OpenAI-wire clients already applied it. The lookup now lives in
`_new_sdk_client`, the one constructor every builder path goes through
(init, /model switch, rebuild, auxiliary), keyed by the caller's raw route
because entries are keyed by the `/v1` form the normalizer strips.
Salvaged direction of #46002 (@wait4xx). Fixes #24293, #9721.
- `_status_403` classified every non-billing 403 as `auth`, so a WAF's plain
"Your request was blocked." or a Cloudflare browser challenge printed "Your
API key was rejected" and could rotate a healthy credential. A 403 carrying
established block/challenge markers is now `upstream_blocked`: no rotation,
no retry, fallback allowed, WAF/User-Agent guidance on every surface (CLI
loop, chat copy, cli chat error copy, TUI gateway + Ink TUI copy). Generic
403 and all 401 keep the auth verdict. Salvaged direction of #70567
(@ooiuuii) and #53114 (@AgenticSpark). Fixes #53099, #70566.
|
||
|
|
82a5affdd3 |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts: # hermes_cli/backup.py # tests/hermes_cli/test_gateway_restart_loop.py # website/docs/developer-guide/web-search-provider-plugin.md # website/docs/getting-started/installation.md # website/docs/getting-started/updating.md # website/docs/index.mdx # website/docs/reference/cli-commands.md # website/docs/user-guide/docker.md # website/docs/user-guide/windows-wsl-quickstart.md # website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/plugins/index.md # website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/web-search-provider-plugin.md # website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/index.mdx # website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/cli-commands.md # website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/environment-variables.md # website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/docker.md # website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/features/plugins.md # website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/security.md # website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/windows-wsl-quickstart.md |
||
|
|
a6ae6ace51 |
Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts: # .github/workflows/js-tests.yml # agent/model_metadata.py # apps/desktop/electron/main.ts # apps/desktop/scripts/bundle-electron-main.mjs # apps/desktop/src/app/settings/about-settings.tsx # apps/desktop/src/app/settings/gateway-settings.test.tsx # apps/desktop/src/app/settings/gateway-settings.tsx # apps/desktop/src/app/updates-overlay.tsx # gateway/shutdown_flush.py # hermes_bootstrap.py # hermes_cli/local_runtime/binaries.py # hermes_cli/main.py # hermes_cli/managed_uv.py # hermes_cli/update_cmd.py # hermes_cli/update_cmd_deps.py # hermes_cli/update_cmd_fleet.py # hermes_cli/update_cmd_maint.py # hermes_cli/update_receipt.py # hermes_cli/update_serve_obligations.py # hermes_constants.py # tests/hermes_cli/test_doctor.py # tests/hermes_cli/test_managed_uv.py # tests/hermes_cli/test_pending_supervisor_recovery.py # tests/hermes_cli/test_startup_fast_guards.py # tests/hermes_cli/test_update_desktop_stale_warning.py # tests/hermes_cli/test_update_fleet_restart_pending.py # tests/hermes_state/test_hermes_state.py # tests/tools/test_tirith_security.py # tools/bot_relay.py # tools/checkpoint_manager.py # tools/write_approval.py # website/docs/getting-started/updating.md # website/docs/reference/environment-variables.md |
||
|
|
5ae7ec8623 |
fix(setup): a provider configured after boot unblocks the dashboard chat without a restart
The serve process's free-tier boot record (`free_tier_bootstrap._record`) is
built once; when the boot inventory found nothing (mint failed or the tier is
off) it says `provider_configured: false` for the process lifetime and
`setup.status` answers from it, so the Ink chat (dashboard /chat, `hermes
--tui`) parks every new session on "Setup Required" no matter what the user
configures afterwards — the Models page, a picker key save, or `hermes setup`
from a shell all land on disk and change nothing in the running process.
Reconcile the record on read instead of re-minting on write: `reconcile_record`
re-runs the cheap inventory (`_inventory_other_providers`, the resolver ladder
with the free-tier rung hidden) when a `False` record's config files
(`config.yaml` / `.env` / `auth.json`) moved since the inventory that built it,
replaces the record's inventory half and broadcasts `setup.ready`. The mint
verdict is kept as is: only the boot bootstrap and its retries mint.
`wait_for_record` (what `setup.status` reads) reconciles, which also covers
writes this process never saw (`/setup`'s `hermes setup` handoff, `hermes model`
in docker exec, a hand edit); the two in-process write paths from #114708
(`/api/model/set`, `model.save_key`) call it for the immediate broadcast.
Dropped from #114708: `retry_bootstrap_mint(force=True)` on the write paths — a
forced portal mint (network, cooldown bypassed) inside an HTTP write; the record
only needed its inventory refreshed. Not taken from #108773: OR-ing the record
with `_has_any_provider_configured()` — that first-run guard counts host-wide
credentials (other host-wide agent-CLI logins) and answers True on a blank machine (verified
live on this host), which would open the gate with nothing configured.
The 4001 hint for a sessionless `config.set model` named "Settings -> Models",
a Desktop-only surface; the Ink chat has no Settings and the dashboard has a
Models page. One string now names /setup, the Models page and Settings ->
Models. The TUI's "Setup Required" panel no longer advertises `/model` as the
in-place fix (sessionless picks are refused by design,
|
||
|
|
9b0ca895b3 |
fix(tui,desktop): send session_id on commands.catalog, complete.slash and skills.reload
The clients called all three RPCs with `{}`, so the server's session-less
fallback (`_completion_cwd({})`) read the process TERMINAL_CWD, which
`gateway/run.py` rewrites to $HOME at import in the default-config case: the
catalog, popup and reload still missed the session's project skills.
- ui-tui: `useCompletion` adds `session_id` to `complete.slash`;
`/reload-skills` sends it on `skills.reload` and the follow-up
`commands.catalog`; the gateway-ready catalog fetch sends it when a session
already exists (reconnect). Before the first session the gateway binds the
same workspace it seeds a new session with.
- Desktop: `useSlashCompletions` takes `sessionId`, sends it on both RPCs and
keys the completion cache per session so two chats in two repos don't
serve each other's catalog.
|