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.
Files changed (199) hide show
  1. {gemcode-0.4.19/src/gemcode.egg-info → gemcode-0.4.20}/PKG-INFO +1 -1
  2. {gemcode-0.4.19 → gemcode-0.4.20}/pyproject.toml +1 -1
  3. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/repl_slash.py +21 -4
  4. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/hitl_bridge.py +14 -2
  5. gemcode-0.4.20/src/gemcode/web/serve_bind.py +106 -0
  6. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/serve_state.py +36 -2
  7. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/server.py +75 -7
  8. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/sessions_api.py +12 -5
  9. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/sse_adapter.py +40 -7
  10. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/terminal_api.py +21 -10
  11. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/workspace_panel_api.py +6 -0
  12. {gemcode-0.4.19 → gemcode-0.4.20/src/gemcode.egg-info}/PKG-INFO +1 -1
  13. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode.egg-info/SOURCES.txt +1 -0
  14. gemcode-0.4.20/tests/test_web_server.py +69 -0
  15. gemcode-0.4.19/tests/test_web_server.py +0 -30
  16. {gemcode-0.4.19 → gemcode-0.4.20}/LICENSE +0 -0
  17. {gemcode-0.4.19 → gemcode-0.4.20}/MANIFEST.in +0 -0
  18. {gemcode-0.4.19 → gemcode-0.4.20}/README.md +0 -0
  19. {gemcode-0.4.19 → gemcode-0.4.20}/setup.cfg +0 -0
  20. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/__init__.py +0 -0
  21. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/__main__.py +0 -0
  22. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/a2a_bridge.py +0 -0
  23. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/agent.py +0 -0
  24. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/agent_habits.py +0 -0
  25. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/agent_intelligence.py +0 -0
  26. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/agent_mesh.py +0 -0
  27. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/agent_triggers.py +0 -0
  28. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/audit.py +0 -0
  29. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/autocompact.py +0 -0
  30. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/automations.py +0 -0
  31. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/autotune.py +0 -0
  32. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/callbacks.py +0 -0
  33. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/capability_routing.py +0 -0
  34. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/checkpoints.py +0 -0
  35. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/cli.py +0 -0
  36. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/codebase_awareness.py +0 -0
  37. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/compaction.py +0 -0
  38. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/computer_use/__init__.py +0 -0
  39. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/computer_use/browser_computer.py +0 -0
  40. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/config.py +0 -0
  41. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/context_budget.py +0 -0
  42. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/context_warning.py +0 -0
  43. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/credentials.py +0 -0
  44. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/curated_memory.py +0 -0
  45. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/delegation_learning.py +0 -0
  46. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/dynamic_policy.py +0 -0
  47. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/evals/harness.py +0 -0
  48. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/event_bus.py +0 -0
  49. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/fleet_reports.py +0 -0
  50. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/hitl_session.py +0 -0
  51. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/hooks.py +0 -0
  52. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/ide_protocol.py +0 -0
  53. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/ide_stdio.py +0 -0
  54. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/intent_classifier.py +0 -0
  55. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/interactions.py +0 -0
  56. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/invoke.py +0 -0
  57. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/kaira_client.py +0 -0
  58. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/kaira_daemon.py +0 -0
  59. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/kaira_ipc.py +0 -0
  60. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/kaira_job_store.py +0 -0
  61. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/learning.py +0 -0
  62. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/limits.py +0 -0
  63. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/live_audio_engine.py +0 -0
  64. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/logging_config.py +0 -0
  65. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/mcp_loader.py +0 -0
  66. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/memory/__init__.py +0 -0
  67. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/memory/embedding_memory_service.py +0 -0
  68. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/memory/file_memory_service.py +0 -0
  69. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/modality_tools.py +0 -0
  70. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/model_errors.py +0 -0
  71. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/model_routing.py +0 -0
  72. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/multimodal_input.py +0 -0
  73. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/openapi_loader.py +0 -0
  74. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/org.py +0 -0
  75. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/output_styles.py +0 -0
  76. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/paths.py +0 -0
  77. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/permissions.py +0 -0
  78. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/plugins/__init__.py +0 -0
  79. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  80. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  81. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/policy_profile.py +0 -0
  82. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/pricing.py +0 -0
  83. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/prompt_suggestions.py +0 -0
  84. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/__init__.py +0 -0
  85. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/config.py +0 -0
  86. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/deps.py +0 -0
  87. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/engine.py +0 -0
  88. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/stop_hooks.py +0 -0
  89. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/token_budget.py +0 -0
  90. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query/transitions.py +0 -0
  91. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/query_sanitizer.py +0 -0
  92. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/refine.py +0 -0
  93. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/repl_commands.py +0 -0
  94. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/review_agent.py +0 -0
  95. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/rules.py +0 -0
  96. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/self_healing.py +0 -0
  97. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/session_runtime.py +0 -0
  98. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/session_store.py +0 -0
  99. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/session_summariser.py +0 -0
  100. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/skills.py +0 -0
  101. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/slash_commands.py +0 -0
  102. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/thinking.py +0 -0
  103. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tool_prompt_manifest.py +0 -0
  104. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tool_registry.py +0 -0
  105. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tool_result_store.py +0 -0
  106. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tool_synthesis.py +0 -0
  107. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/__init__.py +0 -0
  108. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/automations_tools.py +0 -0
  109. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/bash.py +0 -0
  110. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/browser.py +0 -0
  111. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/compress_memory.py +0 -0
  112. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/curated_memory.py +0 -0
  113. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/edit.py +0 -0
  114. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/filesystem.py +0 -0
  115. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/notebook.py +0 -0
  116. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/notes.py +0 -0
  117. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/org_tools.py +0 -0
  118. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/repo_map.py +0 -0
  119. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/search.py +0 -0
  120. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/shell.py +0 -0
  121. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/shell_gate.py +0 -0
  122. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/skills.py +0 -0
  123. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/subtask.py +0 -0
  124. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/tasks.py +0 -0
  125. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/think.py +0 -0
  126. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/todo.py +0 -0
  127. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/user_choice.py +0 -0
  128. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/veomem_tools.py +0 -0
  129. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/web.py +0 -0
  130. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools/web_search.py +0 -0
  131. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tools_inspector.py +0 -0
  132. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/trust.py +0 -0
  133. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tui/input_handler.py +0 -0
  134. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tui/scrollback.py +0 -0
  135. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tui/spinner.py +0 -0
  136. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tui/welcome_banner.py +0 -0
  137. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/tui/welcome_rich.py +0 -0
  138. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/veomem_bridge.py +0 -0
  139. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/version.py +0 -0
  140. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/vertex.py +0 -0
  141. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/wal.py +0 -0
  142. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/__init__.py +0 -0
  143. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/chat_skills.py +0 -0
  144. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/customize_api.py +0 -0
  145. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/fleet_api.py +0 -0
  146. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/preview_api.py +0 -0
  147. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/project_root.py +0 -0
  148. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/runtime_api.py +0 -0
  149. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/terminal_repl.py +0 -0
  150. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/web_config_api.py +0 -0
  151. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/web/web_sse_compat.py +0 -0
  152. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode/workspace_hints.py +0 -0
  153. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode.egg-info/dependency_links.txt +0 -0
  154. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode.egg-info/entry_points.txt +0 -0
  155. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode.egg-info/requires.txt +0 -0
  156. {gemcode-0.4.19 → gemcode-0.4.20}/src/gemcode.egg-info/top_level.txt +0 -0
  157. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_add_dir.py +0 -0
  158. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_agent_habits.py +0 -0
  159. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_agent_instruction.py +0 -0
  160. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_agent_mesh.py +0 -0
  161. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_autocompact.py +0 -0
  162. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_automations.py +0 -0
  163. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_capability_routing.py +0 -0
  164. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_checkpoint_diff_command.py +0 -0
  165. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_cli_init.py +0 -0
  166. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_compress_memory_tool.py +0 -0
  167. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_computer_use_permissions.py +0 -0
  168. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_context_budget.py +0 -0
  169. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_context_warning.py +0 -0
  170. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_credentials.py +0 -0
  171. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_eval_harness_layout.py +0 -0
  172. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_event_bus.py +0 -0
  173. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_fleet_reports.py +0 -0
  174. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_ide_stdio_attachments.py +0 -0
  175. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_interactive_permission_ask.py +0 -0
  176. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_kaira_ipc_paths.py +0 -0
  177. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_kaira_scheduler.py +0 -0
  178. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_modality_tools.py +0 -0
  179. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_model_error_retry.py +0 -0
  180. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_model_errors.py +0 -0
  181. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_model_routing.py +0 -0
  182. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_multimodal_input.py +0 -0
  183. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_output_styles_and_rules.py +0 -0
  184. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_paths.py +0 -0
  185. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_permissions.py +0 -0
  186. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_prompt_suggestions.py +0 -0
  187. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_repl_commands.py +0 -0
  188. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_repl_slash.py +0 -0
  189. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_session_runtime_cache.py +0 -0
  190. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_skills.py +0 -0
  191. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_slash_commands.py +0 -0
  192. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_slash_completion_registry.py +0 -0
  193. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_thinking_config.py +0 -0
  194. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_token_budget.py +0 -0
  195. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_tool_context_circulation.py +0 -0
  196. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_tools.py +0 -0
  197. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_tools_inspector.py +0 -0
  198. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_web_sse_adapter.py +0 -0
  199. {gemcode-0.4.19 → gemcode-0.4.20}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.19
