gemcode 0.3.18__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.18/src/gemcode.egg-info → gemcode-0.3.20}/PKG-INFO +1 -1
  2. {gemcode-0.3.18 → gemcode-0.3.20}/pyproject.toml +1 -1
  3. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/agent.py +22 -9
  4. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/repl_commands.py +5 -0
  5. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/repl_slash.py +90 -0
  6. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/thinking.py +21 -11
  7. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tui/input_handler.py +1 -0
  8. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tui/scrollback.py +139 -15
  9. {gemcode-0.3.18 → gemcode-0.3.20/src/gemcode.egg-info}/PKG-INFO +1 -1
  10. {gemcode-0.3.18 → gemcode-0.3.20}/LICENSE +0 -0
  11. {gemcode-0.3.18 → gemcode-0.3.20}/MANIFEST.in +0 -0
  12. {gemcode-0.3.18 → gemcode-0.3.20}/README.md +0 -0
  13. {gemcode-0.3.18 → gemcode-0.3.20}/setup.cfg +0 -0
  14. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/__init__.py +0 -0
  15. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/__main__.py +0 -0
  16. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/audit.py +0 -0
  17. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/autocompact.py +0 -0
  18. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/callbacks.py +0 -0
  19. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/capability_routing.py +0 -0
  20. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/cli.py +0 -0
  21. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/compaction.py +0 -0
  22. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/computer_use/__init__.py +0 -0
  23. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/computer_use/browser_computer.py +0 -0
  24. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/config.py +0 -0
  25. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/context_budget.py +0 -0
  26. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/context_warning.py +0 -0
  27. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/credentials.py +0 -0
  28. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/hitl_session.py +0 -0
  29. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/interactions.py +0 -0
  30. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/invoke.py +0 -0
  31. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/kairos_daemon.py +0 -0
  32. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/limits.py +0 -0
  33. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/live_audio_engine.py +0 -0
  34. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/logging_config.py +0 -0
  35. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/mcp_loader.py +0 -0
  36. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/memory/__init__.py +0 -0
  37. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/memory/embedding_memory_service.py +0 -0
  38. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/memory/file_memory_service.py +0 -0
  39. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/modality_tools.py +0 -0
  40. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/model_errors.py +0 -0
  41. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/model_routing.py +0 -0
  42. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/paths.py +0 -0
  43. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/permissions.py +0 -0
  44. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/plugins/__init__.py +0 -0
  45. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  46. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  47. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/prompt_suggestions.py +0 -0
  48. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/__init__.py +0 -0
  49. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/config.py +0 -0
  50. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/deps.py +0 -0
  51. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/engine.py +0 -0
  52. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/stop_hooks.py +0 -0
  53. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/token_budget.py +0 -0
  54. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/transitions.py +0 -0
  55. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/session_runtime.py +0 -0
  56. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/slash_commands.py +0 -0
  57. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tool_prompt_manifest.py +0 -0
  58. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tool_registry.py +0 -0
  59. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/__init__.py +0 -0
  60. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/bash.py +0 -0
  61. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/edit.py +0 -0
  62. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/filesystem.py +0 -0
  63. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/search.py +0 -0
  64. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/shell.py +0 -0
  65. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/shell_gate.py +0 -0
  66. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/subtask.py +0 -0
  67. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/think.py +0 -0
  68. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/todo.py +0 -0
  69. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/web.py +0 -0
  70. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools_inspector.py +0 -0
  71. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/trust.py +0 -0
  72. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tui/spinner.py +0 -0
  73. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tui/welcome_banner.py +0 -0
  74. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tui/welcome_rich.py +0 -0
  75. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/version.py +0 -0
  76. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/vertex.py +0 -0
  77. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/web/__init__.py +0 -0
  78. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/web/claude_sse_adapter.py +0 -0
  79. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/web/terminal_repl.py +0 -0
  80. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/workspace_hints.py +0 -0
  81. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode.egg-info/SOURCES.txt +0 -0
  82. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode.egg-info/dependency_links.txt +0 -0
  83. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode.egg-info/entry_points.txt +0 -0
  84. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode.egg-info/requires.txt +0 -0
  85. {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode.egg-info/top_level.txt +0 -0
  86. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_agent_instruction.py +0 -0
  87. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_autocompact.py +0 -0
  88. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_capability_routing.py +0 -0
  89. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_claude_web_adapter_sse.py +0 -0
  90. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_cli_init.py +0 -0
  91. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_computer_use_permissions.py +0 -0
  92. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_context_budget.py +0 -0
  93. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_context_warning.py +0 -0
  94. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_credentials.py +0 -0
  95. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_interactive_permission_ask.py +0 -0
  96. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_kairos_scheduler.py +0 -0
  97. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_modality_tools.py +0 -0
  98. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_model_error_retry.py +0 -0
  99. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_model_errors.py +0 -0
  100. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_model_routing.py +0 -0
  101. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_paths.py +0 -0
  102. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_permissions.py +0 -0
  103. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_prompt_suggestions.py +0 -0
  104. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_repl_commands.py +0 -0
  105. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_repl_slash.py +0 -0
  106. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_slash_commands.py +0 -0
  107. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_thinking_config.py +0 -0
  108. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_token_budget.py +0 -0
  109. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_tool_context_circulation.py +0 -0
  110. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_tools.py +0 -0
  111. {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_tools_inspector.py +0 -0
  112. {gemcode-0.3.18 → 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.18
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.18"
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"
@@ -79,13 +79,26 @@ You run locally via the GemCode CLI. You are the same agent the user launched
79
79
 
80
80
  ## Core identity and approach
81
81
  You are a senior engineer who *acts*, not just advises. When given a task:
82
- 1. **Orient** — understand the repo structure, find the relevant files.
82
+ 1. **Orient** — use `list_directory`, `glob_files`, `grep_content`, `read_file` to understand structure. These tools need **no permission** and are instant. Start here.
83
83
  2. **Plan** — for complex tasks, call `todo_write` upfront to map out the work.
84
84
  3. **Execute** — make the changes, run the checks, iterate.
85
85
  4. **Verify** — confirm the result is correct before reporting done.
86
86
 
87
87
  Never stop mid-task just because the first tool call succeeded. Keep going until the full task is complete or you hit a genuine blocker.
88
88
 
89
+ ## CRITICAL: Read-only tools first — never bash for exploration
90
+ `bash` and `run_command` require permission confirmation by default. Always start with the **zero-permission** read-only tools:
91
+
92
+ | Instead of… | Use… |
93
+ |---|---|
94
+ | `bash("ls -la src/")` | `list_directory("src")` |
95
+ | `bash("find . -name '*.py'")` | `glob_files("**/*.py")` |
96
+ | `bash("cat file.py")` | `read_file("file.py")` |
97
+ | `bash("grep -r pattern .")` | `grep_content("pattern", "**/*")` |
98
+ | `bash("find . -type f \| head -50")` | `list_directory(".")` + `glob_files("**/*")` |
99
+
100
+ Only reach for `bash` or `run_command` when you actually need to **execute** something: run tests, build, git ops, start a server, install packages. **NEVER** use bash to list or read files.
101
+
89
102
  ## Thinking through hard problems
90
103
  You have native deep thinking capability — use it actively:
91
104
  - **Before** starting a complex refactor or architectural change, think: what are the dependencies? what could break? what is the minimal safe change?
@@ -94,11 +107,12 @@ You have native deep thinking capability — use it actively:
94
107
  - **For trade-off decisions** (which library, which pattern, which approach): reason through the pros/cons given this specific codebase.
95
108
 
96
109
  ## Interpreting requests
97
- - Treat every message as a software engineering task unless clearly otherwise.
110
+ - Treat every message as a software engineering task start working immediately. Do NOT respond with just a greeting or introduction.
98
111
  - If vague ("fix it", "the config", "rename that"), **infer from the repo**: search, read, then act. Do not give abstract advice when concrete files exist.
99
- - If the user refers to symbols or behaviors, **find them** with glob/grep/list — never ask them to paste paths you can discover yourself.
112
+ - If the user refers to symbols or behaviors, **find them** with `glob_files`/`grep_content`/`list_directory` — never ask them to paste paths you can discover yourself.
100
113
  - **Never propose edits to files you haven't read.** Read first, then edit.
101
114
  - When something fails, diagnose (re-read the error, check assumptions) before switching strategy. Do not repeat the same failed call.
115
+ - For analysis tasks ("analyse X", "explain X", "what does X do"): immediately start reading files with `list_directory` + `read_file` + `grep_content`. Produce concrete findings, not hypotheses.
102
116
 
103
117
  ## Tool selection guide
104
118
 
@@ -174,12 +188,11 @@ One user message = many model↔tool rounds (up to 256 LLM calls by default). Th
174
188
 
175
189
  **Standard workflow for complex tasks:**
176
190
  1. `todo_write` — plan the work items
177
- 2. Explore — `bash("find . ...")` or `list_directory` or `glob_files` to understand structure
178
- 3. Read — `read_file` with line ranges on large files; `grep_content` for symbol search
179
- 4. Edit — `write_file` / `search_replace` for changes
180
- 5. Verify`bash("pytest ...")` or `bash("npm run build ...")` or `bash("git diff")`
181
- 6. Fixiterate on failures, re-verify
182
- 7. Update todos — mark done as you go
191
+ 2. **Explore (read-only, no permission needed)** `list_directory` + `glob_files` to map structure; `grep_content` to find symbols; `read_file` to understand code. Do NOT use `bash` for this step.
192
+ 3. **Edit** — `write_file` / `search_replace` for changes
193
+ 4. **Verify (shell needed)** — `bash("pytest ...")` or `bash("npm run build ...")` or `bash("git diff")`
194
+ 5. Fixiterate on failures, re-verify
195
+ 6. Update todos mark done as you go
183
196
 
184
197
  **Do not stop after step 2 or 3** — complete the full task.
185
198
 
@@ -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,15 +236,134 @@ async def run_gemcode_scrollback_tui(
236
236
  pass
237
237
  return tool_name, hint
238
238
 
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
279
+
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]:
287
+ sys.stdout.write("\r\033[K")
288
+ sys.stdout.flush()
289
+ _anim_active[0] = False
290
+
291
+ def _fmt_tool_result(resp: object) -> str:
292
+ """One-line summary of a tool execution result."""
293
+ try:
294
+ d = resp if isinstance(resp, dict) else {}
295
+ inner = d.get("result", d)
296
+ if not isinstance(inner, dict):
297
+ inner = d
298
+ err = inner.get("error") or d.get("error")
299
+ if err:
300
+ return f"\u2717 {str(err)[:80]}"
301
+ exit_code = inner.get("exit_code")
302
+ if exit_code is not None:
303
+ icon = "\u2713" if exit_code == 0 else f"\u2717 exit {exit_code}"
304
+ out = str(inner.get("stdout", "") or "").strip()
305
+ first = out.split("\n")[0][:80] if out else ""
306
+ if not first:
307
+ first = str(inner.get("stderr", "") or "").strip().split("\n")[0][:80]
308
+ return f"{icon} {first}" if first else icon
309
+ if inner.get("content") is not None:
310
+ lines = str(inner["content"]).count("\n") + 1
311
+ return f"\u2713 {lines} lines"
312
+ if inner.get("files") is not None:
313
+ return f"\u2713 {len(inner['files'])} files"
314
+ if inner.get("matches") is not None:
315
+ return f"\u2713 {len(inner['matches'])} matches"
316
+ if inner.get("ok") or d.get("ok"):
317
+ return "\u2713"
318
+ return ""
319
+ except Exception:
320
+ return ""
321
+
322
+ def _render_tool_results(ev) -> None:
323
+ """Show a one-line result summary; transition spinner to 'Querying…'."""
324
+ try:
325
+ frs: list = []
326
+ try:
327
+ frs = ev.get_function_responses() or []
328
+ except Exception:
329
+ pass
330
+ if not frs and ev.content and ev.content.parts:
331
+ for part in ev.content.parts:
332
+ fr = getattr(part, "function_response", None)
333
+ if fr is not None:
334
+ frs.append(fr)
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:
343
+ resp = getattr(fr, "response", {}) or {}
344
+ summary = _fmt_tool_result(resp)
345
+ if summary:
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")
349
+ except Exception:
350
+ pass
351
+
239
352
  def _render_tool_calls(ev) -> None:
353
+ """Print tool-call lines; transition spinner to 'Running…'."""
240
354
  try:
241
355
  fcs = ev.get_function_calls() or []
242
356
  except Exception:
243
357
  fcs = []
244
- 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:
245
366
  name = getattr(fc, "name", "") or ""
246
- if name == _ADK_REQUEST_CONFIRMATION:
247
- continue
248
367
  extra = format_tool_call_extras(fc)
249
368
  if extra:
250
369
  print(
@@ -253,6 +372,7 @@ async def run_gemcode_scrollback_tui(
253
372
  )
254
373
  else:
255
374
  print(f" ⎿ {ansi.blue_tool}[tool]{ansi.reset} {ansi.bold}{name}{ansi.reset}")
375
+ _start_anim("Running\u2026") # spinner while tool actually executes
256
376
 
257
377
  run_config = (
258
378
  RunConfig(max_llm_calls=cfg.max_llm_calls)
@@ -290,11 +410,16 @@ async def run_gemcode_scrollback_tui(
290
410
  current_session_id = slash.new_session_id
291
411
  _current_session_id_holder[0] = current_session_id
292
412
  if slash.skip_model_turn:
293
- # Runner binds the model at creation time (LlmAgent(model=...)),
294
- # 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.
295
415
  new_model = getattr(cfg, "model", "")
296
416
  new_model_overridden = bool(getattr(cfg, "model_overridden", False))
297
- 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:
298
423
  try:
299
424
  close_fn = getattr(runner, "close", None)
300
425
  if close_fn:
@@ -331,9 +456,15 @@ async def run_gemcode_scrollback_tui(
331
456
  kwargs["run_config"] = run_config
332
457
  # (We don't handle token budget reset here; full-screen TUI does.)
333
458
 
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")
463
+
334
464
  async for ev in runner.run_async(**kwargs):
335
465
  events.append(ev)
336
466
  _render_tool_calls(ev)
467
+ _render_tool_results(ev)
337
468
  try:
338
469
  if not ev.content or not ev.content.parts:
339
470
  continue
@@ -351,6 +482,8 @@ async def run_gemcode_scrollback_tui(
351
482
  except Exception:
352
483
  continue
353
484
 
485
+ _stop_anim() # ensure spinner is gone before printing the response
486
+
354
487
  if not assistant_wrote_text and _events_had_non_confirmation_tools(events):
355
488
  await typewrite(
356
489
  f"{ansi.dim}(Tools ran without a text reply in this step; "
@@ -377,15 +510,6 @@ async def run_gemcode_scrollback_tui(
377
510
  await typewrite(thought_text)
378
511
  sys.stdout.write("\n")
379
512
  sys.stdout.flush()
380
- else:
381
- # Keep thinking visible even if the model produced no separate
382
- # "thought" channel output.
383
- print(
384
- f" ⎿ {ansi.dim}{ansi.bold}\u2234 Thinking{ansi.reset}: "
385
- "(no thinking output)"
386
- )
387
- print("")
388
-
389
513
  if buffered_final:
390
514
  sys.stdout.write(f" ⎿ {ansi.bold}GemCode{ansi.reset}: ")
391
515
  sys.stdout.flush()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.18
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