gemcode 0.3.19__tar.gz → 0.3.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 (112) hide show
  1. {gemcode-0.3.19/src/gemcode.egg-info → gemcode-0.3.20}/PKG-INFO +1 -1
  2. {gemcode-0.3.19 → gemcode-0.3.20}/pyproject.toml +1 -1
  3. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/repl_commands.py +5 -0
  4. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/repl_slash.py +90 -0
  5. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/thinking.py +21 -11
  6. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tui/input_handler.py +1 -0
  7. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tui/scrollback.py +82 -29
  8. {gemcode-0.3.19 → gemcode-0.3.20/src/gemcode.egg-info}/PKG-INFO +1 -1
  9. {gemcode-0.3.19 → gemcode-0.3.20}/LICENSE +0 -0
  10. {gemcode-0.3.19 → gemcode-0.3.20}/MANIFEST.in +0 -0
  11. {gemcode-0.3.19 → gemcode-0.3.20}/README.md +0 -0
  12. {gemcode-0.3.19 → gemcode-0.3.20}/setup.cfg +0 -0
  13. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/__init__.py +0 -0
  14. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/__main__.py +0 -0
  15. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/agent.py +0 -0
  16. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/audit.py +0 -0
  17. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/autocompact.py +0 -0
  18. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/callbacks.py +0 -0
  19. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/capability_routing.py +0 -0
  20. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/cli.py +0 -0
  21. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/compaction.py +0 -0
  22. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/computer_use/__init__.py +0 -0
  23. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/computer_use/browser_computer.py +0 -0
  24. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/config.py +0 -0
  25. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/context_budget.py +0 -0
  26. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/context_warning.py +0 -0
  27. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/credentials.py +0 -0
  28. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/hitl_session.py +0 -0
  29. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/interactions.py +0 -0
  30. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/invoke.py +0 -0
  31. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/kairos_daemon.py +0 -0
  32. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/limits.py +0 -0
  33. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/live_audio_engine.py +0 -0
  34. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/logging_config.py +0 -0
  35. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/mcp_loader.py +0 -0
  36. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/memory/__init__.py +0 -0
  37. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/memory/embedding_memory_service.py +0 -0
  38. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/memory/file_memory_service.py +0 -0
  39. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/modality_tools.py +0 -0
  40. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/model_errors.py +0 -0
  41. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/model_routing.py +0 -0
  42. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/paths.py +0 -0
  43. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/permissions.py +0 -0
  44. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/plugins/__init__.py +0 -0
  45. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  46. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  47. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/prompt_suggestions.py +0 -0
  48. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/__init__.py +0 -0
  49. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/config.py +0 -0
  50. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/deps.py +0 -0
  51. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/engine.py +0 -0
  52. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/stop_hooks.py +0 -0
  53. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/token_budget.py +0 -0
  54. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/transitions.py +0 -0
  55. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/session_runtime.py +0 -0
  56. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/slash_commands.py +0 -0
  57. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tool_prompt_manifest.py +0 -0
  58. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tool_registry.py +0 -0
  59. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/__init__.py +0 -0
  60. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/bash.py +0 -0
  61. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/edit.py +0 -0
  62. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/filesystem.py +0 -0
  63. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/search.py +0 -0
  64. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/shell.py +0 -0
  65. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/shell_gate.py +0 -0
  66. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/subtask.py +0 -0
  67. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/think.py +0 -0
  68. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/todo.py +0 -0
  69. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/web.py +0 -0
  70. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools_inspector.py +0 -0
  71. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/trust.py +0 -0
  72. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tui/spinner.py +0 -0
  73. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tui/welcome_banner.py +0 -0
  74. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tui/welcome_rich.py +0 -0
  75. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/version.py +0 -0
  76. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/vertex.py +0 -0
  77. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/web/__init__.py +0 -0
  78. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/web/claude_sse_adapter.py +0 -0
  79. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/web/terminal_repl.py +0 -0
  80. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/workspace_hints.py +0 -0
  81. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode.egg-info/SOURCES.txt +0 -0
  82. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode.egg-info/dependency_links.txt +0 -0
  83. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode.egg-info/entry_points.txt +0 -0
  84. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode.egg-info/requires.txt +0 -0
  85. {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode.egg-info/top_level.txt +0 -0
  86. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_agent_instruction.py +0 -0
  87. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_autocompact.py +0 -0
  88. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_capability_routing.py +0 -0
  89. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_claude_web_adapter_sse.py +0 -0
  90. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_cli_init.py +0 -0
  91. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_computer_use_permissions.py +0 -0
  92. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_context_budget.py +0 -0
  93. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_context_warning.py +0 -0
  94. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_credentials.py +0 -0
  95. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_interactive_permission_ask.py +0 -0
  96. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_kairos_scheduler.py +0 -0
  97. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_modality_tools.py +0 -0
  98. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_model_error_retry.py +0 -0
  99. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_model_errors.py +0 -0
  100. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_model_routing.py +0 -0
  101. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_paths.py +0 -0
  102. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_permissions.py +0 -0
  103. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_prompt_suggestions.py +0 -0
  104. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_repl_commands.py +0 -0
  105. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_repl_slash.py +0 -0
  106. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_slash_commands.py +0 -0
  107. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_thinking_config.py +0 -0
  108. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_token_budget.py +0 -0
  109. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_tool_context_circulation.py +0 -0
  110. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_tools.py +0 -0
  111. {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_tools_inspector.py +0 -0
  112. {gemcode-0.3.19 → gemcode-0.3.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.3.19
3
+ Version: 0.3.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.3.19"
7
+ version = "0.3.20"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -176,6 +176,11 @@ def slash_help_lines() -> list[str]:
176
176
  " /model Show model routing info",
177
177
  " /model use <id> Override model for this REPL session",
178
178
  " /model list List available Gemini model IDs",
179
+ " /thinking Show current thinking config",
180
+ " /thinking off Disable model thinking",
181
+ " /thinking on Re-enable thinking (auto budget/level)",
182
+ " /thinking budget <N> Set thinking token budget (Gemini 2.5, 0=off, -1=dynamic)",
183
+ " /thinking level <L> Set thinking level: minimal|low|medium|high (Gemini 3.x)",
179
184
  " /permissions Show permission / HITL settings",
180
185
  " /memory Show persistent memory settings",
181
186
  " /hooks Show post-turn hook configuration",
@@ -39,6 +39,7 @@ class ReplSlashResult:
39
39
  new_session_id: str | None = None
40
40
  skip_model_turn: bool = False
41
41
  model_prompt: str | None = None
42
+ force_rebuild_runner: bool = False # True when agent config changed (thinking, etc.)
42
43
 
43
44
 
44
45
  def _parse_tail_n(args: str, *, default: int = 40) -> int:
@@ -270,6 +271,95 @@ async def process_repl_slash(
270
271
  if name in ("exit", "quit"):
271
272
  return ReplSlashResult(exit_repl=True)
272
273
 
274
+ if name == "thinking":
275
+ model_id = getattr(cfg, "model", "") or ""
276
+ is_25 = "2.5" in model_id
277
+ args = (sc.args or "").strip()
278
+
279
+ if not args:
280
+ # Show current thinking config.
281
+ disable = bool(getattr(cfg, "disable_thinking", False))
282
+ level = getattr(cfg, "thinking_level", None)
283
+ budget = getattr(cfg, "thinking_budget", None)
284
+ out("Thinking config:")
285
+ out(f" model: {model_id or '(default)'}")
286
+ out(f" disable_thinking: {disable}")
287
+ if is_25:
288
+ out(f" thinking_budget: {budget if budget is not None else '(auto / dynamic)'}")
289
+ out()
290
+ out("Gemini 2.5 commands:")
291
+ out(" /thinking off — disable thinking")
292
+ out(" /thinking on — re-enable with auto budget")
293
+ out(" /thinking budget <0-24576> — set exact token budget (0 = off)")
294
+ else:
295
+ out(f" thinking_level: {level if level is not None else '(auto)'}")
296
+ out()
297
+ out("Gemini 3.x commands:")
298
+ out(" /thinking off — use minimal thinking")
299
+ out(" /thinking on — re-enable auto level")
300
+ out(" /thinking level <minimal|low|medium|high>")
301
+ out()
302
+ return ReplSlashResult(skip_model_turn=True)
303
+
304
+ parts = args.split()
305
+ sub = parts[0].lower()
306
+
307
+ if sub == "off":
308
+ setattr(cfg, "disable_thinking", True)
309
+ out("thinking: disabled (runner will rebuild on next turn)")
310
+ out()
311
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
312
+
313
+ if sub in ("on", "auto"):
314
+ setattr(cfg, "disable_thinking", False)
315
+ setattr(cfg, "thinking_level", None)
316
+ setattr(cfg, "thinking_budget", None)
317
+ out("thinking: auto (runner will rebuild on next turn)")
318
+ out()
319
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
320
+
321
+ if sub == "budget":
322
+ if len(parts) < 2:
323
+ out("Usage: /thinking budget <N> e.g. /thinking budget 8192")
324
+ out()
325
+ return ReplSlashResult(skip_model_turn=True)
326
+ try:
327
+ budget = int(parts[1])
328
+ except ValueError:
329
+ out(f"Invalid budget '{parts[1]}' — must be an integer (0–24576, or -1 for dynamic)")
330
+ out()
331
+ return ReplSlashResult(skip_model_turn=True)
332
+ setattr(cfg, "thinking_budget", budget)
333
+ setattr(cfg, "disable_thinking", False)
334
+ out(f"thinking: budget={budget} tokens (runner will rebuild on next turn)")
335
+ out()
336
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
337
+
338
+ if sub == "level":
339
+ if len(parts) < 2:
340
+ out("Usage: /thinking level <minimal|low|medium|high>")
341
+ out()
342
+ return ReplSlashResult(skip_model_turn=True)
343
+ level = parts[1].lower()
344
+ valid = ("minimal", "low", "medium", "high")
345
+ if level not in valid:
346
+ out(f"Unknown level '{level}'. Choose from: {', '.join(valid)}")
347
+ out()
348
+ return ReplSlashResult(skip_model_turn=True)
349
+ setattr(cfg, "thinking_level", level)
350
+ setattr(cfg, "disable_thinking", False)
351
+ out(f"thinking: level={level} (runner will rebuild on next turn)")
352
+ out()
353
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
354
+
355
+ out(f"Unknown /thinking subcommand: {sub}")
356
+ if is_25:
357
+ out("Usage: /thinking [off | on | budget <N>]")
358
+ else:
359
+ out("Usage: /thinking [off | on | level <minimal|low|medium|high>]")
360
+ out()
361
+ return ReplSlashResult(skip_model_turn=True)
362
+
273
363
  out(f"Unknown command: /{sc.command_name}")
274
364
  out("Try /help")
275
365
  out()
@@ -25,6 +25,13 @@ def _is_gemini_3_pro(model_id: str) -> bool:
25
25
  return "3.1-pro" in (model_id or "") or "3-pro" in (model_id or "")
26
26
 
27
27
 
28
+ def _supports_thinking(model_id: str) -> bool:
29
+ """True for Gemini 2.5+ and Gemini 3.x which return thought tokens."""
30
+ m = model_id or ""
31
+ return "2.5" in m or "2.0-flash-thinking" in m or "gemini-3" in m or \
32
+ "-3." in m or "-3-" in m
33
+
34
+
28
35
  def build_thinking_config(cfg: GemCodeConfig) -> Optional[types.ThinkingConfig]:
29
36
  """
30
37
  Returns a `types.ThinkingConfig` to pass to ADK's LlmAgent.generate_content_config.
@@ -96,7 +103,11 @@ def build_thinking_config(cfg: GemCodeConfig) -> Optional[types.ThinkingConfig]:
96
103
  # Otherwise: Claude-like auto mapping based on model_mode.
97
104
  mode = (getattr(cfg, "model_mode", "auto") or "auto").lower()
98
105
  if mode == "auto":
99
- # Let Gemini choose its dynamic/adaptive thinking.
106
+ # Use Gemini's adaptive thinking budget but request thought tokens in the
107
+ # response so the TUI can display them. Models that don't support thinking
108
+ # ignore include_thoughts gracefully; for others we fall back to None.
109
+ if _supports_thinking(model_id):
110
+ return types.ThinkingConfig(include_thoughts=True)
100
111
  return None
101
112
 
102
113
  if not is_25:
@@ -104,33 +115,32 @@ def build_thinking_config(cfg: GemCodeConfig) -> Optional[types.ThinkingConfig]:
104
115
  if mode == "fast":
105
116
  return types.ThinkingConfig(
106
117
  thinking_level="low" if _is_gemini_3_pro(model_id) else "minimal",
107
- include_thoughts=None,
118
+ include_thoughts=True,
108
119
  )
109
120
  if mode == "balanced":
110
121
  return types.ThinkingConfig(
111
122
  thinking_level="medium",
112
- include_thoughts=None,
123
+ include_thoughts=True,
113
124
  )
114
125
  # quality
115
126
  return types.ThinkingConfig(
116
127
  thinking_level="high",
117
- include_thoughts=None,
128
+ include_thoughts=True,
118
129
  )
119
130
 
120
131
  # Gemini 2.5 thinkingBudget mapping.
121
132
  if mode == "fast":
122
133
  if "2.5-pro" in model_id:
123
- return types.ThinkingConfig(thinking_budget=512, include_thoughts=None)
134
+ return types.ThinkingConfig(thinking_budget=512, include_thoughts=True)
124
135
  # flash/flash-preview/flash-lite
125
136
  return types.ThinkingConfig(
126
- thinking_budget=0 if "flash-lite" in model_id else 0, include_thoughts=None
137
+ thinking_budget=0 if "flash-lite" in model_id else 1024, include_thoughts=True
127
138
  )
128
139
  if mode == "balanced":
129
140
  if "2.5-pro" in model_id:
130
- return types.ThinkingConfig(thinking_budget=4096, include_thoughts=None)
131
- return types.ThinkingConfig(thinking_budget=1024, include_thoughts=None)
141
+ return types.ThinkingConfig(thinking_budget=4096, include_thoughts=True)
142
+ return types.ThinkingConfig(thinking_budget=2048, include_thoughts=True)
132
143
 
133
- # quality
134
- # For 2.5 Pro this remains dynamic because disable isn't supported.
135
- return types.ThinkingConfig(thinking_budget=-1, include_thoughts=None)
144
+ # quality — dynamic budget (model decides how much to think)
145
+ return types.ThinkingConfig(thinking_budget=-1, include_thoughts=True)
136
146
 
@@ -49,6 +49,7 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
49
49
  ("compact", "Summarise conversation history to reclaim context space"),
50
50
  ("config", "Show active configuration and environment variables"),
51
51
  ("permissions", "Show current permission mode (default / strict / yes)"),
52
+ ("thinking", "View or change thinking config · /thinking level <low|medium|high> · /thinking budget <N>"),
52
53
  ("audit", "Show recent audit log · /audit [N lines]"),
53
54
  ("memory", "Show memory / storage settings"),
54
55
  ("doctor", "Run diagnostics and validate the environment"),
@@ -236,22 +236,62 @@ async def run_gemcode_scrollback_tui(
236
236
  pass
237
237
  return tool_name, hint
238
238
 
239
- # ── Live status indicator ─────────────────────────────────────────────────
240
- # spinner_active[0] = True while "⟳ Working..." is on the current line.
241
- # _clear_spinner() uses \r + ANSI erase to overwrite it before real output.
242
- spinner_active = [False]
239
+ # ── Live animated status indicator ───────────────────────────────────────
240
+ # An asyncio.Task drives a braille spinner that rewrites the current line
241
+ # every 150 ms with the elapsed time. Because the spinner runs concurrently
242
+ # with `async for ev in runner.run_async(...)`, it updates while we await
243
+ # the next event — giving the user live timing feedback at every phase:
244
+ # "Thinking…" → model generating first response
245
+ # "Running…" → tool executing (can be 30–120 s for npx / cargo / pytest)
246
+ # "Querying…" → model re-querying after tool results
247
+ _anim_task: list = [None] # asyncio.Task | None
248
+ _anim_active: list = [False] # True while spinner text is on stdout
249
+
250
+ async def _spinner_loop(msg: str) -> None:
251
+ frames = "⣾⣽⣻⢿⡿⣟⣯⣷"
252
+ t0 = asyncio.get_running_loop().time()
253
+ i = 0
254
+ try:
255
+ while True:
256
+ elapsed = asyncio.get_running_loop().time() - t0
257
+ frame = frames[i % len(frames)]
258
+ sys.stdout.write(
259
+ f"\r {ansi.dim}{frame} {msg} ({elapsed:.0f}s){ansi.reset} "
260
+ )
261
+ sys.stdout.flush()
262
+ i += 1
263
+ await asyncio.sleep(0.15)
264
+ except asyncio.CancelledError:
265
+ pass # _stop_anim() handles clearing the line
266
+
267
+ def _start_anim(msg: str) -> None:
268
+ """Start (or restart) the spinner with a new status message."""
269
+ _stop_anim()
270
+ if not ansi.enabled:
271
+ return
272
+ try:
273
+ loop = asyncio.get_event_loop()
274
+ if loop.is_running():
275
+ _anim_task[0] = loop.create_task(_spinner_loop(msg))
276
+ _anim_active[0] = True
277
+ except Exception:
278
+ pass
243
279
 
244
- def _clear_spinner() -> None:
245
- if spinner_active[0]:
280
+ def _stop_anim() -> None:
281
+ """Cancel the spinner task and erase its line from stdout."""
282
+ task = _anim_task[0]
283
+ if task is not None:
284
+ task.cancel()
285
+ _anim_task[0] = None
286
+ if _anim_active[0]:
246
287
  sys.stdout.write("\r\033[K")
247
288
  sys.stdout.flush()
248
- spinner_active[0] = False
289
+ _anim_active[0] = False
249
290
 
250
291
  def _fmt_tool_result(resp: object) -> str:
251
292
  """One-line summary of a tool execution result."""
252
293
  try:
253
294
  d = resp if isinstance(resp, dict) else {}
254
- # ADK may wrap the return value; try both the raw dict and a nested "result" key.
255
295
  inner = d.get("result", d)
256
296
  if not isinstance(inner, dict):
257
297
  inner = d
@@ -280,7 +320,7 @@ async def run_gemcode_scrollback_tui(
280
320
  return ""
281
321
 
282
322
  def _render_tool_results(ev) -> None:
283
- """Print a brief one-line summary when a tool finishes executing."""
323
+ """Show a one-line result summary; transition spinner to 'Querying…'."""
284
324
  try:
285
325
  frs: list = []
286
326
  try:
@@ -292,28 +332,38 @@ async def run_gemcode_scrollback_tui(
292
332
  fr = getattr(part, "function_response", None)
293
333
  if fr is not None:
294
334
  frs.append(fr)
295
- for fr in frs:
296
- name = getattr(fr, "name", "") or ""
297
- if name == _ADK_REQUEST_CONFIRMATION:
298
- continue
299
- _clear_spinner()
335
+ real_frs = [
336
+ fr for fr in frs
337
+ if getattr(fr, "name", "") not in ("", _ADK_REQUEST_CONFIRMATION)
338
+ ]
339
+ if not real_frs:
340
+ return # nothing to show; keep spinner running
341
+ _stop_anim() # clear "Running…" before printing results
342
+ for fr in real_frs:
300
343
  resp = getattr(fr, "response", {}) or {}
301
344
  summary = _fmt_tool_result(resp)
302
345
  if summary:
303
346
  print(f" ⎿ {ansi.dim}\u21b3 {summary}{ansi.reset}")
347
+ # Restart spinner while model re-queries with the tool outputs.
348
+ _start_anim("Querying\u2026")
304
349
  except Exception:
305
350
  pass
306
351
 
307
352
  def _render_tool_calls(ev) -> None:
308
- _clear_spinner()
353
+ """Print tool-call lines; transition spinner to 'Running…'."""
309
354
  try:
310
355
  fcs = ev.get_function_calls() or []
311
356
  except Exception:
312
357
  fcs = []
313
- for fc in fcs:
358
+ real_fcs = [
359
+ fc for fc in fcs
360
+ if getattr(fc, "name", "") not in ("", _ADK_REQUEST_CONFIRMATION)
361
+ ]
362
+ if not real_fcs:
363
+ return # no visible tool calls; leave spinner as-is
364
+ _stop_anim() # clear "Thinking…" or "Querying…"
365
+ for fc in real_fcs:
314
366
  name = getattr(fc, "name", "") or ""
315
- if name == _ADK_REQUEST_CONFIRMATION:
316
- continue
317
367
  extra = format_tool_call_extras(fc)
318
368
  if extra:
319
369
  print(
@@ -322,6 +372,7 @@ async def run_gemcode_scrollback_tui(
322
372
  )
323
373
  else:
324
374
  print(f" ⎿ {ansi.blue_tool}[tool]{ansi.reset} {ansi.bold}{name}{ansi.reset}")
375
+ _start_anim("Running\u2026") # spinner while tool actually executes
325
376
 
326
377
  run_config = (
327
378
  RunConfig(max_llm_calls=cfg.max_llm_calls)
@@ -359,11 +410,16 @@ async def run_gemcode_scrollback_tui(
359
410
  current_session_id = slash.new_session_id
360
411
  _current_session_id_holder[0] = current_session_id
361
412
  if slash.skip_model_turn:
362
- # Runner binds the model at creation time (LlmAgent(model=...)),
363
- # so rebuild it when the user overrides the model mid-session.
413
+ # Runner holds the LlmAgent which bakes in model + thinking config at
414
+ # construction time. Rebuild whenever the model or thinking changes.
364
415
  new_model = getattr(cfg, "model", "")
365
416
  new_model_overridden = bool(getattr(cfg, "model_overridden", False))
366
- if new_model != old_model or new_model_overridden != old_model_overridden:
417
+ needs_rebuild = (
418
+ new_model != old_model
419
+ or new_model_overridden != old_model_overridden
420
+ or slash.force_rebuild_runner
421
+ )
422
+ if needs_rebuild:
367
423
  try:
368
424
  close_fn = getattr(runner, "close", None)
369
425
  if close_fn:
@@ -400,13 +456,10 @@ async def run_gemcode_scrollback_tui(
400
456
  kwargs["run_config"] = run_config
401
457
  # (We don't handle token budget reset here; full-screen TUI does.)
402
458
 
403
- # Show a live "Working…" indicator so the user never wonders if the
404
- # agent froze. _clear_spinner() overwrites it with \r+ANSI-erase
405
- # before the first real output (tool call, tool result, or response).
406
- if ansi.enabled:
407
- spinner_active[0] = True
408
- sys.stdout.write(f" {ansi.dim}\u27f3 Working\u2026{ansi.reset}")
409
- sys.stdout.flush()
459
+ # Animated spinner starts immediately so the user always knows the
460
+ # agent is active. It transitions: Thinking… Running… → Querying…
461
+ # as different phases of the turn complete.
462
+ _start_anim("Thinking\u2026")
410
463
 
411
464
  async for ev in runner.run_async(**kwargs):
412
465
  events.append(ev)
@@ -429,7 +482,7 @@ async def run_gemcode_scrollback_tui(
429
482
  except Exception:
430
483
  continue
431
484
 
432
- _clear_spinner() # ensure "Working…" is gone before any response line
485
+ _stop_anim() # ensure spinner is gone before printing the response
433
486
 
434
487
  if not assistant_wrote_text and _events_had_non_confirmation_tools(events):
435
488
  await typewrite(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.19
3
+ Version: 0.3.20
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
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