gemcode 0.4.23__tar.gz → 0.4.25__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.23/src/gemcode.egg-info → gemcode-0.4.25}/PKG-INFO +1 -1
- {gemcode-0.4.23 → gemcode-0.4.25}/pyproject.toml +1 -1
- gemcode-0.4.25/src/gemcode/web/preview_api.py +213 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/server.py +29 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/sse_adapter.py +57 -12
- {gemcode-0.4.23 → gemcode-0.4.25/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode.egg-info/SOURCES.txt +2 -0
- gemcode-0.4.25/tests/test_preview_proxy.py +21 -0
- gemcode-0.4.25/tests/test_web_permissions_hitl.py +71 -0
- gemcode-0.4.23/src/gemcode/web/preview_api.py +0 -82
- {gemcode-0.4.23 → gemcode-0.4.25}/LICENSE +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/MANIFEST.in +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/README.md +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/setup.cfg +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/__init__.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/__main__.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/a2a_bridge.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/agent.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/agent_habits.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/agent_intelligence.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/agent_mesh.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/agent_triggers.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/audit.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/automations.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/autotune.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/cli.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/codebase_awareness.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/compaction.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/config.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/credentials.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/delegation_learning.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/event_bus.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/fleet_reports.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/habit_chains.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/habit_runs.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/hooks.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/interactions.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/invoke.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/kaira_client.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/kaira_ipc.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/learning.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/limits.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/org.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/paths.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/permissions.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/pricing.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/config.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/refine.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/rules.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/self_healing.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/session_store.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/skills.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/thinking.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tool_synthesis.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/automations_tools.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/org_tools.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/trust.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/version.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/vertex.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/wal.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/chat_skills.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/customize_api.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/files_api.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/fleet_api.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/hitl_bridge.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/project_root.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/runtime_api.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/serve_bind.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/serve_state.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/sessions_api.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/terminal_api.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/ui_chat_store.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/web_config_api.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/web/workspace_panel_api.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_add_dir.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_agent_habits.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_agent_mesh.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_autocompact.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_automations.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_capability_routing.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_cli_init.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_context_budget.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_context_warning.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_credentials.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_event_bus.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_files_api.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_fleet_reports.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_habit_chains.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_habit_runs.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_hosted_tenant.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_kaira_ipc_paths.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_modality_tools.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_model_errors.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_model_routing.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_paths.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_permissions.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_repl_commands.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_repl_slash.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_session_runtime_cache.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_skills.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_slash_commands.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_sse_confirmation_batch.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_thinking_config.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_token_budget.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_tools.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_web_server.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.4.23 → gemcode-0.4.25}/tests/test_workspace_hints.py +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Detect local (or tenant-pod) dev servers and reverse-proxy them for the web UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import socket
|
|
6
|
+
from http.client import HTTPConnection
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.error import URLError
|
|
9
|
+
from urllib.parse import unquote
|
|
10
|
+
from urllib.request import Request, urlopen
|
|
11
|
+
|
|
12
|
+
# Common dev-server ports (GemCode web UI uses 3002; API 3001)
|
|
13
|
+
COMMON_DEV_PORTS = (
|
|
14
|
+
3000,
|
|
15
|
+
3003,
|
|
16
|
+
3004,
|
|
17
|
+
5173,
|
|
18
|
+
5174,
|
|
19
|
+
8080,
|
|
20
|
+
8000,
|
|
21
|
+
4200,
|
|
22
|
+
5000,
|
|
23
|
+
4321,
|
|
24
|
+
8888,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_STRIP_RESPONSE_HEADERS = frozenset(
|
|
28
|
+
{
|
|
29
|
+
"transfer-encoding",
|
|
30
|
+
"connection",
|
|
31
|
+
"content-length",
|
|
32
|
+
"content-encoding",
|
|
33
|
+
"x-frame-options",
|
|
34
|
+
"content-security-policy",
|
|
35
|
+
"content-security-policy-report-only",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _port_open(host: str, port: int, *, timeout: float = 0.2) -> bool:
|
|
41
|
+
try:
|
|
42
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
43
|
+
return True
|
|
44
|
+
except OSError:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _probe_http(url: str, *, timeout: float = 1.5) -> dict[str, Any] | None:
|
|
49
|
+
try:
|
|
50
|
+
req = Request(url, headers={"User-Agent": "GemCode-Web-Preview/1.0"})
|
|
51
|
+
with urlopen(req, timeout=timeout) as resp:
|
|
52
|
+
ctype = (resp.headers.get("Content-Type") or "").lower()
|
|
53
|
+
return {
|
|
54
|
+
"ok": True,
|
|
55
|
+
"status": resp.status,
|
|
56
|
+
"content_type": ctype,
|
|
57
|
+
}
|
|
58
|
+
except (URLError, OSError, TimeoutError, ValueError):
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def scan_local_preview_ports(
|
|
63
|
+
*,
|
|
64
|
+
host: str = "127.0.0.1",
|
|
65
|
+
extra_ports: list[int] | None = None,
|
|
66
|
+
) -> list[dict[str, Any]]:
|
|
67
|
+
"""Return listening HTTP-ish ports on localhost (tenant pod when hosted)."""
|
|
68
|
+
ports: list[int] = list(COMMON_DEV_PORTS)
|
|
69
|
+
if extra_ports:
|
|
70
|
+
for p in extra_ports:
|
|
71
|
+
if isinstance(p, int) and 1 < p < 65536 and p not in ports:
|
|
72
|
+
ports.append(p)
|
|
73
|
+
|
|
74
|
+
found: list[dict[str, Any]] = []
|
|
75
|
+
for port in ports:
|
|
76
|
+
if not _port_open(host, port):
|
|
77
|
+
continue
|
|
78
|
+
url = f"http://{host}:{port}"
|
|
79
|
+
probe = _probe_http(url)
|
|
80
|
+
found.append(
|
|
81
|
+
{
|
|
82
|
+
"port": port,
|
|
83
|
+
"url": url,
|
|
84
|
+
"reachable": probe is not None,
|
|
85
|
+
"content_type": (probe or {}).get("content_type"),
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
return found
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def handle_preview_get(kind: str) -> tuple[int, dict[str, Any]]:
|
|
92
|
+
k = (kind or "ports").strip().lower()
|
|
93
|
+
if k == "ports":
|
|
94
|
+
ports = scan_local_preview_ports()
|
|
95
|
+
return 200, {"ok": True, "ports": ports, "host": "127.0.0.1"}
|
|
96
|
+
return 400, {"ok": False, "error": f"Unknown preview kind: {kind}"}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _inject_base_href(html: str, base_path: str) -> str:
|
|
100
|
+
base_tag = f'<base href="{base_path}">'
|
|
101
|
+
if "<base " in html.lower():
|
|
102
|
+
return html
|
|
103
|
+
lower = html.lower()
|
|
104
|
+
idx = lower.find("<head")
|
|
105
|
+
if idx >= 0:
|
|
106
|
+
gt = html.find(">", idx)
|
|
107
|
+
if gt >= 0:
|
|
108
|
+
return html[: gt + 1] + base_tag + html[gt + 1 :]
|
|
109
|
+
return base_tag + html
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def handle_preview_proxy(
|
|
113
|
+
port: int,
|
|
114
|
+
sub_path: str = "/",
|
|
115
|
+
*,
|
|
116
|
+
query: str = "",
|
|
117
|
+
) -> tuple[int, bytes, dict[str, str]]:
|
|
118
|
+
"""
|
|
119
|
+
Reverse-proxy HTTP from 127.0.0.1:{port} (on this process / tenant pod).
|
|
120
|
+
|
|
121
|
+
Returns (status, body, response_headers).
|
|
122
|
+
"""
|
|
123
|
+
if not isinstance(port, int) or port < 1 or port > 65535:
|
|
124
|
+
return 400, b'{"error":"Invalid port"}', {"Content-Type": "application/json; charset=utf-8"}
|
|
125
|
+
|
|
126
|
+
# Block GemCode's own API/UI ports from being framed as "app preview"
|
|
127
|
+
if port in (3001, 3002):
|
|
128
|
+
return (
|
|
129
|
+
400,
|
|
130
|
+
b'{"error":"Port reserved for GemCode API/UI"}',
|
|
131
|
+
{"Content-Type": "application/json; charset=utf-8"},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
raw = sub_path or "/"
|
|
135
|
+
if not raw.startswith("/"):
|
|
136
|
+
raw = "/" + raw
|
|
137
|
+
# Collapse .. segments
|
|
138
|
+
parts: list[str] = []
|
|
139
|
+
for seg in raw.split("/"):
|
|
140
|
+
if seg in ("", "."):
|
|
141
|
+
continue
|
|
142
|
+
if seg == "..":
|
|
143
|
+
if parts:
|
|
144
|
+
parts.pop()
|
|
145
|
+
continue
|
|
146
|
+
parts.append(seg)
|
|
147
|
+
path = "/" + "/".join(parts)
|
|
148
|
+
if raw.endswith("/") and path != "/":
|
|
149
|
+
path += "/"
|
|
150
|
+
|
|
151
|
+
qs = (query or "").lstrip("?")
|
|
152
|
+
request_path = path if not qs else f"{path}?{qs}"
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
conn = HTTPConnection("127.0.0.1", port, timeout=20.0)
|
|
156
|
+
conn.request(
|
|
157
|
+
"GET",
|
|
158
|
+
request_path,
|
|
159
|
+
headers={"User-Agent": "GemCode-Web-Preview/1.0", "Accept": "*/*"},
|
|
160
|
+
)
|
|
161
|
+
resp = conn.getresponse()
|
|
162
|
+
body = resp.read()
|
|
163
|
+
ctype = (resp.getheader("Content-Type") or "").lower()
|
|
164
|
+
headers: dict[str, str] = {}
|
|
165
|
+
for key, value in resp.getheaders():
|
|
166
|
+
if key.lower() in _STRIP_RESPONSE_HEADERS:
|
|
167
|
+
continue
|
|
168
|
+
headers[key] = value
|
|
169
|
+
status = int(resp.status)
|
|
170
|
+
conn.close()
|
|
171
|
+
except OSError as exc:
|
|
172
|
+
msg = f'{{"error":"Preview upstream unreachable: {type(exc).__name__}: {exc}"}}'
|
|
173
|
+
return 502, msg.encode(), {"Content-Type": "application/json; charset=utf-8"}
|
|
174
|
+
|
|
175
|
+
proxy_base = f"/api/preview/proxy/{port}/"
|
|
176
|
+
if "text/html" in ctype:
|
|
177
|
+
try:
|
|
178
|
+
text = body.decode("utf-8", errors="replace")
|
|
179
|
+
text = _inject_base_href(text, proxy_base)
|
|
180
|
+
body = text.encode("utf-8")
|
|
181
|
+
headers["Content-Type"] = "text/html; charset=utf-8"
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
headers["Cache-Control"] = "no-store"
|
|
186
|
+
headers["X-Frame-Options"] = "SAMEORIGIN"
|
|
187
|
+
headers["Content-Security-Policy"] = (
|
|
188
|
+
"default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; "
|
|
189
|
+
"script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline'; "
|
|
190
|
+
"frame-ancestors 'self';"
|
|
191
|
+
)
|
|
192
|
+
return status, body, headers
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def parse_preview_proxy_path(raw_path: str) -> tuple[int, str] | None:
|
|
196
|
+
"""Parse `/api/preview/proxy/{port}` or `/api/preview/proxy/{port}/...` → (port, sub_path)."""
|
|
197
|
+
prefix = "/api/preview/proxy/"
|
|
198
|
+
if not raw_path.startswith(prefix):
|
|
199
|
+
return None
|
|
200
|
+
rest = raw_path[len(prefix) :]
|
|
201
|
+
if not rest:
|
|
202
|
+
return None
|
|
203
|
+
parts = rest.split("/", 1)
|
|
204
|
+
try:
|
|
205
|
+
port = int(parts[0])
|
|
206
|
+
except ValueError:
|
|
207
|
+
return None
|
|
208
|
+
sub = "/"
|
|
209
|
+
if len(parts) > 1 and parts[1]:
|
|
210
|
+
sub = "/" + unquote(parts[1])
|
|
211
|
+
elif rest.endswith("/") or raw_path.endswith("/"):
|
|
212
|
+
sub = "/"
|
|
213
|
+
return port, sub
|
|
@@ -304,6 +304,35 @@ def _build_handler(project_root: str) -> type[BaseHTTPRequestHandler]:
|
|
|
304
304
|
self.wfile.write(body)
|
|
305
305
|
return
|
|
306
306
|
|
|
307
|
+
if parsed.path.startswith("/api/preview/proxy/"):
|
|
308
|
+
try:
|
|
309
|
+
from gemcode.web.preview_api import handle_preview_proxy, parse_preview_proxy_path
|
|
310
|
+
|
|
311
|
+
parsed_proxy = parse_preview_proxy_path(parsed.path)
|
|
312
|
+
if not parsed_proxy:
|
|
313
|
+
status, body, hdrs = (
|
|
314
|
+
400,
|
|
315
|
+
b'{"error":"Invalid preview proxy path"}',
|
|
316
|
+
{"Content-Type": "application/json; charset=utf-8"},
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
port, sub_path = parsed_proxy
|
|
320
|
+
status, body, hdrs = handle_preview_proxy(
|
|
321
|
+
port, sub_path, query=parsed.query or ""
|
|
322
|
+
)
|
|
323
|
+
except Exception as exc:
|
|
324
|
+
status = 500
|
|
325
|
+
body = json.dumps({"error": f"{type(exc).__name__}: {exc}"}).encode()
|
|
326
|
+
hdrs = {"Content-Type": "application/json; charset=utf-8"}
|
|
327
|
+
self.send_response(status)
|
|
328
|
+
for hk, hv in hdrs.items():
|
|
329
|
+
self.send_header(hk, hv)
|
|
330
|
+
self.send_header("Content-Length", str(len(body)))
|
|
331
|
+
self._cors()
|
|
332
|
+
self.end_headers()
|
|
333
|
+
self.wfile.write(body)
|
|
334
|
+
return
|
|
335
|
+
|
|
307
336
|
fleet_get = {
|
|
308
337
|
"/api/org": "org",
|
|
309
338
|
"/api/habits": "habits",
|
|
@@ -489,15 +489,33 @@ def _inject_web_code_context(cfg: GemCodeConfig, prompt: str, req: dict[str, Any
|
|
|
489
489
|
"or type @path — unless they mean the whole project (see below)."
|
|
490
490
|
)
|
|
491
491
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
'When they ask to analyze the whole codebase, project, repo, or "all files", start with `repo_map` and '
|
|
496
|
-
'`list_directory` on `.` — do not ask which file first.',
|
|
497
|
-
"**Web UI permissions** — shell and mutating tools show an **Approve / Deny** dialog in the browser. "
|
|
498
|
-
"Never tell the user to approve in a terminal; wait for the in-app prompt and continue after approval.",
|
|
499
|
-
]
|
|
492
|
+
# Permission mode for THIS turn (must match actual HITL / auto-approve behavior).
|
|
493
|
+
lines.append(
|
|
494
|
+
'When the user says "this file", "the current one", or similar, use the active / @-referenced file above.'
|
|
500
495
|
)
|
|
496
|
+
lines.append(
|
|
497
|
+
'When they ask to analyze the whole codebase, project, repo, or "all files", start with `repo_map` and '
|
|
498
|
+
'`list_directory` on `.` — do not ask which file first.'
|
|
499
|
+
)
|
|
500
|
+
auto_on = bool(
|
|
501
|
+
getattr(cfg, "yes_to_all", False)
|
|
502
|
+
or getattr(cfg, "super_mode", False)
|
|
503
|
+
or not getattr(cfg, "_web_interactive_hitl", True)
|
|
504
|
+
)
|
|
505
|
+
if auto_on:
|
|
506
|
+
lines.append(
|
|
507
|
+
"**Web UI permissions — AUTO-APPROVE is ON for this turn.** "
|
|
508
|
+
"Shell and mutating tools run immediately with **no** Yes/No card. "
|
|
509
|
+
"Never mention Approve, Deny, approval prompts, dialogs, or waiting for the user — "
|
|
510
|
+
"just run tools and report what you did."
|
|
511
|
+
)
|
|
512
|
+
else:
|
|
513
|
+
lines.append(
|
|
514
|
+
"**Web UI permissions — interactive.** Shell and mutating tools pause for an "
|
|
515
|
+
"**inline Yes/No card in the chat** (also pinned above the composer). "
|
|
516
|
+
"Do **not** invent Approve/Deny dialog text or tell the user to open a popup — "
|
|
517
|
+
"wait silently; after they tap Yes or No the turn continues."
|
|
518
|
+
)
|
|
501
519
|
|
|
502
520
|
if mode == "agents":
|
|
503
521
|
lines.extend(
|
|
@@ -920,6 +938,7 @@ def _apply_web_permissions(cfg: GemCodeConfig, req: dict[str, Any]) -> None:
|
|
|
920
938
|
}
|
|
921
939
|
object.__setattr__(cfg, "_web_auto_approve", normalized)
|
|
922
940
|
|
|
941
|
+
# UI is source of truth for web chat — do not leave env GEMCODE_SUPER_MODE stuck on.
|
|
923
942
|
super_mode = bool(perms.get("super_mode"))
|
|
924
943
|
if super_mode:
|
|
925
944
|
cfg.super_mode = True
|
|
@@ -929,6 +948,7 @@ def _apply_web_permissions(cfg: GemCodeConfig, req: dict[str, Any]) -> None:
|
|
|
929
948
|
object.__setattr__(cfg, "_web_interactive_hitl", False)
|
|
930
949
|
return
|
|
931
950
|
|
|
951
|
+
cfg.super_mode = False
|
|
932
952
|
if all(normalized.values()):
|
|
933
953
|
cfg.yes_to_all = True
|
|
934
954
|
cfg.interactive_permission_ask = False
|
|
@@ -946,13 +966,33 @@ def _apply_web_permissions(cfg: GemCodeConfig, req: dict[str, Any]) -> None:
|
|
|
946
966
|
|
|
947
967
|
|
|
948
968
|
def _configure_web_permissions(cfg: GemCodeConfig, req: dict[str, Any]) -> None:
|
|
949
|
-
"""Web chat
|
|
969
|
+
"""Web chat HITL follows the UI request, not process-level GEMCODE_SUPER_MODE.
|
|
970
|
+
|
|
971
|
+
Hosted tenant images historically set ``GEMCODE_SUPER_MODE=1`` so mesh/jobs are
|
|
972
|
+
unattended. That must not silently skip Yes/No cards when the user has
|
|
973
|
+
Auto-approve off in the web UI. Mesh workers still use
|
|
974
|
+
``GEMCODE_MESH_WORKER_UNATTENDED`` independently.
|
|
975
|
+
"""
|
|
950
976
|
object.__setattr__(cfg, "_gemcode_web_sse", True)
|
|
951
|
-
|
|
952
|
-
|
|
977
|
+
|
|
978
|
+
perms = req.get("permissions")
|
|
979
|
+
has_perms = isinstance(perms, dict)
|
|
980
|
+
|
|
981
|
+
# Clear env-applied super mode before applying the UI payload.
|
|
982
|
+
if not (has_perms and bool(perms.get("super_mode"))):
|
|
983
|
+
cfg.super_mode = False
|
|
953
984
|
cfg.yes_to_all = False
|
|
954
985
|
cfg.interactive_permission_ask = True
|
|
955
986
|
object.__setattr__(cfg, "_web_interactive_hitl", True)
|
|
987
|
+
|
|
988
|
+
if has_perms:
|
|
989
|
+
_apply_web_permissions(cfg, req)
|
|
990
|
+
else:
|
|
991
|
+
cfg.super_mode = False
|
|
992
|
+
cfg.yes_to_all = False
|
|
993
|
+
cfg.interactive_permission_ask = True
|
|
994
|
+
object.__setattr__(cfg, "_web_interactive_hitl", True)
|
|
995
|
+
|
|
956
996
|
_ensure_web_hitl(cfg)
|
|
957
997
|
if os.environ.get("GEMCODE_WEB_YES_TO_ALL", "").lower() in ("1", "true", "yes", "on"):
|
|
958
998
|
cfg.yes_to_all = True
|
|
@@ -961,8 +1001,9 @@ def _configure_web_permissions(cfg: GemCodeConfig, req: dict[str, Any]) -> None:
|
|
|
961
1001
|
|
|
962
1002
|
|
|
963
1003
|
def _ensure_web_hitl(cfg: GemCodeConfig) -> None:
|
|
964
|
-
"""Web chat
|
|
1004
|
+
"""Web chat uses the UI approval bridge when tools are not fully auto-approved."""
|
|
965
1005
|
if getattr(cfg, "yes_to_all", False) or getattr(cfg, "super_mode", False):
|
|
1006
|
+
object.__setattr__(cfg, "_web_interactive_hitl", False)
|
|
966
1007
|
return
|
|
967
1008
|
cfg.interactive_permission_ask = True
|
|
968
1009
|
object.__setattr__(cfg, "_web_interactive_hitl", True)
|
|
@@ -1246,6 +1287,10 @@ async def run_adapter(req: dict[str, Any]) -> None:
|
|
|
1246
1287
|
"model": model,
|
|
1247
1288
|
"project_root": str(cfg.project_root),
|
|
1248
1289
|
"workspace_mode": workspace_mode,
|
|
1290
|
+
"hitl_interactive": bool(getattr(cfg, "_web_interactive_hitl", False)),
|
|
1291
|
+
"auto_approve": bool(
|
|
1292
|
+
getattr(cfg, "yes_to_all", False) or getattr(cfg, "super_mode", False)
|
|
1293
|
+
),
|
|
1249
1294
|
**({"skill": resolved.skill_name} if resolved.skill_name else {}),
|
|
1250
1295
|
}
|
|
1251
1296
|
)
|
|
@@ -187,6 +187,7 @@ tests/test_multimodal_input.py
|
|
|
187
187
|
tests/test_output_styles_and_rules.py
|
|
188
188
|
tests/test_paths.py
|
|
189
189
|
tests/test_permissions.py
|
|
190
|
+
tests/test_preview_proxy.py
|
|
190
191
|
tests/test_prompt_suggestions.py
|
|
191
192
|
tests/test_repl_commands.py
|
|
192
193
|
tests/test_repl_slash.py
|
|
@@ -200,6 +201,7 @@ tests/test_token_budget.py
|
|
|
200
201
|
tests/test_tool_context_circulation.py
|
|
201
202
|
tests/test_tools.py
|
|
202
203
|
tests/test_tools_inspector.py
|
|
204
|
+
tests/test_web_permissions_hitl.py
|
|
203
205
|
tests/test_web_server.py
|
|
204
206
|
tests/test_web_sse_adapter.py
|
|
205
207
|
tests/test_workspace_hints.py
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Tests for preview proxy path parsing."""
|
|
2
|
+
|
|
3
|
+
from gemcode.web.preview_api import parse_preview_proxy_path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_parse_preview_proxy_root() -> None:
|
|
7
|
+
assert parse_preview_proxy_path("/api/preview/proxy/8000") == (8000, "/")
|
|
8
|
+
assert parse_preview_proxy_path("/api/preview/proxy/8000/") == (8000, "/")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_parse_preview_proxy_subpath() -> None:
|
|
12
|
+
assert parse_preview_proxy_path("/api/preview/proxy/5173/assets/app.js") == (
|
|
13
|
+
5173,
|
|
14
|
+
"/assets/app.js",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_parse_preview_proxy_invalid() -> None:
|
|
19
|
+
assert parse_preview_proxy_path("/api/preview") is None
|
|
20
|
+
assert parse_preview_proxy_path("/api/preview/proxy/") is None
|
|
21
|
+
assert parse_preview_proxy_path("/api/preview/proxy/abc") is None
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Web UI permissions must override process-level GEMCODE_SUPER_MODE."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from gemcode.config import GemCodeConfig, apply_super_mode
|
|
8
|
+
from gemcode.web import sse_adapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_web_permissions_clear_env_super_mode(tmp_path: Path, monkeypatch) -> None:
|
|
12
|
+
monkeypatch.setenv("GEMCODE_SUPER_MODE", "1")
|
|
13
|
+
cfg = GemCodeConfig(project_root=tmp_path)
|
|
14
|
+
assert cfg.super_mode is True
|
|
15
|
+
apply_super_mode(cfg)
|
|
16
|
+
assert cfg.yes_to_all is True
|
|
17
|
+
|
|
18
|
+
sse_adapter._configure_web_permissions(
|
|
19
|
+
cfg,
|
|
20
|
+
{
|
|
21
|
+
"permissions": {
|
|
22
|
+
"super_mode": False,
|
|
23
|
+
"auto_approve": {
|
|
24
|
+
"bash": False,
|
|
25
|
+
"file_read": False,
|
|
26
|
+
"file_write": False,
|
|
27
|
+
"web_search": False,
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
assert cfg.super_mode is False
|
|
33
|
+
assert cfg.yes_to_all is False
|
|
34
|
+
assert getattr(cfg, "_web_interactive_hitl") is True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_web_permissions_ui_super_mode_on(tmp_path: Path, monkeypatch) -> None:
|
|
38
|
+
monkeypatch.delenv("GEMCODE_SUPER_MODE", raising=False)
|
|
39
|
+
cfg = GemCodeConfig(project_root=tmp_path)
|
|
40
|
+
cfg.super_mode = False
|
|
41
|
+
cfg.yes_to_all = False
|
|
42
|
+
|
|
43
|
+
sse_adapter._configure_web_permissions(
|
|
44
|
+
cfg,
|
|
45
|
+
{"permissions": {"super_mode": True, "auto_approve": {}}},
|
|
46
|
+
)
|
|
47
|
+
assert cfg.super_mode is True
|
|
48
|
+
assert cfg.yes_to_all is True
|
|
49
|
+
assert getattr(cfg, "_web_interactive_hitl") is False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_inject_web_context_auto_approve_copy(tmp_path: Path) -> None:
|
|
53
|
+
cfg = GemCodeConfig(project_root=tmp_path)
|
|
54
|
+
cfg.yes_to_all = True
|
|
55
|
+
object.__setattr__(cfg, "_web_workspace_mode", "code")
|
|
56
|
+
object.__setattr__(cfg, "_web_interactive_hitl", False)
|
|
57
|
+
out = sse_adapter._inject_web_code_context(cfg, "hello", {})
|
|
58
|
+
assert "AUTO-APPROVE is ON" in out
|
|
59
|
+
assert "Never mention Approve" in out
|
|
60
|
+
assert "inline Yes/No card" not in out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_inject_web_context_interactive_copy(tmp_path: Path) -> None:
|
|
64
|
+
cfg = GemCodeConfig(project_root=tmp_path)
|
|
65
|
+
cfg.super_mode = False
|
|
66
|
+
cfg.yes_to_all = False
|
|
67
|
+
object.__setattr__(cfg, "_web_workspace_mode", "code")
|
|
68
|
+
object.__setattr__(cfg, "_web_interactive_hitl", True)
|
|
69
|
+
out = sse_adapter._inject_web_code_context(cfg, "hello", {})
|
|
70
|
+
assert "inline Yes/No card" in out
|
|
71
|
+
assert "AUTO-APPROVE is ON" not in out
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"""Detect local dev servers for in-UI localhost preview."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import socket
|
|
6
|
-
from typing import Any
|
|
7
|
-
from urllib.error import URLError
|
|
8
|
-
from urllib.request import Request, urlopen
|
|
9
|
-
|
|
10
|
-
# Common dev-server ports (GemCode web UI uses 3002; API 3001)
|
|
11
|
-
COMMON_DEV_PORTS = (
|
|
12
|
-
3000,
|
|
13
|
-
3003,
|
|
14
|
-
3004,
|
|
15
|
-
5173,
|
|
16
|
-
5174,
|
|
17
|
-
8080,
|
|
18
|
-
8000,
|
|
19
|
-
4200,
|
|
20
|
-
5000,
|
|
21
|
-
4321,
|
|
22
|
-
8888,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _port_open(host: str, port: int, *, timeout: float = 0.2) -> bool:
|
|
27
|
-
try:
|
|
28
|
-
with socket.create_connection((host, port), timeout=timeout):
|
|
29
|
-
return True
|
|
30
|
-
except OSError:
|
|
31
|
-
return False
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _probe_http(url: str, *, timeout: float = 1.5) -> dict[str, Any] | None:
|
|
35
|
-
try:
|
|
36
|
-
req = Request(url, headers={"User-Agent": "GemCode-Web-Preview/1.0"})
|
|
37
|
-
with urlopen(req, timeout=timeout) as resp:
|
|
38
|
-
ctype = (resp.headers.get("Content-Type") or "").lower()
|
|
39
|
-
return {
|
|
40
|
-
"ok": True,
|
|
41
|
-
"status": resp.status,
|
|
42
|
-
"content_type": ctype,
|
|
43
|
-
}
|
|
44
|
-
except (URLError, OSError, TimeoutError, ValueError):
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def scan_local_preview_ports(
|
|
49
|
-
*,
|
|
50
|
-
host: str = "127.0.0.1",
|
|
51
|
-
extra_ports: list[int] | None = None,
|
|
52
|
-
) -> list[dict[str, Any]]:
|
|
53
|
-
"""Return listening HTTP-ish ports on localhost."""
|
|
54
|
-
ports: list[int] = list(COMMON_DEV_PORTS)
|
|
55
|
-
if extra_ports:
|
|
56
|
-
for p in extra_ports:
|
|
57
|
-
if isinstance(p, int) and 1 < p < 65536 and p not in ports:
|
|
58
|
-
ports.append(p)
|
|
59
|
-
|
|
60
|
-
found: list[dict[str, Any]] = []
|
|
61
|
-
for port in ports:
|
|
62
|
-
if not _port_open(host, port):
|
|
63
|
-
continue
|
|
64
|
-
url = f"http://{host}:{port}"
|
|
65
|
-
probe = _probe_http(url)
|
|
66
|
-
found.append(
|
|
67
|
-
{
|
|
68
|
-
"port": port,
|
|
69
|
-
"url": url,
|
|
70
|
-
"reachable": probe is not None,
|
|
71
|
-
"content_type": (probe or {}).get("content_type"),
|
|
72
|
-
}
|
|
73
|
-
)
|
|
74
|
-
return found
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def handle_preview_get(kind: str) -> tuple[int, dict[str, Any]]:
|
|
78
|
-
k = (kind or "ports").strip().lower()
|
|
79
|
-
if k == "ports":
|
|
80
|
-
ports = scan_local_preview_ports()
|
|
81
|
-
return 200, {"ok": True, "ports": ports, "host": "127.0.0.1"}
|
|
82
|
-
return 400, {"ok": False, "error": f"Unknown preview kind: {kind}"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|