Skip to content

Add interactive askUserQuestion and configurable swarm spawn mode#209

Open
Zephyr709 wants to merge 34 commits into
1jehuang:masterfrom
Zephyr709:feature/ask-user-question-tool
Open

Add interactive askUserQuestion and configurable swarm spawn mode#209
Zephyr709 wants to merge 34 commits into
1jehuang:masterfrom
Zephyr709:feature/ask-user-question-tool

Conversation

@Zephyr709
Copy link
Copy Markdown

Summary

This branch collects the current agent/tooling UX work:

  • adds askUserQuestion tool support and an interactive TUI modal
  • adds ToolSearch deferred-tool registry plumbing
  • improves provider/model allowlist handling and local startup recovery
  • adds configurable swarm spawn behavior via spawn_mode = visible | headless | auto
  • documents background/non-focus-stealing agent orchestration guidance
  • activates the macOS desktop window on launch
  • includes rustfmt cleanup for touched provider/TUI files

Notable new swarm behavior

swarm spawn and spawn-backed assignment actions can now request:

  • visible: launch a terminal window
  • headless: create an in-process/headless worker, avoiding Ghostty focus stealing
  • auto: preserve historical visible-then-headless fallback behavior

Validation

  • cargo fmt --check
  • git diff --check
  • cargo check -p jcode-protocol
  • cargo check -p jcode
  • cargo check -p jcode-desktop
  • cargo test -p jcode-protocol test_comm_spawn_roundtrip_with_optional_nonce -- --nocapture
  • cargo test -p jcode-protocol test_comm_assign_next_roundtrip -- --nocapture
  • cargo test -p jcode --lib assign_next_prefers_worker_with_dependency_context -- --nocapture
  • cargo test -p jcode --lib assign_next_prefers_worker_with_matching_subsystem_metadata -- --nocapture

Follow-ups

  • Add a config-level default for swarm spawn mode so users can prefer headless globally.
  • Add a Jcode ingestion bridge for the local agent telemetry warehouse.

Zephyr709 and others added 16 commits May 13, 2026 00:02
Adds a real ToolSearch implementation that lets Anthropic OAuth sessions
discover and unlock registry tools at runtime, including the previously
unreachable askUserQuestion.

- New tool: src/tool/tool_search.rs (15-entry curated deferred registry,
  scored matching, per-session unlock state)
- Registry::definitions_for_names: resolve specific tools by registry key
- Agent::tool_definitions appends unlocked tools (works around the
  locked_tools cache so unlocks mid-session reach the API)
- AnthropicProvider::format_tools OAuth branch now passes through any
  unlocked tools alongside the 10 hardcoded Claude Code tools, with
  ephemeral cache_control moved to the actual last tool

Tests: 5 tool_search unit tests + existing ask_user_question tests pass.
Replace the side-panel markdown rendering with a real blocking overlay:
- Arrow keys / j-k to navigate options, Enter to pick
- Recommended option pre-selected for fast Enter-to-confirm
- Synthetic "Other (type custom answer)" row switches into free-form
  text input mode where the user can type any reply
- Space toggles options in multi-select mode
- Letter shortcuts (A, B, ...) jump to the matching option id
- Esc cancels and the tool returns a Canceled outcome
- 1h timeout fallback if no answer arrives

Architecture:
- New crate::ask_user module exposes a global pending-request registry
  keyed by request_id with a oneshot sender/receiver pair, mirroring
  the existing StdinInputRequest pattern.
- BusEvent::AskUserQuestionOpened lets the tool publish a question for
  the TUI to render without holding direct App state.
- src/tui/ask_user_modal.rs is the ratatui overlay widget; centered,
  uses Clear for full opacity, drawn last in draw() so it sits on top
  of chat + side panel.
- src/tui/app/ask_user_modal_app.rs is the App glue (open / dispatch
  keys / submit answer).
- handle_modal_key short-circuits to the modal when visible so the
  user gets full input focus.

Side-panel close: Alt+M already toggles visibility (documented in help).

Mermaid render: gated behind JCODE_ENABLE_MERMAID=1 and the
mermaid-renderer feature in jcode-tui-markdown which is not enabled
by default. Not addressed in this commit; tracked separately.

17 tests pass: 9 modal unit tests + 4 tool integration tests with
unique session ids + 3 ask_user registry tests + 1 tool_search test.
The askUserQuestion tool runs inside the shared server process while the
TUI is a remote-client. Bus events do not cross the IPC boundary, so the
previous commit's modal would never open for remote-mode clients.

This commit adds the missing wire round-trip:

Server -> Client (push the question):
- New jcode_protocol::AskUserQuestion/Option/Answer payload types
  shared by both ends as the wire schema and by crate::ask_user as the
  in-process bus payload (one source of truth via type aliases).
