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.
- {gemcode-0.3.18/src/gemcode.egg-info → gemcode-0.3.20}/PKG-INFO +1 -1
- {gemcode-0.3.18 → gemcode-0.3.20}/pyproject.toml +1 -1
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/agent.py +22 -9
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/repl_commands.py +5 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/repl_slash.py +90 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/thinking.py +21 -11
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tui/input_handler.py +1 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tui/scrollback.py +139 -15
- {gemcode-0.3.18 → gemcode-0.3.20/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.18 → gemcode-0.3.20}/LICENSE +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/MANIFEST.in +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/README.md +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/setup.cfg +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/config.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/version.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode.egg-info/SOURCES.txt +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_credentials.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_paths.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_permissions.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_tools.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.18 → gemcode-0.3.20}/tests/test_workspace_hints.py +0 -0
|
@@ -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** —
|
|
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
|
|
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
|
|
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
|
|
178
|
-
3.
|
|
179
|
-
4.
|
|
180
|
-
5.
|
|
181
|
-
6.
|
|
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. Fix — iterate 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
|
-
#
|
|
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=
|
|
118
|
+
include_thoughts=True,
|
|
108
119
|
)
|
|
109
120
|
if mode == "balanced":
|
|
110
121
|
return types.ThinkingConfig(
|
|
111
122
|
thinking_level="medium",
|
|
112
|
-
include_thoughts=
|
|
123
|
+
include_thoughts=True,
|
|
113
124
|
)
|
|
114
125
|
# quality
|
|
115
126
|
return types.ThinkingConfig(
|
|
116
127
|
thinking_level="high",
|
|
117
|
-
include_thoughts=
|
|
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=
|
|
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
|
|
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=
|
|
131
|
-
return types.ThinkingConfig(thinking_budget=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
294
|
-
#
|
|
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
|
-
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|