3
+ Version: 0.4.20
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.4.19"
7
+ version = "0.4.20"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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 running and info:
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
- out(f"[serve] failed: {result.get('error') or 'unknown error'}")
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
- out(f"[serve] started at {result.get('url')}")
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("Open the web UI (npm run dev) it proxies to port 3001 by default.")
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 = 300.0
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
- return {"ok": True, "started": True, **payload, "health": health}
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
- return {"ok": True, "started": True, **payload}
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
- from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
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
- chunk = proc.stdout.read(4096)
450
- if not chunk:
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
- proc.wait(timeout=900)
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
- httpd = ThreadingHTTPServer((host, bind_port), handler)
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
- print("Connect the web UI to this URL (Settings → API URL or NEXT_PUBLIC_API_URL).", flush=True)
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 == "touch":
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 workspace_mode != "chat":
993
- if "enable_deep_research" in caps:
994
- cfg.enable_deep_research = bool(caps.get("enable_deep_research"))
995
- if "enable_computer_use" in caps:
996
- cfg.enable_computer_use = bool(caps.get("enable_computer_use"))
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 = os.environ.get("GEMCODE_WEB_YES_TO_ALL", "true").lower() in (
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(root, prompt=prompt, session_id=session_id, user_id=user_id)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.19
3
+ Version: 0.4.20
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License