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.
- {gemcode-0.3.19/src/gemcode.egg-info → gemcode-0.3.20}/PKG-INFO +1 -1
- {gemcode-0.3.19 → gemcode-0.3.20}/pyproject.toml +1 -1
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/repl_commands.py +5 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/repl_slash.py +90 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/thinking.py +21 -11
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tui/input_handler.py +1 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tui/scrollback.py +82 -29
- {gemcode-0.3.19 → gemcode-0.3.20/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.19 → gemcode-0.3.20}/LICENSE +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/MANIFEST.in +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/README.md +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/setup.cfg +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/agent.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/config.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/version.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode.egg-info/SOURCES.txt +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_credentials.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_paths.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_permissions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_tools.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.20}/tests/test_workspace_hints.py +0 -0
|
@@ -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,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
|
-
#
|
|
241
|
-
#
|
|
242
|
-
|
|
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
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
363
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
404
|
-
# agent
|
|
405
|
-
#
|
|
406
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
File without changes
|