- New ServerEvent::AskUserQuestionOpened variant.
- client_lifecycle forwards BusEvent::AskUserQuestionOpened to the
  active client when session_id matches, mirroring SidePanelUpdated.
- remote/server_events.rs dispatches the event by opening the modal
  in-place via App::open_ask_user_modal.

Client -> Server (return the answer):
- New Request::SubmitAskUserAnswer variant (tagged lightweight so it
  cuts through processing-state queues).
- RemoteConnection::submit_ask_user_answer uses the detached send path
  so the synchronous key handler does not have to await.
- client_lifecycle dispatches SubmitAskUserAnswer to
  crate::ask_user::submit_answer to wake the pending oneshot.

Modal-side glue:
- AskUserModalApp now enqueues each completed answer into a new
  App::pending_ask_user_answers vec in addition to calling local
  submit_answer (so in-process / test paths still work uniformly).
- remote::handle_tick drains the queue every tick and dispatches each
  answer via remote.submit_ask_user_answer.

Result: the modal opens in the remote TUI and the picked answer
reaches the agent's tool task across the socket.

Verified end-to-end against the live shared-server build (4s reload).
Introduces [provider.model_allowlist] in config.toml so users can restrict
the model picker and `/model` command to a chosen subset for each
built-in provider. Previously the allowlist field on
[providers.<profile>.models] only applied to custom openai-compatible
profiles, so OAuth providers (claude/openai/gemini/antigravity) always
exposed the full vendor catalog.

The new field is a BTreeMap<String, Vec<String>> keyed by the provider
canonical name (anthropic/openai/gemini/antigravity, with "claude" as
an alias for anthropic). Substring + case-insensitive matching keeps
patterns short.

Filtering is applied at every visible surface that lists models:
- Agent::available_models_display() and Agent::model_routes()
- CLI `jcode model list`
- TUI slash-command suggestion list
- TUI inline model picker route fetch

Round-trip is covered by new tests in jcode-config-types.
Match Claude Code's pattern: the modal takes over the chat input slot
instead of floating as a centered popup. Cleaner focus and clearly
communicates that the agent is blocked on the user.

- new AskUserModal::render_inline(frame, area) draws into a host-supplied Rect
- AskUserModal::desired_height() reports needed rows
- ui.rs reserves the input chunk for the modal when visible and
  skips drawing the input box / inline UI / idle animation
The openrouter provider impl backs every named openai-compatible profile
(opencode-go, ollama-cloud, deepseek, ...) and always reports
provider.name() == "openrouter". Extend
provider_model_allowlist_patterns() so it also accepts the active
profile id, which is the natural key users expect in
`[provider.model_allowlist]` (e.g.
`opencode-go = [\"deepseek-v4-flash\"]`).

Adds active_openai_compatible_profile_id() helper alongside the existing
active_openai_compatible_display_name(). The lookup tries the canonical
provider name, the `claude`/`anthropic` alias pair, and finally the
active openai-compatible profile id.
Previously filter_model_routes_by_allowlist applied a single allowlist
key to every route in the list. When the agent surfaces the aggregated
MultiProvider routes, that meant only the current provider's allowlist
was respected and other providers' routes were never filtered.

Resolve the allowlist key from each route's (provider, api_method) pair
so cross-provider model pickers honour every configured allowlist:

- Anthropic / OpenAI / Gemini / Antigravity / Copilot / Cursor map to
  their respective allowlist keys.
- openai-compatible:<profile-id> routes (opencode-go, ollama-cloud,
  deepseek, ...) map to the embedded profile id.

Providers without a matching allowlist remain unrestricted.
Zephyr709 added 13 commits May 13, 2026 21:46
- docker-compose.agent-db.yml: postgres:16-alpine on port 5432, well-known localhost creds
- db-execute tool: agents execute scoped SQL via docker exec psql into their session schema
- Schema-per-session isolation (agent_<session_suffix> convention)
- Session-id sanitization: lowercase alphanumeric with leading-letter guarantee
- Compiles clean, passes end-to-end smoke test with CRUD + cross-schema namespace isolation

Part of goal: autonomous-local-neon-postgres-analytics-substrate-for-agents
Zephyr709 added 5 commits May 14, 2026 23:18
When JCODE_PROVIDER_PROFILE_ACTIVE is not set (no CLI override),
clear any lingering JCODE_NAMED_PROVIDER_PROFILE, JCODE_ACTIVE_PROVIDER,
JCODE_RUNTIME_PROVIDER, and JCODE_OPENROUTER_CACHE_NAMESPACE env vars
before the default_provider guard runs. Without this, stale env from a
previous jcode session's shell environment blocks config.toml's
default_provider from taking effect, causing the model picker and
session provider key to resolve to the wrong provider.

Regression tests cover both the stale-clear case and the explicit
CLI override preservation case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant