gemcode 0.3.2__tar.gz → 0.3.7__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 (106) hide show
  1. {gemcode-0.3.2/src/gemcode.egg-info → gemcode-0.3.7}/PKG-INFO +1 -1
  2. {gemcode-0.3.2 → gemcode-0.3.7}/pyproject.toml +1 -1
  3. gemcode-0.3.7/src/gemcode/__init__.py +5 -0
  4. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/agent.py +25 -3
  5. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/config.py +6 -0
  6. gemcode-0.3.7/src/gemcode/logging_config.py +44 -0
  7. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/repl_commands.py +6 -1
  8. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/repl_slash.py +25 -0
  9. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tui/app.py +103 -13
  10. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tui/scrollback.py +155 -14
  11. gemcode-0.3.7/src/gemcode/version.py +15 -0
  12. gemcode-0.3.7/src/gemcode/workspace_hints.py +23 -0
  13. {gemcode-0.3.2 → gemcode-0.3.7/src/gemcode.egg-info}/PKG-INFO +1 -1
  14. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode.egg-info/SOURCES.txt +6 -1
  15. gemcode-0.3.7/tests/test_agent_instruction.py +12 -0
  16. gemcode-0.3.7/tests/test_workspace_hints.py +16 -0
  17. gemcode-0.3.2/src/gemcode/__init__.py +0 -3
  18. {gemcode-0.3.2 → gemcode-0.3.7}/LICENSE +0 -0
  19. {gemcode-0.3.2 → gemcode-0.3.7}/MANIFEST.in +0 -0
  20. {gemcode-0.3.2 → gemcode-0.3.7}/README.md +0 -0
  21. {gemcode-0.3.2 → gemcode-0.3.7}/setup.cfg +0 -0
  22. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/__main__.py +0 -0
  23. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/audit.py +0 -0
  24. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/autocompact.py +0 -0
  25. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/callbacks.py +0 -0
  26. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/capability_routing.py +0 -0
  27. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/cli.py +0 -0
  28. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/compaction.py +0 -0
  29. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/computer_use/__init__.py +0 -0
  30. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/computer_use/browser_computer.py +0 -0
  31. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/context_budget.py +0 -0
  32. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/context_warning.py +0 -0
  33. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/credentials.py +0 -0
  34. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/hitl_session.py +0 -0
  35. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/interactions.py +0 -0
  36. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/invoke.py +0 -0
  37. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/kairos_daemon.py +0 -0
  38. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/limits.py +0 -0
  39. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/live_audio_engine.py +0 -0
  40. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/mcp_loader.py +0 -0
  41. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/memory/__init__.py +0 -0
  42. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/memory/embedding_memory_service.py +0 -0
  43. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/memory/file_memory_service.py +0 -0
  44. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/modality_tools.py +0 -0
  45. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/model_errors.py +0 -0
  46. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/model_routing.py +0 -0
  47. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/paths.py +0 -0
  48. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/permissions.py +0 -0
  49. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/plugins/__init__.py +0 -0
  50. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  51. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  52. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/prompt_suggestions.py +0 -0
  53. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/__init__.py +0 -0
  54. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/config.py +0 -0
  55. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/deps.py +0 -0
  56. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/engine.py +0 -0
  57. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/stop_hooks.py +0 -0
  58. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/token_budget.py +0 -0
  59. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/transitions.py +0 -0
  60. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/session_runtime.py +0 -0
  61. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/slash_commands.py +0 -0
  62. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/thinking.py +0 -0
  63. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tool_prompt_manifest.py +0 -0
  64. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tool_registry.py +0 -0
  65. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/__init__.py +0 -0
  66. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/edit.py +0 -0
  67. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/filesystem.py +0 -0
  68. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/search.py +0 -0
  69. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/shell.py +0 -0
  70. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/shell_gate.py +0 -0
  71. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/todo.py +0 -0
  72. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools_inspector.py +0 -0
  73. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/trust.py +0 -0
  74. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/vertex.py +0 -0
  75. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/web/__init__.py +0 -0
  76. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/web/claude_sse_adapter.py +0 -0
  77. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/web/terminal_repl.py +0 -0
  78. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode.egg-info/dependency_links.txt +0 -0
  79. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode.egg-info/entry_points.txt +0 -0
  80. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode.egg-info/requires.txt +0 -0
  81. {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode.egg-info/top_level.txt +0 -0
  82. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_autocompact.py +0 -0
  83. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_capability_routing.py +0 -0
  84. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_claude_web_adapter_sse.py +0 -0
  85. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_cli_init.py +0 -0
  86. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_computer_use_permissions.py +0 -0
  87. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_context_budget.py +0 -0
  88. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_context_warning.py +0 -0
  89. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_credentials.py +0 -0
  90. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_interactive_permission_ask.py +0 -0
  91. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_kairos_scheduler.py +0 -0
  92. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_modality_tools.py +0 -0
  93. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_model_error_retry.py +0 -0
  94. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_model_errors.py +0 -0
  95. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_model_routing.py +0 -0
  96. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_paths.py +0 -0
  97. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_permissions.py +0 -0
  98. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_prompt_suggestions.py +0 -0
  99. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_repl_commands.py +0 -0
  100. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_repl_slash.py +0 -0
  101. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_slash_commands.py +0 -0
  102. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_thinking_config.py +0 -0
  103. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_token_budget.py +0 -0
  104. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_tool_context_circulation.py +0 -0
  105. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_tools.py +0 -0
  106. {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_tools_inspector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.2
3
+ Version: 0.3.7
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.2"
7
+ version = "0.3.7"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,5 @@
1
+ """GemCode: Gemini + ADK coding agent."""
2
+
3
+ from gemcode.version import get_version
4
+
5
+ __version__ = get_version()
@@ -56,11 +56,28 @@ def _load_gemini_md(project_root: Path) -> str:
56
56
  return ""
57
57
 
58
58
 
59
+ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
60
+ """
61
+ Injected every session so the model does not hallucinate deployment, permissions,
62
+ or "how to switch Pro" the way a product-agnostic base prompt would.
63
+ """
64
+ root = cfg.project_root.resolve()
65
+ model = (getattr(cfg, "model", None) or "").strip() or "(default)"
66
+ return f"""## Runtime facts (authoritative for this session)
67
+ - **Project root** — every filesystem tool path is relative to: `{root}`
68
+ - **Model id in use:** `{model}`. Changing it requires restarting GemCode with `--model <id>` or env `GEMCODE_MODEL`, or using `/model` in the REPL for routing info.
69
+ - **UI banner** phrases such as "GemCode Pro" are **terminal marketing**, not a separate API tier or model you enable from chat.
70
+ - **Env toggles** (`GEMCODE_ENABLE_COMPUTER_USE`, `GEMCODE_MODEL`, etc.) affect only the **OS process** that launched `gemcode`. Pasting `VAR=1` in chat does **not** reconfigure a running session—tell the user to export in their shell, use project `.env`, or restart the CLI.
71
+ - **Working in subfolders** — use tools: e.g. `list_directory("Desktop")`, `glob_files("**/query.ts")`, `read_file("testing/ai-edtech-app/src/app/page.tsx")`, or `run_command` with `cwd_subdir`. Never claim the sandbox cannot reach a subpath unless a tool returned an explicit error."""
72
+
73
+
59
74
  def build_instruction(cfg: GemCodeConfig) -> str:
60
75
  # Layered instructions mirror the *structure* of mature coding agents (scope,
61
76
  # task interpretation, tool choice, parallelism, risk)—not proprietary text.
62
- base = """You are GemCode, an expert software engineering agent.
63
- You operate only inside the user's project directory (current working directory).
77
+ base = f"""You are GemCode, an expert software engineering agent.
78
+ You run locally via the GemCode CLI and call **Google Gemini** through its API. You are the same agent stack the user launched—not a hosted "portal" you can reconfigure from inside the conversation.
79
+
80
+ {_build_runtime_facts(cfg)}
64
81
 
65
82
  ## How to interpret requests
66
83
  - Treat every message as a **software engineering** task in this repo unless the user clearly wants something else. If the instruction is vague ("fix it", "rename that", "the config", "see codebase"), **infer intent from the repository**: search, read, then act—do not answer with abstract advice when concrete files exist.
@@ -79,6 +96,10 @@ You operate only inside the user's project directory (current working directory)
79
96
  - **Parallelize:** when you need several **independent** reads or searches (no output from one is required to form the next call), issue them together in one turn so the user gets answers faster. When step B depends on step A's result, run **sequentially**.
80
97
  - **Deletion:** use `delete_file` for a single file under the project root; reserve `rm` via `run_command` for unusual cases.
81
98
  - **Autonomy:** explore with `list_directory` ("."), `glob_files` (e.g. `**/*.md`, `**/*keyword*`), and `grep_content` before asking "which file?". Prefer widening your search over interrogating the user.
99
+ - **Workspace scope:** All file tools use paths **relative to the project root** (the current working directory GemCode was started in). That root may be the user's home folder—then subfolders like `Desktop`, `Desktop/code`, or `Documents` are **inside** the sandbox. **Call** `list_directory("Desktop")` or `glob_files("**/*name*.ts")` instead of assuming access is blocked. **Only** treat access as denied when a tool returns an `error` string—**do not** invent extra "security" or "permission" policies the runtime did not report.
100
+ - **Finding files:** For a basename like `query.ts`, try several globs in one turn when needed: `**/query.ts`, `**/*query.ts`, `**/*_query.ts`. If the user names a parent path (e.g. Desktop), **list that path** and narrow down. If a search fails, **change the pattern** (broader `**`, partial stem) before saying "not found".
101
+ - **Agentic turns:** One user message can include **many** model↔tool rounds (bounded by runtime). If the task is **not** done after the first tool (e.g. you only searched, or read one file), **keep going** with more tools in the same turn until you can answer or have a clear blocker—do not stop at the first tool call unless it fully satisfies the request.
102
+ - **Model output:** If a response is mostly **function calls** without prose, that is normal—execute tools, then synthesize a clear **text** answer for the user once you have enough information.
82
103
 
83
104
  ## Risk and permissions
84
105
  - Destructive or irreversible actions (deletes, force pushes, anything that wipes data) deserve a clear, honest description; the runtime may require explicit user approval. If the session uses **inline** approval, wait for it—do not instruct the user to "re-run with --yes" unless that is actually required by the environment.
@@ -86,7 +107,8 @@ You operate only inside the user's project directory (current working directory)
86
107
 
87
108
  ## Communication
88
109
  - Before the first tool call in a turn, give a **short** line on what you are about to do. Assume the user does not see raw tool internals—summarize outcomes in plain language.
89
- - Prefer small, testable edits and accurate reporting over breadth."""
110
+ - Prefer small, testable edits and accurate reporting over breadth.
111
+ - If the user pastes **UI copy** or noise (e.g. fragments of a webpage, marketing lines, or mixed headings), infer intent: they often want that clutter **removed or replaced** in source—read the file, then edit the real `page.tsx` (or relevant file), do not treat pasted UI strings as a dialogue prompt."""
90
112
 
91
113
  tool_manifest = build_tool_manifest(cfg)
92
114
 
@@ -295,3 +295,9 @@ def load_cli_environment() -> None:
295
295
  from gemcode.credentials import apply_saved_google_api_key_to_environ
296
296
 
297
297
  apply_saved_google_api_key_to_environ()
298
+
299
+ from gemcode.logging_config import apply_gemcode_logging_filters
300
+ from gemcode.version import get_version
301
+
302
+ os.environ.setdefault("GEMCODE_VERSION", get_version())
303
+ apply_gemcode_logging_filters()
@@ -0,0 +1,44 @@
1
+ """
2
+ Tune third-party loggers for interactive CLI/TUI (expected Gemini function-call noise).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ import os
9
+
10
+
11
+ def apply_gemcode_logging_filters() -> None:
12
+ """
13
+ google.genai logs logger.warning when .text strips non-text parts (normal with tools).
14
+
15
+ That uses the **logging** module, not warnings.warn — filterwarnings cannot silence it.
16
+ Set GEMCODE_VERBOSE_GENAI=1 to keep those lines.
17
+ """
18
+ if os.environ.get("GEMCODE_VERBOSE_GENAI", "").lower() in (
19
+ "1",
20
+ "true",
21
+ "yes",
22
+ "on",
23
+ ):
24
+ return
25
+
26
+ class _Filter(logging.Filter):
27
+ def filter(self, record: logging.LogRecord) -> bool:
28
+ try:
29
+ msg = record.getMessage()
30
+ except Exception:
31
+ return True
32
+ if "non-text parts in the response" in msg:
33
+ return False
34
+ if "multiple candidates in the response" in msg:
35
+ return False
36
+ return True
37
+
38
+ f = _Filter()
39
+ for name in (
40
+ "google_genai.types",
41
+ "google_genai",
42
+ "google.genai.types",
43
+ ):
44
+ logging.getLogger(name).addFilter(f)
@@ -14,6 +14,7 @@ from typing import Any, Iterable
14
14
 
15
15
  from gemcode.config import GemCodeConfig
16
16
  from gemcode.trust import is_trusted_root
17
+ from gemcode.version import get_version
17
18
 
18
19
 
19
20
  def _is_executable(p: Path) -> bool:
@@ -52,7 +53,9 @@ def format_doctor_lines(cfg: GemCodeConfig) -> list[str]:
52
53
  except Exception as e:
53
54
  lines.append(f" project_root: ERROR {e}")
54
55
  lines.append(f" folder_trusted: {is_trusted_root(cfg.project_root)}")
55
- lines.append(f" GEMCODE_VERSION: {os.environ.get('GEMCODE_VERSION', '(unset)')}")
56
+ lines.append(
57
+ f" gemcode_version: {os.environ.get('GEMCODE_VERSION', get_version())}"
58
+ )
56
59
  return lines
57
60
 
58
61
 
@@ -157,6 +160,7 @@ def format_tools_lines(
157
160
  def slash_help_lines() -> list[str]:
158
161
  return [
159
162
  "Slash commands:",
163
+ " (CLI) gemcode -C DIR Use a project folder as root (recommended vs. ~ )",
160
164
  " (CLI) gemcode login Save or change API key (~/.gemcode/credentials.json)",
161
165
  " /help Show this help",
162
166
  " /status Show current session/model info",
@@ -170,6 +174,7 @@ def slash_help_lines() -> list[str]:
170
174
  " /tools List tool inventory for this config",
171
175
  " /doctor Environment sanity check",
172
176
  " /model Show model routing info",
177
+ " /model use <id> Override model for this REPL session",
173
178
  " /permissions Show permission / HITL settings",
174
179
  " /memory Show persistent memory settings",
175
180
  " /hooks Show post-turn hook configuration",
@@ -81,7 +81,32 @@ async def process_repl_slash(
81
81
  return ReplSlashResult(skip_model_turn=True)
82
82
 
83
83
  if name in ("model", "models"):
84
+ args = (sc.args or "").strip()
85
+ if not args:
86
+ out("\n".join(format_model_lines(cfg)))
87
+ out()
88
+ return ReplSlashResult(skip_model_turn=True)
89
+
90
+ parts = args.split()
91
+ sub = parts[0].lower()
92
+ if sub in ("use", "set") and len(parts) >= 2:
93
+ new_model = " ".join(parts[1:]).strip()
94
+ if not new_model:
95
+ out("Usage: /model use <model-id>")
96
+ out()
97
+ return ReplSlashResult(skip_model_turn=True)
98
+ # Persist override for this session; pick_effective_model() respects this.
99
+ cfg.model = new_model
100
+ setattr(cfg, "model_overridden", True)
101
+ out(f"model: {cfg.model}")
102
+ out("model_overridden: True")
103
+ out("Note: this applies to subsequent turns in this REPL session.")
104
+ out()
105
+ return ReplSlashResult(skip_model_turn=True)
106
+
107
+ # Fallback: show current routing info.
84
108
  out("\n".join(format_model_lines(cfg)))
109
+ out("Tip: /model use <model-id> to override for this session.")
85
110
  out()
86
111
  return ReplSlashResult(skip_model_turn=True)
87
112
 
@@ -8,8 +8,11 @@ import warnings
8
8
  from datetime import datetime
9
9
 
10
10
  from gemcode.capability_routing import apply_capability_routing
11
+ from gemcode.config import load_cli_environment
11
12
  from gemcode.model_routing import pick_effective_model
12
13
  from gemcode.repl_slash import process_repl_slash
14
+ from gemcode.tui.scrollback import format_tool_call_extras
15
+ from gemcode.version import get_version
13
16
 
14
17
 
15
18
  async def run_gemcode_tui(
@@ -26,6 +29,7 @@ async def run_gemcode_tui(
26
29
  ``extra_tools`` matches the runner (e.g. MCP toolsets when ``--mcp``) so
27
30
  ``/tools`` lists the same inventory as the agent.
28
31
  """
32
+ load_cli_environment()
29
33
  session_state = {"id": session_id}
30
34
 
31
35
  from prompt_toolkit.application import Application
@@ -65,7 +69,7 @@ async def run_gemcode_tui(
65
69
  height=D(weight=1),
66
70
  )
67
71
  input_box = TextArea(
68
- prompt="> ",
72
+ prompt=" ",
69
73
  multiline=True,
70
74
  wrap_lines=True,
71
75
  height=D(min=3, max=6, preferred=3),
@@ -175,12 +179,14 @@ async def run_gemcode_tui(
175
179
 
176
180
  # Non-modal permission prompt state. Modal dialogs can corrupt a full-screen TUI.
177
181
  pending_confirm: dict[str, object] = {"future": None, "tool": "", "hint": ""}
182
+ assistant_busy: dict[str, bool] = {"value": False}
183
+ spinner_idx: dict[str, int] = {"value": 0}
178
184
 
179
185
  def _set_input_prompt() -> None:
180
186
  if pending_confirm.get("future") is not None:
181
- input_box.prompt = "perm> "
187
+ input_box.prompt = "perm "
182
188
  else:
183
- input_box.prompt = "> "
189
+ input_box.prompt = " "
184
190
 
185
191
  def _input_help_text():
186
192
  if pending_confirm.get("future") is not None:
@@ -214,6 +220,15 @@ async def run_gemcode_tui(
214
220
  ("class:muted", " "),
215
221
  ("class:muted", "(Esc cancels)"),
216
222
  ]
223
+ if assistant_busy.get("value"):
224
+ frames = ["|", "/", "-", "\\"]
225
+ fr = frames[spinner_idx.get("value", 0) % len(frames)]
226
+ return [
227
+ ("class:muted", " "),
228
+ ("class:pill", f"thinking {fr}"),
229
+ ("class:muted", " "),
230
+ ("class:muted", "Tip: Esc=interrupt"),
231
+ ]
217
232
  return [
218
233
  ("class:muted", " "),
219
234
  ("class:pill", f"🌿 {_git_branch()}" if _git_branch() else "📁 no-git"),
@@ -224,6 +239,18 @@ async def run_gemcode_tui(
224
239
  status.content = FormattedTextControl(_status_text)
225
240
  _set_input_prompt()
226
241
 
242
+ async def _spin_status() -> None:
243
+ frames = ["|", "/", "-", "\\"]
244
+ i = 0
245
+ while assistant_busy.get("value"):
246
+ spinner_idx["value"] = i % len(frames)
247
+ i += 1
248
+ try:
249
+ app.invalidate()
250
+ except Exception:
251
+ pass
252
+ await asyncio.sleep(0.12)
253
+
227
254
  input_help = Window(
228
255
  height=1,
229
256
  dont_extend_height=True,
@@ -272,7 +299,10 @@ async def run_gemcode_tui(
272
299
  return s[: max(0, w - 1)] + "…"
273
300
  return s + (" " * (w - len(s)))
274
301
 
275
- mid_title = "│" + pad(f" GemCode v{os.environ.get('GEMCODE_VERSION', '0.1.0')}", width - 2) + "│"
302
+ mid_title = "│" + pad(
303
+ f" GemCode v{os.environ.get('GEMCODE_VERSION', get_version())}",
304
+ width - 2,
305
+ ) + "│"
276
306
 
277
307
  welcome = f"Welcome back {_uname()}!"
278
308
  bot = [
@@ -516,6 +546,9 @@ async def run_gemcode_tui(
516
546
  apply_capability_routing(cfg, prompt, context="prompt")
517
547
  cfg.model = pick_effective_model(cfg, prompt)
518
548
 
549
+ assistant_busy["value"] = True
550
+ spinner_task = asyncio.create_task(_spin_status())
551
+
519
552
  try:
520
553
  REQUEST_CONFIRMATION_FC = "adk_request_confirmation"
521
554
  # Terminal width for stable box rendering.
@@ -571,7 +604,11 @@ async def run_gemcode_tui(
571
604
  name = getattr(fc, "name", "") or ""
572
605
  if name == REQUEST_CONFIRMATION_FC:
573
606
  continue
574
- _box("tool", [name])
607
+ extra = format_tool_call_extras(fc)
608
+ if extra:
609
+ _box("tool", [name, extra])
610
+ else:
611
+ _box("tool", [name])
575
612
 
576
613
  # Token-budget reset matches invoke.run_turn behavior.
577
614
  state_delta = None
@@ -591,12 +628,20 @@ async def run_gemcode_tui(
591
628
 
592
629
  assistant_started = False
593
630
 
631
+ def _normalize_ws(s: str) -> str:
632
+ # For Gemini, "thinking" and final text can sometimes be identical.
633
+ # Normalize whitespace so we can detect exact duplicates robustly.
634
+ return " ".join((s or "").split()).strip().lower()
635
+
594
636
  while True:
595
637
  # Stream events from ADK runner.
596
638
  events: list = []
597
- # Buffer assistant text for this pass. If a confirmation is requested,
598
- # we discard buffered text to avoid the noisy "rerun with --yes" spiel.
599
- buffered: list[str] = []
639
+ # Buffer assistant text for this pass.
640
+ # Claude differentiates "thinking" from the final response, and we
641
+ # also do that here by routing streamed parts with `part.thought=True`
642
+ # into a separate buffer.
643
+ buffered_thought: list[str] = []
644
+ buffered_final: list[str] = []
600
645
  kwargs = dict(
601
646
  user_id="local",
602
647
  session_id=session_state["id"],
@@ -626,7 +671,10 @@ async def run_gemcode_tui(
626
671
  if not delta:
627
672
  continue
628
673
  assistant_started = True
629
- buffered.append(delta)
674
+ if getattr(part, "thought", None):
675
+ buffered_thought.append(delta)
676
+ else:
677
+ buffered_final.append(delta)
630
678
  except Exception:
631
679
  continue
632
680
 
@@ -637,10 +685,25 @@ async def run_gemcode_tui(
637
685
  # Handle in-TUI tool confirmations (HITL) Claude-style.
638
686
  confirmation_fcs = _get_confirmation_fcs(events)
639
687
  if not confirmation_fcs:
640
- # Now that we know no confirmation is needed, render buffered text.
641
- if buffered:
642
- append_inline("GemCode: ")
643
- await typewrite("".join(buffered))
688
+ # Now that we know no confirmation is needed, render buffered
689
+ # thinking + final response separately.
690
+ thought_text = "".join(buffered_thought)
691
+ final_text = "".join(buffered_final)
692
+ if buffered_thought:
693
+ # If Gemini returns the same content for both "thought" and
694
+ # final text, don't repeat it (Claude typically doesn't).
695
+ if buffered_final and _normalize_ws(thought_text) == _normalize_ws(final_text):
696
+ append_inline("⎿ GemCode (thinking): ")
697
+ await typewrite("(omitted: identical to final response)")
698
+ append("")
699
+ else:
700
+ append_inline("⎿ GemCode (thinking): ")
701
+ await typewrite(thought_text)
702
+ # Ensure visual separation before the final response section.
703
+ append("")
704
+ if buffered_final:
705
+ append_inline("⎿ GemCode: ")
706
+ await typewrite("".join(buffered_final))
644
707
  break
645
708
 
646
709
  interactive_enabled = bool(getattr(cfg, "interactive_permission_ask", False))
@@ -690,8 +753,35 @@ async def run_gemcode_tui(
690
753
  if not assistant_started:
691
754
  append_inline("(no text output)")
692
755
  append("") # newline after assistant turn
756
+ if os.environ.get("GEMCODE_TUI_TURN_FOOTER", "1").lower() in (
757
+ "1",
758
+ "true",
759
+ "yes",
760
+ "on",
761
+ ):
762
+ sid = session_state["id"]
763
+ sid_short = sid[:8] if len(sid) >= 8 else sid
764
+ model = getattr(cfg, "model", "") or ""
765
+ append(f"\033[2m · {model} · session {sid_short}\033[0m")
766
+ if os.environ.get("GEMCODE_TUI_TURN_RULE", "1").lower() in (
767
+ "1",
768
+ "true",
769
+ "yes",
770
+ "on",
771
+ ):
772
+ try:
773
+ cw = app.output.get_size().columns
774
+ except Exception:
775
+ cw = 80
776
+ append("\033[2m" + ("─" * max(40, min(cw - 2, 200))) + "\033[0m")
693
777
  except Exception as e:
694
778
  append(f"GemCode: error: {e}\n")
779
+ finally:
780
+ assistant_busy["value"] = False
781
+ try:
782
+ spinner_task.cancel()
783
+ except Exception:
784
+ pass
695
785
 
696
786
  @kb.add("enter")
697
787
  def _enter(event) -> None:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import json
4
5
  import os
5
6
  import sys
6
7
  from dataclasses import dataclass
@@ -9,8 +10,88 @@ from google.adk.agents.run_config import RunConfig
9
10
  from google.genai import types
10
11
 
11
12
  from gemcode.capability_routing import apply_capability_routing
13
+ from gemcode.config import load_cli_environment
12
14
  from gemcode.model_routing import pick_effective_model
13
15
  from gemcode.repl_slash import process_repl_slash
16
+ from gemcode.version import get_version
17
+ from gemcode.workspace_hints import narrow_workspace_tip
18
+
19
+ _ADK_REQUEST_CONFIRMATION = "adk_request_confirmation"
20
+
21
+
22
+ def format_tool_call_extras(fc) -> str:
23
+ """
24
+ One-line summary of tool arguments for Claude-style ``[tool] name …`` lines.
25
+
26
+ Parses ``FunctionCall.args`` / nested ``originalFunctionCall.args`` when present.
27
+ """
28
+ try:
29
+ raw = getattr(fc, "args", None)
30
+ if raw is None:
31
+ return ""
32
+ if isinstance(raw, str):
33
+ try:
34
+ raw = json.loads(raw)
35
+ except Exception:
36
+ return ""
37
+ if not isinstance(raw, dict):
38
+ return ""
39
+ inner: dict = {}
40
+ orig = raw.get("originalFunctionCall")
41
+ if isinstance(orig, dict):
42
+ a = orig.get("args")
43
+ if isinstance(a, dict):
44
+ inner = a
45
+ elif isinstance(a, str):
46
+ try:
47
+ inner = json.loads(a) if a.strip() else {}
48
+ except Exception:
49
+ inner = {}
50
+ if not inner:
51
+ inner = {
52
+ k: v
53
+ for k, v in raw.items()
54
+ if k not in ("originalFunctionCall", "toolConfirmation")
55
+ }
56
+ if not isinstance(inner, dict) or not inner:
57
+ return ""
58
+ for key in (
59
+ "path",
60
+ "glob_pattern",
61
+ "pattern",
62
+ "command",
63
+ "query",
64
+ "url",
65
+ "file_path",
66
+ "target_file",
67
+ ):
68
+ if key in inner and inner[key] not in (None, ""):
69
+ v = str(inner[key])
70
+ if len(v) > 80:
71
+ v = v[:77] + "..."
72
+ return f"{key}={v}"
73
+ parts: list[str] = []
74
+ for k, v in list(inner.items())[:4]:
75
+ if k in ("originalFunctionCall",):
76
+ continue
77
+ sv = str(v)
78
+ if len(sv) > 40:
79
+ sv = sv[:37] + "..."
80
+ parts.append(f"{k}={sv}")
81
+ return " ".join(parts) if parts else ""
82
+ except Exception:
83
+ return ""
84
+
85
+
86
+ def _events_had_non_confirmation_tools(events: list) -> bool:
87
+ for ev in events:
88
+ try:
89
+ for fc in ev.get_function_calls() or []:
90
+ if getattr(fc, "name", "") != _ADK_REQUEST_CONFIRMATION:
91
+ return True
92
+ except Exception:
93
+ continue
94
+ return False
14
95
 
15
96
 
16
97
  @dataclass(frozen=True)
@@ -72,7 +153,7 @@ def _hr(ch: str = "─") -> str:
72
153
 
73
154
  def _dashboard(cfg) -> str:
74
155
  w = _term_width()
75
- title = f" GemCode v{os.environ.get('GEMCODE_VERSION', '0.1.0')} "
156
+ title = f" GemCode v{os.environ.get('GEMCODE_VERSION', get_version())} "
76
157
  left_w = (w - 4) * 2 // 3
77
158
  right_w = (w - 4) - left_w
78
159
 
@@ -116,6 +197,9 @@ def _dashboard(cfg) -> str:
116
197
  lines.append(
117
198
  "│ " + pad(left[i], left_w) + " │ " + pad(right[i], right_w) + " │"
118
199
  )
200
+ nt = narrow_workspace_tip(getattr(cfg, "project_root"))
201
+ if nt:
202
+ lines.append("│" + pad(f" {nt}", w - 2) + "│")
119
203
  lines.append(box_bot)
120
204
  lines.append("")
121
205
  lines.append(" ↑ GemCode Pro now supports larger contexts · faster streaming")
@@ -134,6 +218,7 @@ async def run_gemcode_scrollback_tui(
134
218
  - Tool calls are shown as a short "internal state" block.
135
219
  - Permission prompts are inline: type y/n at the prompt.
136
220
  """
221
+ load_cli_environment()
137
222
  os.environ["GEMCODE_TUI_ACTIVE"] = "1"
138
223
 
139
224
  ansi = _Ansi(
@@ -179,14 +264,14 @@ async def run_gemcode_scrollback_tui(
179
264
  sys.stdout.flush()
180
265
  await asyncio.sleep(char_delay_ms / 1000.0)
181
266
 
182
- REQUEST_CONFIRMATION_FC = "adk_request_confirmation"
267
+ REQUEST_CONFIRMATION_FC = _ADK_REQUEST_CONFIRMATION
183
268
 
184
269
  def _get_confirmation_fcs(events: list) -> list[types.FunctionCall]:
185
270
  out: list[types.FunctionCall] = []
186
271
  for ev in events:
187
272
  try:
188
273
  for fc in ev.get_function_calls() or []:
189
- if getattr(fc, "name", None) == REQUEST_CONFIRMATION_FC:
274
+ if getattr(fc, "name", None) == _ADK_REQUEST_CONFIRMATION:
190
275
  out.append(fc)
191
276
  except Exception:
192
277
  continue
@@ -212,9 +297,16 @@ async def run_gemcode_scrollback_tui(
212
297
  fcs = []
213
298
  for fc in fcs:
214
299
  name = getattr(fc, "name", "") or ""
215
- if name == REQUEST_CONFIRMATION_FC:
300
+ if name == _ADK_REQUEST_CONFIRMATION:
216
301
  continue
217
- print(f" ⎿ {ansi.blue_tool}[tool]{ansi.reset} {ansi.bold}{name}{ansi.reset}")
302
+ extra = format_tool_call_extras(fc)
303
+ if extra:
304
+ print(
305
+ f" ⎿ {ansi.blue_tool}[tool]{ansi.reset} {ansi.bold}{name}{ansi.reset} "
306
+ f"{ansi.dim}{extra}{ansi.reset}"
307
+ )
308
+ else:
309
+ print(f" ⎿ {ansi.blue_tool}[tool]{ansi.reset} {ansi.bold}{name}{ansi.reset}")
218
310
 
219
311
  run_config = (
220
312
  RunConfig(max_llm_calls=cfg.max_llm_calls)
@@ -254,15 +346,18 @@ async def run_gemcode_scrollback_tui(
254
346
  apply_capability_routing(cfg, prompt, context="prompt")
255
347
  cfg.model = pick_effective_model(cfg, prompt)
256
348
 
257
- # Start streaming assistant output.
258
- sys.stdout.write(f" ⎿ {ansi.bold}GemCode{ansi.reset}: ")
259
- sys.stdout.flush()
260
-
261
349
  current_message = types.Content(role="user", parts=[types.Part(text=prompt)])
262
350
  do_reset = True
351
+ def _normalize_ws(s: str) -> str:
352
+ # Gemini can sometimes return identical content for both "thinking" and
353
+ # final text; normalize whitespace to detect exact duplicates.
354
+ return " ".join((s or "").split()).strip().lower()
263
355
 
264
356
  while True:
265
357
  events: list = []
358
+ assistant_wrote_text = False
359
+ buffered_thought: list[str] = []
360
+ buffered_final: list[str] = []
266
361
  kwargs = dict(
267
362
  user_id="local", session_id=current_session_id, new_message=current_message
268
363
  )
@@ -280,13 +375,42 @@ async def run_gemcode_scrollback_tui(
280
375
  continue
281
376
  for part in ev.content.parts:
282
377
  delta = getattr(part, "text", None)
283
- if delta:
284
- await typewrite(delta)
378
+ if not delta:
379
+ continue
380
+ assistant_wrote_text = True
381
+ if getattr(part, "thought", None):
382
+ buffered_thought.append(delta)
383
+ else:
384
+ buffered_final.append(delta)
285
385
  except Exception:
286
386
  continue
287
387
 
388
+ if not assistant_wrote_text and _events_had_non_confirmation_tools(events):
389
+ await typewrite(
390
+ f"{ansi.dim}(Tools ran without a text reply in this step; "
391
+ f"the run may continue in the background. Ask a follow-up if you need more.){ansi.reset}"
392
+ )
393
+
288
394
  confirmation_fcs = _get_confirmation_fcs(events)
289
395
  if not confirmation_fcs:
396
+ # Render buffered thinking and final response separately.
397
+ thought_text = "".join(buffered_thought)
398
+ final_text = "".join(buffered_final)
399
+ if buffered_thought:
400
+ if buffered_final and _normalize_ws(thought_text) == _normalize_ws(final_text):
401
+ print(f" ⎿ {ansi.dim}{ansi.bold}GemCode{ansi.reset} (thinking): {ansi.reset}(omitted: identical to final response)")
402
+ print("")
403
+ else:
404
+ sys.stdout.write(f" ⎿ {ansi.dim}{ansi.bold}GemCode{ansi.reset} (thinking): ")
405
+ sys.stdout.flush()
406
+ await typewrite(thought_text)
407
+ sys.stdout.write("\n")
408
+ sys.stdout.flush()
409
+
410
+ if buffered_final:
411
+ sys.stdout.write(f" ⎿ {ansi.bold}GemCode{ansi.reset}: ")
412
+ sys.stdout.flush()
413
+ await typewrite(final_text)
290
414
  break
291
415
 
292
416
  interactive_enabled = bool(getattr(cfg, "interactive_permission_ask", False))
@@ -314,9 +438,6 @@ async def run_gemcode_scrollback_tui(
314
438
  f" ⎿ Allow? ({ansi.blue_ok}y{ansi.reset}/{ansi.dim}N{ansi.reset}) "
315
439
  ).strip().lower()
316
440
  ok = ans in ("y", "yes")
317
- # Resume the assistant indent after permission prompt.
318
- sys.stdout.write(f" ⎿ {ansi.bold}GemCode{ansi.reset}: ")
319
- sys.stdout.flush()
320
441
 
321
442
  parts.append(
322
443
  types.Part(
@@ -331,5 +452,25 @@ async def run_gemcode_scrollback_tui(
331
452
  do_reset = False
332
453
 
333
454
  print("")
455
+ if os.environ.get("GEMCODE_TUI_TURN_FOOTER", "1").lower() in (
456
+ "1",
457
+ "true",
458
+ "yes",
459
+ "on",
460
+ ):
461
+ sid = (
462
+ current_session_id[:8]
463
+ if len(current_session_id) >= 8
464
+ else current_session_id
465
+ )
466
+ model = getattr(cfg, "model", "") or ""
467
+ print(f"{ansi.dim} · {model} · session {sid}{ansi.reset}")
468
+ if os.environ.get("GEMCODE_TUI_TURN_RULE", "1").lower() in (
469
+ "1",
470
+ "true",
471
+ "yes",
472
+ "on",
473
+ ):
474
+ print(f"{ansi.dim}{_hr(ch='─')}{ansi.reset}")
334
475
  print("")
335
476
 
@@ -0,0 +1,15 @@
1
+ """Installed package version (PyPI / wheel metadata)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try:
6
+ from importlib.metadata import PackageNotFoundError, version
7
+ except ImportError: # pragma: no cover
8
+ from importlib_metadata import PackageNotFoundError, version
9
+
10
+
11
+ def get_version() -> str:
12
+ try:
13
+ return version("gemcode")
14
+ except PackageNotFoundError:
15
+ return "0.0.0"
@@ -0,0 +1,23 @@
1
+ """UX hints when the project root is unusually broad (e.g. user home)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def project_root_is_user_home(project_root: Path) -> bool:
9
+ try:
10
+ return project_root.resolve() == Path.home().resolve()
11
+ except OSError:
12
+ return False
13
+
14
+
15
+ def narrow_workspace_tip(project_root: Path) -> str | None:
16
+ """
17
+ One-line suggestion when GemCode is anchored at ~ so searches span the whole account.
18
+ """
19
+ if not project_root_is_user_home(project_root):
20
+ return None
21
+ return (
22
+ "Tip: narrow the workspace — restart with: gemcode -C /path/to/your/repo"
23
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.2
3
+ Version: 0.3.7
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -21,6 +21,7 @@ src/gemcode/invoke.py
21
21
  src/gemcode/kairos_daemon.py
22
22
  src/gemcode/limits.py
23
23
  src/gemcode/live_audio_engine.py
24
+ src/gemcode/logging_config.py
24
25
  src/gemcode/mcp_loader.py
25
26
  src/gemcode/modality_tools.py
26
27
  src/gemcode/model_errors.py
@@ -37,7 +38,9 @@ src/gemcode/tool_prompt_manifest.py
37
38
  src/gemcode/tool_registry.py
38
39
  src/gemcode/tools_inspector.py
39
40
  src/gemcode/trust.py
41
+ src/gemcode/version.py
40
42
  src/gemcode/vertex.py
43
+ src/gemcode/workspace_hints.py
41
44
  src/gemcode.egg-info/PKG-INFO
42
45
  src/gemcode.egg-info/SOURCES.txt
43
46
  src/gemcode.egg-info/dependency_links.txt
@@ -71,6 +74,7 @@ src/gemcode/tui/scrollback.py
71
74
  src/gemcode/web/__init__.py
72
75
  src/gemcode/web/claude_sse_adapter.py
73
76
  src/gemcode/web/terminal_repl.py
77
+ tests/test_agent_instruction.py
74
78
  tests/test_autocompact.py
75
79
  tests/test_capability_routing.py
76
80
  tests/test_claude_web_adapter_sse.py
@@ -95,4 +99,5 @@ tests/test_thinking_config.py
95
99
  tests/test_token_budget.py
96
100
  tests/test_tool_context_circulation.py
97
101
  tests/test_tools.py
98
- tests/test_tools_inspector.py
102
+ tests/test_tools_inspector.py
103
+ tests/test_workspace_hints.py
@@ -0,0 +1,12 @@
1
+ from pathlib import Path
2
+
3
+ from gemcode.agent import build_instruction
4
+ from gemcode.config import GemCodeConfig
5
+
6
+
7
+ def test_instruction_includes_runtime_facts(tmp_path: Path) -> None:
8
+ cfg = GemCodeConfig(project_root=tmp_path, model="gemini-2.5-flash")
9
+ text = build_instruction(cfg)
10
+ assert str(tmp_path.resolve()) in text
11
+ assert "gemini-2.5-flash" in text
12
+ assert "GEMCODE_MODEL" in text
@@ -0,0 +1,16 @@
1
+ from pathlib import Path
2
+
3
+ from gemcode import workspace_hints
4
+
5
+
6
+ def test_narrow_tip_none_for_non_home(tmp_path: Path) -> None:
7
+ assert workspace_hints.narrow_workspace_tip(tmp_path) is None
8
+ assert workspace_hints.project_root_is_user_home(tmp_path) is False
9
+
10
+
11
+ def test_narrow_tip_for_home_directory() -> None:
12
+ home = Path.home()
13
+ assert workspace_hints.project_root_is_user_home(home) is True
14
+ tip = workspace_hints.narrow_workspace_tip(home)
15
+ assert tip is not None
16
+ assert "gemcode -C" in tip
@@ -1,3 +0,0 @@
1
- """GemCode: Gemini + ADK coding agent."""
2
-
3
- __version__ = "0.1.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes