gemcode 0.3.19__tar.gz → 0.3.21__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.21}/PKG-INFO +1 -1
- {gemcode-0.3.19 → gemcode-0.3.21}/pyproject.toml +1 -1
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/repl_commands.py +5 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/repl_slash.py +90 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/thinking.py +21 -11
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tui/input_handler.py +1 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tui/scrollback.py +111 -51
- {gemcode-0.3.19 → gemcode-0.3.21/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.19 → gemcode-0.3.21}/LICENSE +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/MANIFEST.in +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/README.md +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/setup.cfg +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/agent.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/config.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/version.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode.egg-info/SOURCES.txt +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_credentials.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_paths.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_permissions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_tools.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.19 → gemcode-0.3.21}/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"),
|
|
@@ -9,6 +9,8 @@ from dataclasses import dataclass
|
|
|
9
9
|
from google.adk.agents.run_config import RunConfig
|
|
10
10
|
from google.genai import types
|
|
11
11
|
from rich.console import Console
|
|
12
|
+
from rich.markdown import Markdown as _RichMarkdown
|
|
13
|
+
from rich.padding import Padding as _RichPadding
|
|
12
14
|
|
|
13
15
|
from gemcode.capability_routing import apply_capability_routing
|
|
14
16
|
from gemcode.config import load_cli_environment
|
|
@@ -236,22 +238,62 @@ async def run_gemcode_scrollback_tui(
|
|
|
236
238
|
pass
|
|
237
239
|
return tool_name, hint
|
|
238
240
|
|
|
239
|
-
# ── Live status indicator
|
|
240
|
-
#
|
|
241
|
-
#
|
|
242
|
-
|
|
241
|
+
# ── Live animated status indicator ───────────────────────────────────────
|
|
242
|
+
# An asyncio.Task drives a braille spinner that rewrites the current line
|
|
243
|
+
# every 150 ms with the elapsed time. Because the spinner runs concurrently
|
|
244
|
+
# with `async for ev in runner.run_async(...)`, it updates while we await
|
|
245
|
+
# the next event — giving the user live timing feedback at every phase:
|
|
246
|
+
# "Thinking…" → model generating first response
|
|
247
|
+
# "Running…" → tool executing (can be 30–120 s for npx / cargo / pytest)
|
|
248
|
+
# "Querying…" → model re-querying after tool results
|
|
249
|
+
_anim_task: list = [None] # asyncio.Task | None
|
|
250
|
+
_anim_active: list = [False] # True while spinner text is on stdout
|
|
251
|
+
|
|
252
|
+
async def _spinner_loop(msg: str) -> None:
|
|
253
|
+
frames = "⣾⣽⣻⢿⡿⣟⣯⣷"
|
|
254
|
+
t0 = asyncio.get_running_loop().time()
|
|
255
|
+
i = 0
|
|
256
|
+
try:
|
|
257
|
+
while True:
|
|
258
|
+
elapsed = asyncio.get_running_loop().time() - t0
|
|
259
|
+
frame = frames[i % len(frames)]
|
|
260
|
+
sys.stdout.write(
|
|
261
|
+
f"\r {ansi.dim}{frame} {msg} ({elapsed:.0f}s){ansi.reset} "
|
|
262
|
+
)
|
|
263
|
+
sys.stdout.flush()
|
|
264
|
+
i += 1
|
|
265
|
+
await asyncio.sleep(0.15)
|
|
266
|
+
except asyncio.CancelledError:
|
|
267
|
+
pass # _stop_anim() handles clearing the line
|
|
268
|
+
|
|
269
|
+
def _start_anim(msg: str) -> None:
|
|
270
|
+
"""Start (or restart) the spinner with a new status message."""
|
|
271
|
+
_stop_anim()
|
|
272
|
+
if not ansi.enabled:
|
|
273
|
+
return
|
|
274
|
+
try:
|
|
275
|
+
loop = asyncio.get_event_loop()
|
|
276
|
+
if loop.is_running():
|
|
277
|
+
_anim_task[0] = loop.create_task(_spinner_loop(msg))
|
|
278
|
+
_anim_active[0] = True
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
243
281
|
|
|
244
|
-
def
|
|
245
|
-
|
|
282
|
+
def _stop_anim() -> None:
|
|
283
|
+
"""Cancel the spinner task and erase its line from stdout."""
|
|
284
|
+
task = _anim_task[0]
|
|
285
|
+
if task is not None:
|
|
286
|
+
task.cancel()
|
|
287
|
+
_anim_task[0] = None
|
|
288
|
+
if _anim_active[0]:
|
|
246
289
|
sys.stdout.write("\r\033[K")
|
|
247
290
|
sys.stdout.flush()
|
|
248
|
-
|
|
291
|
+
_anim_active[0] = False
|
|
249
292
|
|
|
250
293
|
def _fmt_tool_result(resp: object) -> str:
|
|
251
294
|
"""One-line summary of a tool execution result."""
|
|
252
295
|
try:
|
|
253
296
|
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
297
|
inner = d.get("result", d)
|
|
256
298
|
if not isinstance(inner, dict):
|
|
257
299
|
inner = d
|
|
@@ -280,7 +322,7 @@ async def run_gemcode_scrollback_tui(
|
|
|
280
322
|
return ""
|
|
281
323
|
|
|
282
324
|
def _render_tool_results(ev) -> None:
|
|
283
|
-
"""
|
|
325
|
+
"""Show a one-line result summary; transition spinner to 'Querying…'."""
|
|
284
326
|
try:
|
|
285
327
|
frs: list = []
|
|
286
328
|
try:
|
|
@@ -292,28 +334,38 @@ async def run_gemcode_scrollback_tui(
|
|
|
292
334
|
fr = getattr(part, "function_response", None)
|
|
293
335
|
if fr is not None:
|
|
294
336
|
frs.append(fr)
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
337
|
+
real_frs = [
|
|
338
|
+
fr for fr in frs
|
|
339
|
+
if getattr(fr, "name", "") not in ("", _ADK_REQUEST_CONFIRMATION)
|
|
340
|
+
]
|
|
341
|
+
if not real_frs:
|
|
342
|
+
return # nothing to show; keep spinner running
|
|
343
|
+
_stop_anim() # clear "Running…" before printing results
|
|
344
|
+
for fr in real_frs:
|
|
300
345
|
resp = getattr(fr, "response", {}) or {}
|
|
301
346
|
summary = _fmt_tool_result(resp)
|
|
302
347
|
if summary:
|
|
303
348
|
print(f" ⎿ {ansi.dim}\u21b3 {summary}{ansi.reset}")
|
|
349
|
+
# Restart spinner while model re-queries with the tool outputs.
|
|
350
|
+
_start_anim("Querying\u2026")
|
|
304
351
|
except Exception:
|
|
305
352
|
pass
|
|
306
353
|
|
|
307
354
|
def _render_tool_calls(ev) -> None:
|
|
308
|
-
|
|
355
|
+
"""Print tool-call lines; transition spinner to 'Running…'."""
|
|
309
356
|
try:
|
|
310
357
|
fcs = ev.get_function_calls() or []
|
|
311
358
|
except Exception:
|
|
312
359
|
fcs = []
|
|
313
|
-
|
|
360
|
+
real_fcs = [
|
|
361
|
+
fc for fc in fcs
|
|
362
|
+
if getattr(fc, "name", "") not in ("", _ADK_REQUEST_CONFIRMATION)
|
|
363
|
+
]
|
|
364
|
+
if not real_fcs:
|
|
365
|
+
return # no visible tool calls; leave spinner as-is
|
|
366
|
+
_stop_anim() # clear "Thinking…" or "Querying…"
|
|
367
|
+
for fc in real_fcs:
|
|
314
368
|
name = getattr(fc, "name", "") or ""
|
|
315
|
-
if name == _ADK_REQUEST_CONFIRMATION:
|
|
316
|
-
continue
|
|
317
369
|
extra = format_tool_call_extras(fc)
|
|
318
370
|
if extra:
|
|
319
371
|
print(
|
|
@@ -322,6 +374,7 @@ async def run_gemcode_scrollback_tui(
|
|
|
322
374
|
)
|
|
323
375
|
else:
|
|
324
376
|
print(f" ⎿ {ansi.blue_tool}[tool]{ansi.reset} {ansi.bold}{name}{ansi.reset}")
|
|
377
|
+
_start_anim("Running\u2026") # spinner while tool actually executes
|
|
325
378
|
|
|
326
379
|
run_config = (
|
|
327
380
|
RunConfig(max_llm_calls=cfg.max_llm_calls)
|
|
@@ -359,11 +412,16 @@ async def run_gemcode_scrollback_tui(
|
|
|
359
412
|
current_session_id = slash.new_session_id
|
|
360
413
|
_current_session_id_holder[0] = current_session_id
|
|
361
414
|
if slash.skip_model_turn:
|
|
362
|
-
# Runner
|
|
363
|
-
#
|
|
415
|
+
# Runner holds the LlmAgent which bakes in model + thinking config at
|
|
416
|
+
# construction time. Rebuild whenever the model or thinking changes.
|
|
364
417
|
new_model = getattr(cfg, "model", "")
|
|
365
418
|
new_model_overridden = bool(getattr(cfg, "model_overridden", False))
|
|
366
|
-
|
|
419
|
+
needs_rebuild = (
|
|
420
|
+
new_model != old_model
|
|
421
|
+
or new_model_overridden != old_model_overridden
|
|
422
|
+
or slash.force_rebuild_runner
|
|
423
|
+
)
|
|
424
|
+
if needs_rebuild:
|
|
367
425
|
try:
|
|
368
426
|
close_fn = getattr(runner, "close", None)
|
|
369
427
|
if close_fn:
|
|
@@ -400,13 +458,10 @@ async def run_gemcode_scrollback_tui(
|
|
|
400
458
|
kwargs["run_config"] = run_config
|
|
401
459
|
# (We don't handle token budget reset here; full-screen TUI does.)
|
|
402
460
|
|
|
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()
|
|
461
|
+
# Animated spinner starts immediately so the user always knows the
|
|
462
|
+
# agent is active. It transitions: Thinking… → Running… → Querying…
|
|
463
|
+
# as different phases of the turn complete.
|
|
464
|
+
_start_anim("Thinking\u2026")
|
|
410
465
|
|
|
411
466
|
async for ev in runner.run_async(**kwargs):
|
|
412
467
|
events.append(ev)
|
|
@@ -429,7 +484,7 @@ async def run_gemcode_scrollback_tui(
|
|
|
429
484
|
except Exception:
|
|
430
485
|
continue
|
|
431
486
|
|
|
432
|
-
|
|
487
|
+
_stop_anim() # ensure spinner is gone before printing the response
|
|
433
488
|
|
|
434
489
|
if not assistant_wrote_text and _events_had_non_confirmation_tools(events):
|
|
435
490
|
await typewrite(
|
|
@@ -439,28 +494,33 @@ async def run_gemcode_scrollback_tui(
|
|
|
439
494
|
|
|
440
495
|
confirmation_fcs = _get_confirmation_fcs(events)
|
|
441
496
|
if not confirmation_fcs:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
497
|
+
thought_text = "".join(buffered_thought).strip()
|
|
498
|
+
final_text = "".join(buffered_final).strip()
|
|
499
|
+
|
|
500
|
+
# ── Thinking indicator (collapsed, never full dump) ───────────────
|
|
501
|
+
# Show only a brief one-line excerpt so the user knows thinking
|
|
502
|
+
# happened without drowning in the raw internal monologue.
|
|
503
|
+
if thought_text and not (
|
|
504
|
+
buffered_final and _normalize_ws(thought_text) == _normalize_ws(final_text)
|
|
505
|
+
):
|
|
506
|
+
excerpt = thought_text.replace("\n", " ").strip()
|
|
507
|
+
if len(excerpt) > 100:
|
|
508
|
+
excerpt = excerpt[:97] + "\u2026"
|
|
509
|
+
print(
|
|
510
|
+
f" \u23bf {ansi.dim}\u2234 Thinking{ansi.reset}"
|
|
511
|
+
f"{ansi.dim} {excerpt}{ansi.reset}"
|
|
512
|
+
)
|
|
513
|
+
print("")
|
|
514
|
+
|
|
515
|
+
# ── Response — rendered as Rich Markdown ──────────────────────────
|
|
516
|
+
# Pipes the text through Rich's Markdown renderer so **bold**,
|
|
517
|
+
# *italic*, `code`, bullet lists and fenced code blocks all display
|
|
518
|
+
# correctly instead of showing raw asterisks and backticks.
|
|
519
|
+
if final_text:
|
|
520
|
+
print(f" \u23bf {ansi.bold}GemCode{ansi.reset}:")
|
|
521
|
+
console.print(
|
|
522
|
+
_RichPadding(_RichMarkdown(final_text), (0, 0, 0, 4)),
|
|
523
|
+
)
|
|
464
524
|
break
|
|
465
525
|
|
|
466
526
|
interactive_enabled = bool(getattr(cfg, "interactive_permission_ask", False))
|
|
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
|