gemcode 0.4.19__tar.gz → 0.4.20__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gemcode-0.4.19/src/gemcode.egg-info → gemcode-0.4.20}/PKG-INFO +1 -1
- {gemcode-0.4.19 → gemcode-0.4.20}/pyproject.toml +1 -1
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/repl_slash.py +21 -4
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/hitl_bridge.py +14 -2
- gemcode-0.4.20/src/gemcode/web/serve_bind.py +106 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/serve_state.py +36 -2
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/server.py +75 -7
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/sessions_api.py +12 -5
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/sse_adapter.py +40 -7
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/terminal_api.py +21 -10
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/workspace_panel_api.py +6 -0
- {gemcode-0.4.19 → gemcode-0.4.20/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode.egg-info/SOURCES.txt +1 -0
- gemcode-0.4.20/tests/test_web_server.py +69 -0
- gemcode-0.4.19/tests/test_web_server.py +0 -30
- {gemcode-0.4.19 → gemcode-0.4.20}/LICENSE +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/MANIFEST.in +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/README.md +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/setup.cfg +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/__init__.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/__main__.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/a2a_bridge.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/agent.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/agent_habits.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/agent_intelligence.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/agent_mesh.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/agent_triggers.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/audit.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/automations.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/autotune.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/cli.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/codebase_awareness.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/compaction.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/config.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/credentials.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/delegation_learning.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/event_bus.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/fleet_reports.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/hooks.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/interactions.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/invoke.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/kaira_client.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/kaira_ipc.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/learning.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/limits.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/org.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/paths.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/permissions.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/pricing.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/config.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/refine.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/rules.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/self_healing.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/session_store.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/skills.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/thinking.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tool_synthesis.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/automations_tools.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/org_tools.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/trust.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/version.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/vertex.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/wal.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/chat_skills.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/customize_api.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/fleet_api.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/preview_api.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/project_root.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/runtime_api.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/web_config_api.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_add_dir.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_agent_habits.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_agent_mesh.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_autocompact.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_automations.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_capability_routing.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_cli_init.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_context_budget.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_context_warning.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_credentials.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_event_bus.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_fleet_reports.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_kaira_ipc_paths.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_modality_tools.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_model_errors.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_model_routing.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_paths.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_permissions.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_repl_commands.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_repl_slash.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_session_runtime_cache.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_skills.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_slash_commands.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_thinking_config.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_token_budget.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_tools.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_workspace_hints.py +0 -0
|
@@ -2584,7 +2584,13 @@ async def process_repl_slash(
|
|
|
2584
2584
|
|
|
2585
2585
|
if first in ("status", "ps"):
|
|
2586
2586
|
running, info = is_serve_running(cfg.project_root, host=host, port=port)
|
|
2587
|
-
if
|
|
2587
|
+
if info and info.get("project_mismatch"):
|
|
2588
|
+
out("[serve] port in use by another GemCode API (different project)")
|
|
2589
|
+
out(f" url: {info.get('url') or serve_base_url(host, port)}")
|
|
2590
|
+
out(f" this project: {info.get('expected_root') or cfg.project_root}")
|
|
2591
|
+
out(f" server root: {info.get('actual_root') or '?'}")
|
|
2592
|
+
out(" Stop the other server or run `gemcode serve -C ...` for the matching folder.")
|
|
2593
|
+
elif running and info:
|
|
2588
2594
|
out("[serve] running")
|
|
2589
2595
|
out(f" url: {info.get('url') or serve_base_url(host, port)}")
|
|
2590
2596
|
out(f" project: {info.get('project_root') or cfg.project_root}")
|
|
@@ -2617,19 +2623,30 @@ async def process_repl_slash(
|
|
|
2617
2623
|
session_id=session_id,
|
|
2618
2624
|
)
|
|
2619
2625
|
if not result.get("ok"):
|
|
2620
|
-
|
|
2626
|
+
if result.get("project_mismatch"):
|
|
2627
|
+
out("[serve] port 3001 is used by GemCode for a different project:")
|
|
2628
|
+
out(f" expected: {result.get('expected_root')}")
|
|
2629
|
+
out(f" actual: {result.get('actual_root')}")
|
|
2630
|
+
out(" Stop that server first, or start this project's API on another port.")
|
|
2631
|
+
else:
|
|
2632
|
+
out(f"[serve] failed: {result.get('error') or 'unknown error'}")
|
|
2621
2633
|
if result.get("log"):
|
|
2622
2634
|
out(f" log: {result.get('log')}")
|
|
2623
2635
|
elif result.get("already_running"):
|
|
2624
2636
|
out(f"[serve] already running at {result.get('url')}")
|
|
2625
2637
|
else:
|
|
2626
|
-
|
|
2638
|
+
if result.get("port_fallback"):
|
|
2639
|
+
pref = result.get("preferred_port") or DEFAULT_SERVE_PORT
|
|
2640
|
+
out(f"[serve] port {pref} was busy — started at {result.get('url')}")
|
|
2641
|
+
out(f" Set NEXT_PUBLIC_API_URL={result.get('url')} in gemcode-web-ui/.env.local")
|
|
2642
|
+
else:
|
|
2643
|
+
out(f"[serve] started at {result.get('url')}")
|
|
2627
2644
|
if result.get("warming"):
|
|
2628
2645
|
out("[serve] warming up — retry /serve status in a moment")
|
|
2629
2646
|
if result.get("log"):
|
|
2630
2647
|
out(f" log: {result.get('log')}")
|
|
2631
2648
|
out(f"[serve] session_id: {session_id}")
|
|
2632
|
-
out("
|
|
2649
|
+
out("Web UI: npm run dev in gemcode-web-ui → http://localhost:3002")
|
|
2633
2650
|
out()
|
|
2634
2651
|
return ReplSlashResult(skip_model_turn=True)
|
|
2635
2652
|
|
|
@@ -4,16 +4,26 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
7
8
|
import time
|
|
8
9
|
import uuid
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any, Callable
|
|
11
12
|
|
|
12
13
|
_APPROVAL_DIR = Path.home() / ".gemcode" / "web_approvals"
|
|
13
|
-
_DEFAULT_TIMEOUT_S =
|
|
14
|
+
_DEFAULT_TIMEOUT_S = float(os.environ.get("GEMCODE_WEB_HITL_TIMEOUT_S", "3600"))
|
|
14
15
|
_POLL_INTERVAL_S = 0.25
|
|
15
16
|
# approval_ids currently blocked in wait_for_web_approval (same process)
|
|
16
17
|
_active_waiters: set[str] = set()
|
|
18
|
+
# emitted to the UI but wait_for_web_approval not started yet (preflight)
|
|
19
|
+
_pending_approval_ids: set[str] = set()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register_pending_approval(approval_id: str) -> None:
|
|
23
|
+
"""Mark an approval as expected so early UI clicks are not reported as late."""
|
|
24
|
+
aid = (approval_id or "").strip()
|
|
25
|
+
if aid:
|
|
26
|
+
_pending_approval_ids.add(aid)
|
|
17
27
|
|
|
18
28
|
|
|
19
29
|
def _approval_path(approval_id: str) -> Path:
|
|
@@ -48,7 +58,7 @@ def resolve_web_approval(approval_id: str, *, confirmed: bool) -> dict[str, Any]
|
|
|
48
58
|
path = _approval_path(approval_id)
|
|
49
59
|
payload = {"confirmed": bool(confirmed), "resolved_ms": int(time.time() * 1000)}
|
|
50
60
|
path.write_text(json.dumps(payload), encoding="utf-8")
|
|
51
|
-
late = approval_id not in _active_waiters
|
|
61
|
+
late = approval_id not in _active_waiters and approval_id not in _pending_approval_ids
|
|
52
62
|
return {
|
|
53
63
|
"ok": True,
|
|
54
64
|
"approval_id": approval_id,
|
|
@@ -73,6 +83,7 @@ async def wait_for_web_approval(
|
|
|
73
83
|
if existing is not None:
|
|
74
84
|
return existing
|
|
75
85
|
|
|
86
|
+
_pending_approval_ids.discard(approval_id)
|
|
76
87
|
_active_waiters.add(approval_id)
|
|
77
88
|
try:
|
|
78
89
|
deadline = time.monotonic() + max(1.0, timeout_s)
|
|
@@ -93,6 +104,7 @@ async def wait_for_web_approval(
|
|
|
93
104
|
return False
|
|
94
105
|
finally:
|
|
95
106
|
_active_waiters.discard(approval_id)
|
|
107
|
+
_pending_approval_ids.discard(approval_id)
|
|
96
108
|
|
|
97
109
|
|
|
98
110
|
def new_approval_id(session_id: str, fc_id: str | None) -> str:
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Bind helpers for ``gemcode serve`` (port scan + user-facing connection hints)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import errno
|
|
6
|
+
import os
|
|
7
|
+
import socket
|
|
8
|
+
from http.server import ThreadingHTTPServer
|
|
9
|
+
from typing import Callable, TypeVar
|
|
10
|
+
|
|
11
|
+
from gemcode.web.serve_state import DEFAULT_SERVE_PORT, serve_base_url
|
|
12
|
+
|
|
13
|
+
# Next.js dev server default — skip when auto-picking an API port.
|
|
14
|
+
UI_RESERVED_PORTS: frozenset[int] = frozenset({3002})
|
|
15
|
+
|
|
16
|
+
HandlerFactory = TypeVar("HandlerFactory", bound=Callable[..., type])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def iter_candidate_ports(
|
|
20
|
+
preferred: int,
|
|
21
|
+
*,
|
|
22
|
+
max_attempts: int | None = None,
|
|
23
|
+
skip: frozenset[int] = UI_RESERVED_PORTS,
|
|
24
|
+
) -> list[int]:
|
|
25
|
+
"""Ports to try, starting at *preferred*, skipping reserved UI ports."""
|
|
26
|
+
attempts = max_attempts
|
|
27
|
+
if attempts is None:
|
|
28
|
+
attempts = int(os.environ.get("GEMCODE_WEB_PORT_SCAN", "30"))
|
|
29
|
+
ports: list[int] = []
|
|
30
|
+
port = preferred
|
|
31
|
+
while len(ports) < attempts:
|
|
32
|
+
while port in skip:
|
|
33
|
+
port += 1
|
|
34
|
+
ports.append(port)
|
|
35
|
+
port += 1
|
|
36
|
+
return ports
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def bind_http_server(
|
|
40
|
+
host: str,
|
|
41
|
+
handler_factory: HandlerFactory,
|
|
42
|
+
preferred_port: int,
|
|
43
|
+
*,
|
|
44
|
+
max_attempts: int | None = None,
|
|
45
|
+
) -> tuple[ThreadingHTTPServer, int]:
|
|
46
|
+
"""Bind ``ThreadingHTTPServer``, advancing to the next port if busy."""
|
|
47
|
+
last_err: OSError | None = None
|
|
48
|
+
for port in iter_candidate_ports(preferred_port, max_attempts=max_attempts):
|
|
49
|
+
try:
|
|
50
|
+
httpd = ThreadingHTTPServer((host, port), handler_factory)
|
|
51
|
+
return httpd, port
|
|
52
|
+
except OSError as exc:
|
|
53
|
+
if exc.errno != errno.EADDRINUSE:
|
|
54
|
+
raise
|
|
55
|
+
last_err = exc
|
|
56
|
+
raise OSError(
|
|
57
|
+
errno.EADDRINUSE,
|
|
58
|
+
f"No free port near {preferred_port} after {len(iter_candidate_ports(preferred_port, max_attempts=max_attempts))} attempts",
|
|
59
|
+
) from last_err
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def port_is_free(host: str, port: int) -> bool:
|
|
63
|
+
try:
|
|
64
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
65
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
66
|
+
sock.bind((host, port))
|
|
67
|
+
return True
|
|
68
|
+
except OSError:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def find_available_port(
|
|
73
|
+
host: str,
|
|
74
|
+
preferred_port: int,
|
|
75
|
+
*,
|
|
76
|
+
max_attempts: int | None = None,
|
|
77
|
+
) -> int:
|
|
78
|
+
"""Return the first bindable port without starting a server."""
|
|
79
|
+
for port in iter_candidate_ports(preferred_port, max_attempts=max_attempts):
|
|
80
|
+
if port_is_free(host, port):
|
|
81
|
+
return port
|
|
82
|
+
raise OSError(
|
|
83
|
+
errno.EADDRINUSE,
|
|
84
|
+
f"No free port near {preferred_port}",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def format_connection_lines(
|
|
89
|
+
host: str,
|
|
90
|
+
actual_port: int,
|
|
91
|
+
*,
|
|
92
|
+
preferred_port: int = DEFAULT_SERVE_PORT,
|
|
93
|
+
) -> list[str]:
|
|
94
|
+
"""Human-readable instructions for API + web UI wiring."""
|
|
95
|
+
url = serve_base_url(host, actual_port)
|
|
96
|
+
lines: list[str] = []
|
|
97
|
+
if actual_port != preferred_port:
|
|
98
|
+
lines.append(
|
|
99
|
+
f"Port {preferred_port} is busy — GemCode API is on {url} instead."
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
lines.append(f"GemCode API listening on {url}")
|
|
103
|
+
lines.append("Web UI: run `npm run dev` in gemcode-web-ui → http://localhost:3002")
|
|
104
|
+
lines.append(f"Point the UI at this API (gemcode-web-ui/.env.local):")
|
|
105
|
+
lines.append(f" NEXT_PUBLIC_API_URL={url}")
|
|
106
|
+
return lines
|
|
@@ -90,6 +90,22 @@ def is_serve_running(
|
|
|
90
90
|
|
|
91
91
|
health = probe_health(url)
|
|
92
92
|
if health and health.get("status") == "ok":
|
|
93
|
+
expected = str(project_root.expanduser().resolve())
|
|
94
|
+
raw_root = health.get("project_root") or health.get("cwd")
|
|
95
|
+
if raw_root:
|
|
96
|
+
try:
|
|
97
|
+
actual = str(Path(str(raw_root)).expanduser().resolve())
|
|
98
|
+
if actual != expected:
|
|
99
|
+
return False, {
|
|
100
|
+
"url": url,
|
|
101
|
+
"health": health,
|
|
102
|
+
"external": True,
|
|
103
|
+
"project_mismatch": True,
|
|
104
|
+
"expected_root": expected,
|
|
105
|
+
"actual_root": actual,
|
|
106
|
+
}
|
|
107
|
+
except (OSError, ValueError):
|
|
108
|
+
pass
|
|
93
109
|
return True, {"url": url, "health": health, "external": True}
|
|
94
110
|
return False, state
|
|
95
111
|
|
|
@@ -103,9 +119,19 @@ def start_background_serve(
|
|
|
103
119
|
) -> dict[str, Any]:
|
|
104
120
|
root = project_root.expanduser().resolve()
|
|
105
121
|
running, info = is_serve_running(root, host=host, port=port)
|
|
122
|
+
if info and info.get("project_mismatch"):
|
|
123
|
+
return {"ok": False, "project_mismatch": True, **info}
|
|
106
124
|
if running:
|
|
107
125
|
return {"ok": True, "already_running": True, **(info or {})}
|
|
108
126
|
|
|
127
|
+
preferred_port = port
|
|
128
|
+
try:
|
|
129
|
+
from gemcode.web.serve_bind import find_available_port
|
|
130
|
+
|
|
131
|
+
port = find_available_port(host, preferred_port)
|
|
132
|
+
except OSError as exc:
|
|
133
|
+
return {"ok": False, "error": str(exc), "preferred_port": preferred_port}
|
|
134
|
+
|
|
109
135
|
log_path = root / ".gemcode" / "web-serve.log"
|
|
110
136
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
111
137
|
log_fh = open(log_path, "a", encoding="utf-8")
|
|
@@ -147,7 +173,11 @@ def start_background_serve(
|
|
|
147
173
|
"log": str(log_path),
|
|
148
174
|
}
|
|
149
175
|
write_serve_state(root, payload)
|
|
150
|
-
|
|
176
|
+
result = {"ok": True, "started": True, **payload, "health": health}
|
|
177
|
+
if port != preferred_port:
|
|
178
|
+
result["preferred_port"] = preferred_port
|
|
179
|
+
result["port_fallback"] = True
|
|
180
|
+
return result
|
|
151
181
|
if pid_alive(proc.pid):
|
|
152
182
|
payload = {
|
|
153
183
|
"pid": proc.pid,
|
|
@@ -160,7 +190,11 @@ def start_background_serve(
|
|
|
160
190
|
"warming": True,
|
|
161
191
|
}
|
|
162
192
|
write_serve_state(root, payload)
|
|
163
|
-
|
|
193
|
+
result = {"ok": True, "started": True, **payload}
|
|
194
|
+
if port != preferred_port:
|
|
195
|
+
result["preferred_port"] = preferred_port
|
|
196
|
+
result["port_fallback"] = True
|
|
197
|
+
return result
|
|
164
198
|
return {
|
|
165
199
|
"ok": False,
|
|
166
200
|
"error": f"Server failed to start — see {log_path}",
|
|
@@ -6,16 +6,19 @@ import argparse
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import platform
|
|
9
|
+
import queue
|
|
9
10
|
import re
|
|
10
11
|
import signal
|
|
11
12
|
import subprocess
|
|
12
13
|
import sys
|
|
13
14
|
import threading
|
|
14
|
-
|
|
15
|
+
import time
|
|
16
|
+
from http.server import BaseHTTPRequestHandler
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
from typing import Any
|
|
17
19
|
from urllib.parse import parse_qs, urlparse
|
|
18
20
|
|
|
21
|
+
from gemcode.web.serve_bind import bind_http_server, format_connection_lines
|
|
19
22
|
from gemcode.web.serve_state import (
|
|
20
23
|
clear_serve_state,
|
|
21
24
|
serve_base_url,
|
|
@@ -25,6 +28,10 @@ from gemcode.web.serve_state import (
|
|
|
25
28
|
PYTHON = sys.executable
|
|
26
29
|
SERVER_VERSION = "GemCodeServe/1.0"
|
|
27
30
|
|
|
31
|
+
# SSE / long-turn limits (0 = no server-side turn cap).
|
|
32
|
+
_SSE_KEEPALIVE_S = float(os.environ.get("GEMCODE_WEB_SSE_KEEPALIVE_S", "20"))
|
|
33
|
+
_TURN_TIMEOUT_S = float(os.environ.get("GEMCODE_WEB_TURN_TIMEOUT_S", "0"))
|
|
34
|
+
|
|
28
35
|
|
|
29
36
|
def _mock_allowed() -> bool:
|
|
30
37
|
return os.environ.get("GEMCODE_WEB_ALLOW_MOCK", "").lower() in (
|
|
@@ -184,6 +191,8 @@ def _build_handler(project_root: str) -> type[BaseHTTPRequestHandler]:
|
|
|
184
191
|
"mock_mode": _mock_mode_active(),
|
|
185
192
|
"project_root": root,
|
|
186
193
|
"cwd": root,
|
|
194
|
+
"port": self.server.server_address[1],
|
|
195
|
+
"url": serve_base_url(self.server.server_address[0], self.server.server_address[1]),
|
|
187
196
|
}
|
|
188
197
|
).encode()
|
|
189
198
|
self.send_response(200)
|
|
@@ -436,18 +445,67 @@ def _build_handler(project_root: str) -> type[BaseHTTPRequestHandler]:
|
|
|
436
445
|
|
|
437
446
|
self.send_response(200)
|
|
438
447
|
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
|
439
|
-
self.send_header("Cache-Control", "no-cache")
|
|
448
|
+
self.send_header("Cache-Control", "no-cache, no-transform")
|
|
440
449
|
self.send_header("Connection", "keep-alive")
|
|
450
|
+
self.send_header("X-Accel-Buffering", "no")
|
|
441
451
|
self._cors()
|
|
442
452
|
self.end_headers()
|
|
443
453
|
|
|
454
|
+
try:
|
|
455
|
+
self.connection.settimeout(None)
|
|
456
|
+
except Exception:
|
|
457
|
+
pass
|
|
458
|
+
|
|
459
|
+
out_q: queue.Queue[bytes | None] = queue.Queue()
|
|
460
|
+
|
|
461
|
+
def _stdout_reader() -> None:
|
|
462
|
+
try:
|
|
463
|
+
while proc.stdout is not None:
|
|
464
|
+
chunk = proc.stdout.read(4096)
|
|
465
|
+
if not chunk:
|
|
466
|
+
break
|
|
467
|
+
out_q.put(chunk)
|
|
468
|
+
except Exception:
|
|
469
|
+
pass
|
|
470
|
+
finally:
|
|
471
|
+
out_q.put(None)
|
|
472
|
+
|
|
473
|
+
threading.Thread(target=_stdout_reader, daemon=True).start()
|
|
474
|
+
|
|
444
475
|
done_re = re.compile(rb'"type"\s*:\s*"done"')
|
|
445
476
|
saw_done = False
|
|
446
477
|
carry = b""
|
|
478
|
+
last_activity = time.monotonic()
|
|
479
|
+
turn_deadline = (
|
|
480
|
+
time.monotonic() + _TURN_TIMEOUT_S if _TURN_TIMEOUT_S > 0 else None
|
|
481
|
+
)
|
|
447
482
|
try:
|
|
448
483
|
while True:
|
|
449
|
-
|
|
450
|
-
|
|
484
|
+
if turn_deadline is not None and time.monotonic() > turn_deadline:
|
|
485
|
+
proc.kill()
|
|
486
|
+
if not saw_done:
|
|
487
|
+
payload = json.dumps(
|
|
488
|
+
{
|
|
489
|
+
"type": "error",
|
|
490
|
+
"error": (
|
|
491
|
+
f"Turn exceeded server limit ({int(_TURN_TIMEOUT_S)}s). "
|
|
492
|
+
"Set GEMCODE_WEB_TURN_TIMEOUT_S=0 for no cap."
|
|
493
|
+
),
|
|
494
|
+
}
|
|
495
|
+
)
|
|
496
|
+
self.wfile.write(f"data: {payload}\n\n".encode())
|
|
497
|
+
self.wfile.write(b'data: {"type": "done"}\n\n')
|
|
498
|
+
self.wfile.flush()
|
|
499
|
+
break
|
|
500
|
+
try:
|
|
501
|
+
chunk = out_q.get(timeout=5.0)
|
|
502
|
+
except queue.Empty:
|
|
503
|
+
if time.monotonic() - last_activity >= _SSE_KEEPALIVE_S:
|
|
504
|
+
self.wfile.write(b": keepalive\n\n")
|
|
505
|
+
self.wfile.flush()
|
|
506
|
+
last_activity = time.monotonic()
|
|
507
|
+
continue
|
|
508
|
+
if chunk is None:
|
|
451
509
|
break
|
|
452
510
|
window = carry + chunk
|
|
453
511
|
if not saw_done and done_re.search(window):
|
|
@@ -455,11 +513,15 @@ def _build_handler(project_root: str) -> type[BaseHTTPRequestHandler]:
|
|
|
455
513
|
carry = chunk[-64:]
|
|
456
514
|
self.wfile.write(chunk)
|
|
457
515
|
self.wfile.flush()
|
|
516
|
+
last_activity = time.monotonic()
|
|
458
517
|
except (BrokenPipeError, ConnectionResetError):
|
|
459
518
|
proc.kill()
|
|
460
519
|
finally:
|
|
461
520
|
try:
|
|
462
|
-
|
|
521
|
+
if _TURN_TIMEOUT_S > 0:
|
|
522
|
+
proc.wait(timeout=max(30.0, _TURN_TIMEOUT_S))
|
|
523
|
+
else:
|
|
524
|
+
proc.wait()
|
|
463
525
|
except subprocess.TimeoutExpired:
|
|
464
526
|
proc.kill()
|
|
465
527
|
proc.wait(timeout=5)
|
|
@@ -548,7 +610,8 @@ def run_server(
|
|
|
548
610
|
print(f"[serve] Mesh scheduler bootstrap failed: {exc}", flush=True)
|
|
549
611
|
|
|
550
612
|
handler = _build_handler(str(root))
|
|
551
|
-
|
|
613
|
+
preferred_port = bind_port
|
|
614
|
+
httpd, bind_port = bind_http_server(host, handler, preferred_port)
|
|
552
615
|
url = serve_base_url(host, bind_port)
|
|
553
616
|
|
|
554
617
|
def _shutdown(*_args: Any) -> None:
|
|
@@ -578,7 +641,12 @@ def run_server(
|
|
|
578
641
|
print(f"Project root: {root}", flush=True)
|
|
579
642
|
if session_id:
|
|
580
643
|
print(f"CLI session id: {session_id}", flush=True)
|
|
581
|
-
|
|
644
|
+
for line in format_connection_lines(host, bind_port, preferred_port=preferred_port):
|
|
645
|
+
print(line, flush=True)
|
|
646
|
+
if _TURN_TIMEOUT_S <= 0:
|
|
647
|
+
print("[serve] Long turns: no server-side turn timeout (GEMCODE_WEB_TURN_TIMEOUT_S=0)", flush=True)
|
|
648
|
+
else:
|
|
649
|
+
print(f"[serve] Long turns: server cap {int(_TURN_TIMEOUT_S)}s (set GEMCODE_WEB_TURN_TIMEOUT_S=0 to disable)", flush=True)
|
|
582
650
|
if _mock_mode_active():
|
|
583
651
|
print("[serve] WARNING: mock mode active (GEMCODE_WEB_ALLOW_MOCK=1)", flush=True)
|
|
584
652
|
else:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import uuid
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -25,22 +26,28 @@ def handle_sessions_post(project_root: str, data: dict[str, Any]) -> tuple[int,
|
|
|
25
26
|
return 400, {"ok": False, "error": "Invalid project root"}
|
|
26
27
|
|
|
27
28
|
action = str(data.get("action") or "").strip().lower()
|
|
28
|
-
session_id = str(data.get("session_id") or "").strip()
|
|
29
|
-
if not session_id:
|
|
30
|
-
return 400, {"ok": False, "error": "session_id is required"}
|
|
31
29
|
|
|
32
30
|
try:
|
|
33
31
|
from gemcode.session_store import name_session, touch_session
|
|
34
32
|
|
|
33
|
+
if action == "new":
|
|
34
|
+
session_id = str(data.get("session_id") or "").strip() or uuid.uuid4().hex
|
|
35
|
+
touch_session(root, session_id)
|
|
36
|
+
return 200, {"ok": True, "session_id": session_id, "action": "new"}
|
|
37
|
+
|
|
38
|
+
session_id = str(data.get("session_id") or "").strip()
|
|
39
|
+
if not session_id:
|
|
40
|
+
return 400, {"ok": False, "error": "session_id is required"}
|
|
41
|
+
|
|
35
42
|
if action in ("name", "rename"):
|
|
36
43
|
name = str(data.get("name") or "").strip()
|
|
37
44
|
if not name:
|
|
38
45
|
return 400, {"ok": False, "error": "name is required"}
|
|
39
46
|
name_session(root, session_id, name)
|
|
40
47
|
return 200, {"ok": True, "session_id": session_id, "name": name}
|
|
41
|
-
if action
|
|
48
|
+
if action in ("touch", "resume"):
|
|
42
49
|
touch_session(root, session_id)
|
|
43
|
-
return 200, {"ok": True, "session_id": session_id}
|
|
50
|
+
return 200, {"ok": True, "session_id": session_id, "action": action}
|
|
44
51
|
return 400, {"ok": False, "error": f"Unknown action: {action}"}
|
|
45
52
|
except Exception as exc:
|
|
46
53
|
return 500, {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
@@ -357,10 +357,11 @@ async def _resolve_web_tool_permission(
|
|
|
357
357
|
if getattr(cfg, "yes_to_all", False) or getattr(cfg, "super_mode", False):
|
|
358
358
|
return True
|
|
359
359
|
|
|
360
|
-
from gemcode.web.hitl_bridge import new_approval_id, wait_for_web_approval
|
|
360
|
+
from gemcode.web.hitl_bridge import new_approval_id, register_pending_approval, wait_for_web_approval
|
|
361
361
|
|
|
362
362
|
aid = (approval_id or "").strip() or new_approval_id(session_id, fc_id or tool_name)
|
|
363
363
|
already_emitted = bool((approval_id or "").strip())
|
|
364
|
+
register_pending_approval(aid)
|
|
364
365
|
if not already_emitted:
|
|
365
366
|
_sse_emit(
|
|
366
367
|
{
|
|
@@ -590,7 +591,7 @@ async def _stream_gemcode_turn(
|
|
|
590
591
|
if not web_hitl:
|
|
591
592
|
return emitted
|
|
592
593
|
try:
|
|
593
|
-
from gemcode.web.hitl_bridge import new_approval_id
|
|
594
|
+
from gemcode.web.hitl_bridge import new_approval_id, register_pending_approval
|
|
594
595
|
|
|
595
596
|
for fc in event.get_function_calls() or []:
|
|
596
597
|
name = getattr(fc, "name", "") or ""
|
|
@@ -603,6 +604,7 @@ async def _stream_gemcode_turn(
|
|
|
603
604
|
continue
|
|
604
605
|
hint = format_tool_call_extras(fc) or ""
|
|
605
606
|
approval_id = new_approval_id(session_id, tool_id)
|
|
607
|
+
register_pending_approval(approval_id)
|
|
606
608
|
preflight_approvals.append((approval_id, name, hint, tool_id))
|
|
607
609
|
_sse_emit(
|
|
608
610
|
{
|
|
@@ -989,11 +991,19 @@ def _apply_web_request_options(cfg: GemCodeConfig, req: dict[str, Any], workspac
|
|
|
989
991
|
cfg.enable_memory = bool(caps.get("enable_memory"))
|
|
990
992
|
if "enable_web_search" in caps:
|
|
991
993
|
cfg.enable_web_search = bool(caps.get("enable_web_search"))
|
|
992
|
-
if
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
994
|
+
if "enable_deep_research" in caps:
|
|
995
|
+
cfg.enable_deep_research = bool(caps.get("enable_deep_research"))
|
|
996
|
+
if "enable_computer_use" in caps:
|
|
997
|
+
cfg.enable_computer_use = bool(caps.get("enable_computer_use"))
|
|
998
|
+
if "enable_embeddings" in caps:
|
|
999
|
+
cfg.enable_embeddings = bool(caps.get("enable_embeddings"))
|
|
1000
|
+
if "enable_maps_grounding" in caps:
|
|
1001
|
+
cfg.enable_maps_grounding = bool(caps.get("enable_maps_grounding"))
|
|
1002
|
+
if "enable_code_executor" in caps:
|
|
1003
|
+
cfg.enable_code_executor = bool(caps.get("enable_code_executor"))
|
|
1004
|
+
cap_mode = caps.get("capability_mode")
|
|
1005
|
+
if isinstance(cap_mode, str) and cap_mode.strip():
|
|
1006
|
+
cfg.capability_mode = cap_mode.strip().lower()
|
|
997
1007
|
if bool(caps.get("proposal_mode")):
|
|
998
1008
|
object.__setattr__(cfg, "ide_proposal_mode", True)
|
|
999
1009
|
object.__setattr__(cfg, "ide_allow_write", True)
|
|
@@ -1021,6 +1031,17 @@ def _apply_web_request_options(cfg: GemCodeConfig, req: dict[str, Any], workspac
|
|
|
1021
1031
|
except (TypeError, ValueError):
|
|
1022
1032
|
pass
|
|
1023
1033
|
|
|
1034
|
+
thinking_budget = req.get("thinking_budget")
|
|
1035
|
+
if thinking_budget is not None:
|
|
1036
|
+
try:
|
|
1037
|
+
cfg.thinking_budget = int(thinking_budget)
|
|
1038
|
+
except (TypeError, ValueError):
|
|
1039
|
+
pass
|
|
1040
|
+
|
|
1041
|
+
thinking_level = req.get("thinking_level")
|
|
1042
|
+
if isinstance(thinking_level, str) and thinking_level.strip():
|
|
1043
|
+
cfg.thinking_level = thinking_level.strip().lower()
|
|
1044
|
+
|
|
1024
1045
|
return workspace_mode
|
|
1025
1046
|
|
|
1026
1047
|
|
|
@@ -1071,6 +1092,8 @@ async def run_adapter(req: dict[str, Any]) -> None:
|
|
|
1071
1092
|
caps = req.get("capabilities")
|
|
1072
1093
|
chat_research = isinstance(caps, dict) and bool(caps.get("enable_deep_research"))
|
|
1073
1094
|
chat_computer = isinstance(caps, dict) and bool(caps.get("enable_computer_use"))
|
|
1095
|
+
chat_embeddings = isinstance(caps, dict) and bool(caps.get("enable_embeddings"))
|
|
1096
|
+
chat_maps = isinstance(caps, dict) and bool(caps.get("enable_maps_grounding"))
|
|
1074
1097
|
if not chat_research:
|
|
1075
1098
|
try:
|
|
1076
1099
|
cfg.enable_deep_research = False
|
|
@@ -1082,6 +1105,16 @@ async def run_adapter(req: dict[str, Any]) -> None:
|
|
|
1082
1105
|
cfg.enable_computer_use = False
|
|
1083
1106
|
except Exception:
|
|
1084
1107
|
pass
|
|
1108
|
+
if not chat_embeddings:
|
|
1109
|
+
try:
|
|
1110
|
+
cfg.enable_embeddings = False
|
|
1111
|
+
except Exception:
|
|
1112
|
+
pass
|
|
1113
|
+
if not chat_maps:
|
|
1114
|
+
try:
|
|
1115
|
+
cfg.enable_maps_grounding = False
|
|
1116
|
+
except Exception:
|
|
1117
|
+
pass
|
|
1085
1118
|
|
|
1086
1119
|
workspace_mode = _apply_web_request_options(cfg, req, workspace_mode)
|
|
1087
1120
|
|
|
@@ -11,10 +11,9 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
from gemcode.config import GemCodeConfig
|
|
13
13
|
from gemcode.session_runtime import create_runner
|
|
14
|
-
from gemcode.web.terminal_repl import run_mock_turn, run_single_turn
|
|
15
|
-
|
|
16
|
-
|
|
17
14
|
from gemcode.web.project_root import resolve_web_project_root
|
|
15
|
+
from gemcode.web.sse_adapter import _apply_web_permissions, _ensure_web_hitl
|
|
16
|
+
from gemcode.web.terminal_repl import run_mock_turn, run_single_turn
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
def _resolve_root(raw_path: str) -> Path:
|
|
@@ -27,18 +26,23 @@ async def _terminal_turn_async(
|
|
|
27
26
|
prompt: str,
|
|
28
27
|
session_id: str,
|
|
29
28
|
user_id: str,
|
|
29
|
+
permissions: dict[str, Any] | None = None,
|
|
30
30
|
) -> dict[str, Any]:
|
|
31
31
|
from gemcode.cli import _initialize_gemcode_project
|
|
32
32
|
|
|
33
33
|
cfg = GemCodeConfig(project_root=project_root)
|
|
34
34
|
cfg.permission_mode = os.environ.get("GEMCODE_PERMISSION_MODE", cfg.permission_mode)
|
|
35
|
-
cfg.yes_to_all =
|
|
36
|
-
"1",
|
|
37
|
-
"true",
|
|
38
|
-
"yes",
|
|
39
|
-
"on",
|
|
40
|
-
)
|
|
35
|
+
cfg.yes_to_all = False
|
|
41
36
|
cfg.interactive_permission_ask = False
|
|
37
|
+
object.__setattr__(cfg, "_gemcode_web_sse", False)
|
|
38
|
+
if isinstance(permissions, dict):
|
|
39
|
+
_apply_web_permissions(cfg, {"permissions": permissions})
|
|
40
|
+
_ensure_web_hitl(cfg)
|
|
41
|
+
# Terminal is one-shot (no SSE HITL bridge) — respect explicit auto-approve only.
|
|
42
|
+
if getattr(cfg, "_web_interactive_hitl", False):
|
|
43
|
+
cfg.yes_to_all = False
|
|
44
|
+
cfg.interactive_permission_ask = False
|
|
45
|
+
object.__setattr__(cfg, "_web_interactive_hitl", False)
|
|
42
46
|
_initialize_gemcode_project(cfg)
|
|
43
47
|
|
|
44
48
|
buf = io.StringIO()
|
|
@@ -76,6 +80,7 @@ def handle_terminal_post(data: dict[str, Any], raw_path: str) -> tuple[int, dict
|
|
|
76
80
|
|
|
77
81
|
session_id = str(data.get("session_id") or "web-terminal")
|
|
78
82
|
user_id = str(data.get("user_id") or "web-terminal")
|
|
83
|
+
permissions = data.get("permissions") if isinstance(data.get("permissions"), dict) else None
|
|
79
84
|
|
|
80
85
|
if not os.environ.get("GEMCODE_WEB_MOCK_RESPONSE") and not (
|
|
81
86
|
os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY")
|
|
@@ -87,7 +92,13 @@ def handle_terminal_post(data: dict[str, Any], raw_path: str) -> tuple[int, dict
|
|
|
87
92
|
|
|
88
93
|
try:
|
|
89
94
|
payload = asyncio.run(
|
|
90
|
-
_terminal_turn_async(
|
|
95
|
+
_terminal_turn_async(
|
|
96
|
+
root,
|
|
97
|
+
prompt=prompt,
|
|
98
|
+
session_id=session_id,
|
|
99
|
+
user_id=user_id,
|
|
100
|
+
permissions=permissions,
|
|
101
|
+
)
|
|
91
102
|
)
|
|
92
103
|
return 200, payload
|
|
93
104
|
except Exception as exc:
|
|
@@ -44,6 +44,12 @@ def handle_panel_get(
|
|
|
44
44
|
lines = _format_status_lines(cfg, session_id=session_id)
|
|
45
45
|
return 200, {"ok": True, "kind": k, "lines": lines, "text": "\n".join(lines)}
|
|
46
46
|
|
|
47
|
+
if k == "tools":
|
|
48
|
+
from gemcode.repl_commands import format_tools_lines
|
|
49
|
+
|
|
50
|
+
lines = format_tools_lines(cfg)
|
|
51
|
+
return 200, {"ok": True, "kind": k, "lines": lines, "text": "\n".join(lines)}
|
|
52
|
+
|
|
47
53
|
if k == "hooks":
|
|
48
54
|
from gemcode.repl_commands import format_hooks_lines
|
|
49
55
|
|