gemcode 0.4.17__tar.gz → 0.4.18__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 (183) hide show
  1. {gemcode-0.4.17/src/gemcode.egg-info → gemcode-0.4.18}/PKG-INFO +1 -1
  2. {gemcode-0.4.17 → gemcode-0.4.18}/pyproject.toml +1 -1
  3. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/bash.py +25 -1
  4. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/shell.py +41 -1
  5. {gemcode-0.4.17 → gemcode-0.4.18/src/gemcode.egg-info}/PKG-INFO +1 -1
  6. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_tools.py +19 -12
  7. {gemcode-0.4.17 → gemcode-0.4.18}/LICENSE +0 -0
  8. {gemcode-0.4.17 → gemcode-0.4.18}/MANIFEST.in +0 -0
  9. {gemcode-0.4.17 → gemcode-0.4.18}/README.md +0 -0
  10. {gemcode-0.4.17 → gemcode-0.4.18}/setup.cfg +0 -0
  11. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/__init__.py +0 -0
  12. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/__main__.py +0 -0
  13. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/a2a_bridge.py +0 -0
  14. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/agent.py +0 -0
  15. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/agent_habits.py +0 -0
  16. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/agent_intelligence.py +0 -0
  17. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/agent_mesh.py +0 -0
  18. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/agent_triggers.py +0 -0
  19. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/audit.py +0 -0
  20. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/autocompact.py +0 -0
  21. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/automations.py +0 -0
  22. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/autotune.py +0 -0
  23. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/callbacks.py +0 -0
  24. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/capability_routing.py +0 -0
  25. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/checkpoints.py +0 -0
  26. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/cli.py +0 -0
  27. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/codebase_awareness.py +0 -0
  28. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/compaction.py +0 -0
  29. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/computer_use/__init__.py +0 -0
  30. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/computer_use/browser_computer.py +0 -0
  31. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/config.py +0 -0
  32. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/context_budget.py +0 -0
  33. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/context_warning.py +0 -0
  34. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/credentials.py +0 -0
  35. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/curated_memory.py +0 -0
  36. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/delegation_learning.py +0 -0
  37. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/dynamic_policy.py +0 -0
  38. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/evals/harness.py +0 -0
  39. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/event_bus.py +0 -0
  40. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/fleet_reports.py +0 -0
  41. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/hitl_session.py +0 -0
  42. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/hooks.py +0 -0
  43. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/ide_protocol.py +0 -0
  44. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/ide_stdio.py +0 -0
  45. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/intent_classifier.py +0 -0
  46. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/interactions.py +0 -0
  47. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/invoke.py +0 -0
  48. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/kaira_client.py +0 -0
  49. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/kaira_daemon.py +0 -0
  50. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/kaira_ipc.py +0 -0
  51. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/kaira_job_store.py +0 -0
  52. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/learning.py +0 -0
  53. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/limits.py +0 -0
  54. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/live_audio_engine.py +0 -0
  55. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/logging_config.py +0 -0
  56. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/mcp_loader.py +0 -0
  57. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/memory/__init__.py +0 -0
  58. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/memory/embedding_memory_service.py +0 -0
  59. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/memory/file_memory_service.py +0 -0
  60. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/modality_tools.py +0 -0
  61. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/model_errors.py +0 -0
  62. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/model_routing.py +0 -0
  63. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/multimodal_input.py +0 -0
  64. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/openapi_loader.py +0 -0
  65. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/org.py +0 -0
  66. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/output_styles.py +0 -0
  67. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/paths.py +0 -0
  68. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/permissions.py +0 -0
  69. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/plugins/__init__.py +0 -0
  70. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  71. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  72. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/policy_profile.py +0 -0
  73. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/pricing.py +0 -0
  74. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/prompt_suggestions.py +0 -0
  75. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/query/__init__.py +0 -0
  76. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/query/config.py +0 -0
  77. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/query/deps.py +0 -0
  78. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/query/engine.py +0 -0
  79. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/query/stop_hooks.py +0 -0
  80. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/query/token_budget.py +0 -0
  81. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/query/transitions.py +0 -0
  82. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/query_sanitizer.py +0 -0
  83. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/refine.py +0 -0
  84. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/repl_commands.py +0 -0
  85. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/repl_slash.py +0 -0
  86. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/review_agent.py +0 -0
  87. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/rules.py +0 -0
  88. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/self_healing.py +0 -0
  89. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/session_runtime.py +0 -0
  90. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/session_store.py +0 -0
  91. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/session_summariser.py +0 -0
  92. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/skills.py +0 -0
  93. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/slash_commands.py +0 -0
  94. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/thinking.py +0 -0
  95. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tool_prompt_manifest.py +0 -0
  96. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tool_registry.py +0 -0
  97. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tool_result_store.py +0 -0
  98. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tool_synthesis.py +0 -0
  99. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/__init__.py +0 -0
  100. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/automations_tools.py +0 -0
  101. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/browser.py +0 -0
  102. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/compress_memory.py +0 -0
  103. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/curated_memory.py +0 -0
  104. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/edit.py +0 -0
  105. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/filesystem.py +0 -0
  106. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/notebook.py +0 -0
  107. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/notes.py +0 -0
  108. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/org_tools.py +0 -0
  109. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/repo_map.py +0 -0
  110. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/search.py +0 -0
  111. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/shell_gate.py +0 -0
  112. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/skills.py +0 -0
  113. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/subtask.py +0 -0
  114. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/tasks.py +0 -0
  115. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/think.py +0 -0
  116. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/todo.py +0 -0
  117. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/user_choice.py +0 -0
  118. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/veomem_tools.py +0 -0
  119. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/web.py +0 -0
  120. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools/web_search.py +0 -0
  121. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tools_inspector.py +0 -0
  122. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/trust.py +0 -0
  123. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tui/input_handler.py +0 -0
  124. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tui/scrollback.py +0 -0
  125. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tui/spinner.py +0 -0
  126. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tui/welcome_banner.py +0 -0
  127. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/tui/welcome_rich.py +0 -0
  128. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/veomem_bridge.py +0 -0
  129. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/version.py +0 -0
  130. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/vertex.py +0 -0
  131. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/wal.py +0 -0
  132. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/web/__init__.py +0 -0
  133. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/web/sse_adapter.py +0 -0
  134. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/web/terminal_repl.py +0 -0
  135. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/web/web_sse_compat.py +0 -0
  136. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode/workspace_hints.py +0 -0
  137. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode.egg-info/SOURCES.txt +0 -0
  138. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode.egg-info/dependency_links.txt +0 -0
  139. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode.egg-info/entry_points.txt +0 -0
  140. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode.egg-info/requires.txt +0 -0
  141. {gemcode-0.4.17 → gemcode-0.4.18}/src/gemcode.egg-info/top_level.txt +0 -0
  142. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_add_dir.py +0 -0
  143. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_agent_habits.py +0 -0
  144. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_agent_instruction.py +0 -0
  145. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_agent_mesh.py +0 -0
  146. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_autocompact.py +0 -0
  147. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_automations.py +0 -0
  148. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_capability_routing.py +0 -0
  149. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_checkpoint_diff_command.py +0 -0
  150. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_cli_init.py +0 -0
  151. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_compress_memory_tool.py +0 -0
  152. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_computer_use_permissions.py +0 -0
  153. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_context_budget.py +0 -0
  154. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_context_warning.py +0 -0
  155. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_credentials.py +0 -0
  156. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_eval_harness_layout.py +0 -0
  157. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_event_bus.py +0 -0
  158. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_fleet_reports.py +0 -0
  159. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_ide_stdio_attachments.py +0 -0
  160. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_interactive_permission_ask.py +0 -0
  161. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_kaira_ipc_paths.py +0 -0
  162. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_kaira_scheduler.py +0 -0
  163. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_modality_tools.py +0 -0
  164. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_model_error_retry.py +0 -0
  165. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_model_errors.py +0 -0
  166. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_model_routing.py +0 -0
  167. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_multimodal_input.py +0 -0
  168. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_output_styles_and_rules.py +0 -0
  169. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_paths.py +0 -0
  170. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_permissions.py +0 -0
  171. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_prompt_suggestions.py +0 -0
  172. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_repl_commands.py +0 -0
  173. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_repl_slash.py +0 -0
  174. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_session_runtime_cache.py +0 -0
  175. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_skills.py +0 -0
  176. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_slash_commands.py +0 -0
  177. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_slash_completion_registry.py +0 -0
  178. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_thinking_config.py +0 -0
  179. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_token_budget.py +0 -0
  180. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_tool_context_circulation.py +0 -0
  181. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_tools_inspector.py +0 -0
  182. {gemcode-0.4.17 → gemcode-0.4.18}/tests/test_web_sse_adapter.py +0 -0
  183. {gemcode-0.4.17 → gemcode-0.4.18}/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.17
