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.
- {gemcode-0.3.2/src/gemcode.egg-info → gemcode-0.3.7}/PKG-INFO +1 -1
- {gemcode-0.3.2 → gemcode-0.3.7}/pyproject.toml +1 -1
- gemcode-0.3.7/src/gemcode/__init__.py +5 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/agent.py +25 -3
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/config.py +6 -0
- gemcode-0.3.7/src/gemcode/logging_config.py +44 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/repl_commands.py +6 -1
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/repl_slash.py +25 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tui/app.py +103 -13
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tui/scrollback.py +155 -14
- gemcode-0.3.7/src/gemcode/version.py +15 -0
- gemcode-0.3.7/src/gemcode/workspace_hints.py +23 -0
- {gemcode-0.3.2 → gemcode-0.3.7/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode.egg-info/SOURCES.txt +6 -1
- gemcode-0.3.7/tests/test_agent_instruction.py +12 -0
- gemcode-0.3.7/tests/test_workspace_hints.py +16 -0
- gemcode-0.3.2/src/gemcode/__init__.py +0 -3
- {gemcode-0.3.2 → gemcode-0.3.7}/LICENSE +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/MANIFEST.in +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/README.md +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/setup.cfg +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_credentials.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_paths.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_permissions.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_tools.py +0 -0
- {gemcode-0.3.2 → gemcode-0.3.7}/tests/test_tools_inspector.py +0 -0
|
@@ -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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
598
|
-
#
|
|
599
|
-
|
|
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
|
-
|
|
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
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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',
|
|
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 =
|
|
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) ==
|
|
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 ==
|
|
300
|
+
if name == _ADK_REQUEST_CONFIRMATION:
|
|
216
301
|
continue
|
|
217
|
-
|
|
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
|
-
|
|
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
|
+
)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|