3
+ Version: 0.4.18
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.17"
7
+ version = "0.4.18"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -38,7 +38,7 @@ def make_bash_tool(cfg: GemCodeConfig):
38
38
  except Exception:
39
39
  pass
40
40
 
41
- def bash(
41
+ def _bash_sync(
42
42
  command: str,
43
43
  timeout_seconds: int = 120,
44
44
  cwd_subdir: str = ".",
@@ -232,6 +232,30 @@ def make_bash_tool(cfg: GemCodeConfig):
232
232
  except subprocess.TimeoutExpired:
233
233
  return {"error": f"Timeout after {timeout_seconds}s", "command": command}
234
234
 
235
+ async def bash(
236
+ command: str,
237
+ timeout_seconds: int = 120,
238
+ cwd_subdir: str = ".",
239
+ background: bool = False,
240
+ ) -> dict:
241
+ """
242
+ Async wrapper for bash execution.
243
+
244
+ Why: synchronous subprocess calls block the TUI event loop, freezing the
245
+ live spinner timers ("Running…", "Querying…"). We run blocking shell work
246
+ on a worker thread so the UI keeps updating.
247
+ """
248
+ if background:
249
+ # Background start is quick; keep it synchronous for simplicity.
250
+ return _bash_sync(command, timeout_seconds=timeout_seconds, cwd_subdir=cwd_subdir, background=True)
251
+ return await asyncio.to_thread(
252
+ _bash_sync,
253
+ command,
254
+ timeout_seconds,
255
+ cwd_subdir,
256
+ False,
257
+ )
258
+
235
259
  return bash
236
260
 
237
261
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import os
6
7
  import re
7
8
  import shutil
@@ -45,7 +46,7 @@ def make_run_command(cfg: GemCodeConfig):
45
46
  except Exception:
46
47
  pass
47
48
 
48
- def run_command(
49
+ def _run_command_sync(
49
50
  command: str,
50
51
  args: list[str] | None = None,
51
52
  timeout_seconds: int = 120,
@@ -200,4 +201,43 @@ def make_run_command(cfg: GemCodeConfig):
200
201
  except subprocess.TimeoutExpired:
201
202
  return {"error": f"Timeout after {timeout_seconds}s"}
202
203
 
204
+ async def run_command(
205
+ command: str,
206
+ args: list[str] | None = None,
207
+ timeout_seconds: int = 120,
208
+ tool_context: ToolContext | None = None,
209
+ cwd_subdir: str = ".",
210
+ background: bool = False,
211
+ extra_env_keys: list[str] | None = None,
212
+ extra_env_values: list[str] | None = None,
213
+ ) -> dict:
214
+ """
215
+ Async wrapper for allowlisted subprocess execution.
216
+
217
+ Why: synchronous subprocess calls block the TUI event loop, freezing the live
218
+ spinner timers while tools run. We offload blocking work to a thread.
219
+ """
220
+ if background:
221
+ return _run_command_sync(
222
+ command,
223
+ args=args,
224
+ timeout_seconds=timeout_seconds,
225
+ tool_context=tool_context,
226
+ cwd_subdir=cwd_subdir,
227
+ background=True,
228
+ extra_env_keys=extra_env_keys,
229
+ extra_env_values=extra_env_values,
230
+ )
231
+ return await asyncio.to_thread(
232
+ _run_command_sync,
233
+ command,
234
+ args,
235
+ timeout_seconds,
236
+ tool_context,
237
+ cwd_subdir,
238
+ False,
239
+ extra_env_keys,
240
+ extra_env_values,
241
+ )
242
+
203
243
  return run_command
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.17
3
+ Version: 0.4.18
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -2,6 +2,8 @@ from pathlib import Path
2
2
  import sys
3
3
  from unittest.mock import MagicMock
4
4
 
5
+ import pytest
6
+
5
7
  from gemcode.hitl_session import HITL_STICKY_SESSION_KEY
6
8
 
7
9
  from gemcode.config import GemCodeConfig
@@ -43,7 +45,8 @@ def test_read_file(tmp_path: Path, monkeypatch) -> None:
43
45
  assert out["content"] == "hello"
44
46
 
45
47
 
46
- def test_run_command_allowlist_bypass_after_shell_gate(tmp_path: Path, monkeypatch) -> None:
48
+ @pytest.mark.asyncio
49
+ async def test_run_command_allowlist_bypass_after_shell_gate(tmp_path: Path, monkeypatch) -> None:
47
50
  """Interactive approval arms one shot: rm works without being on GEMCODE_ALLOW_COMMANDS."""
48
51
  monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
49
52
  trust_root(tmp_path, trusted=True)
@@ -53,22 +56,23 @@ def test_run_command_allowlist_bypass_after_shell_gate(tmp_path: Path, monkeypat
53
56
  tgt.write_text("hi", encoding="utf-8")
54
57
  arm_confirmed_shell_basename("rm")
55
58
  run_command = make_run_command(cfg)
56
- out = run_command("rm", ["x.txt"])
59
+ out = await run_command("rm", ["x.txt"])
57
60
  assert out.get("exit_code") == 0
58
61
  assert not tgt.exists()
59
62
  # Gate is not consumed when a different executable runs first (still non-allowlisted).
60
63
  arm_confirmed_shell_basename("rm")
61
64
  assert "uname" not in cfg.allow_commands
62
- wrong = run_command("uname", ["-a"])
65
+ wrong = await run_command("uname", ["-a"])
63
66
  assert "not in allowlist" in str(wrong.get("error", ""))
64
67
  tgt2 = tmp_path / "y.txt"
65
68
  tgt2.write_text("z", encoding="utf-8")
66
- ok2 = run_command("rm", ["y.txt"])
69
+ ok2 = await run_command("rm", ["y.txt"])
67
70
  assert ok2.get("exit_code") == 0
68
71
  assert not tgt2.exists()
69
72
 
70
73
 
71
- def test_run_command_sticky_session_bypasses_allowlist(tmp_path: Path, monkeypatch) -> None:
74
+ @pytest.mark.asyncio
75
+ async def test_run_command_sticky_session_bypasses_allowlist(tmp_path: Path, monkeypatch) -> None:
72
76
  monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
73
77
  trust_root(tmp_path, trusted=True)
74
78
  cfg = GemCodeConfig(project_root=tmp_path)
@@ -76,12 +80,13 @@ def test_run_command_sticky_session_bypasses_allowlist(tmp_path: Path, monkeypat
76
80
  run_command = make_run_command(cfg)
77
81
  ctx = MagicMock()
78
82
  ctx.state = {HITL_STICKY_SESSION_KEY: True}
79
- out = run_command("uname", ["-a"], tool_context=ctx)
83
+ out = await run_command("uname", ["-a"], tool_context=ctx)
80
84
  assert out.get("exit_code") == 0
81
85
  assert (out.get("stdout") or out.get("stderr") or "").strip()
82
86
 
83
87
 
84
- def test_run_command_cwd_subdir(tmp_path: Path, monkeypatch) -> None:
88
+ @pytest.mark.asyncio
89
+ async def test_run_command_cwd_subdir(tmp_path: Path, monkeypatch) -> None:
85
90
  monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
86
91
  trust_root(tmp_path, trusted=True)
87
92
  cfg = GemCodeConfig(project_root=tmp_path)
@@ -91,7 +96,7 @@ def test_run_command_cwd_subdir(tmp_path: Path, monkeypatch) -> None:
91
96
  run_command = make_run_command(cfg)
92
97
  ctx = MagicMock()
93
98
  ctx.state = {HITL_STICKY_SESSION_KEY: True}
94
- out = run_command(
99
+ out = await run_command(
95
100
  "python3",
96
101
  ["-c", "print(open('marker.txt').read())"],
97
102
  cwd_subdir="nest",
@@ -101,14 +106,15 @@ def test_run_command_cwd_subdir(tmp_path: Path, monkeypatch) -> None:
101
106
  assert "in-nest" in (out.get("stdout") or "")
102
107
 
103
108
 
104
- def test_run_command_background_returns_pid(tmp_path: Path, monkeypatch) -> None:
109
+ @pytest.mark.asyncio
110
+ async def test_run_command_background_returns_pid(tmp_path: Path, monkeypatch) -> None:
105
111
  monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
106
112
  trust_root(tmp_path, trusted=True)
107
113
  cfg = GemCodeConfig(project_root=tmp_path)
108
114
  run_command = make_run_command(cfg)
109
115
  ctx = MagicMock()
110
116
  ctx.state = {HITL_STICKY_SESSION_KEY: True}
111
- out = run_command(
117
+ out = await run_command(
112
118
  "python3",
113
119
  ["-c", "print(1)"],
114
120
  background=True,
@@ -118,14 +124,15 @@ def test_run_command_background_returns_pid(tmp_path: Path, monkeypatch) -> None
118
124
  assert isinstance(out.get("pid"), int)
119
125
 
120
126
 
121
- def test_run_command_extra_env_merges(tmp_path: Path, monkeypatch) -> None:
127
+ @pytest.mark.asyncio
128
+ async def test_run_command_extra_env_merges(tmp_path: Path, monkeypatch) -> None:
122
129
  monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
123
130
  trust_root(tmp_path, trusted=True)
124
131
  cfg = GemCodeConfig(project_root=tmp_path)
125
132
  run_command = make_run_command(cfg)
126
133
  ctx = MagicMock()
127
134
  ctx.state = {HITL_STICKY_SESSION_KEY: True}
128
- out = run_command(
135
+ out = await run_command(
129
136
  "python3",
130
137
  ["-c", "import os; print(os.environ.get('GEMCODE_TEST_EXTRA', ''))"],
131
138
  extra_env_keys=["GEMCODE_TEST_EXTRA"],
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