gemcode 0.3.1__tar.gz → 0.3.4__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.1/src/gemcode.egg-info → gemcode-0.3.4}/PKG-INFO +1 -1
- {gemcode-0.3.1 → gemcode-0.3.4}/pyproject.toml +1 -1
- gemcode-0.3.4/src/gemcode/__init__.py +5 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/agent.py +26 -4
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/config.py +6 -0
- gemcode-0.3.4/src/gemcode/logging_config.py +44 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/repl_commands.py +5 -1
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tool_prompt_manifest.py +1 -1
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tools/shell.py +20 -9
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tui/app.py +38 -6
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tui/scrollback.py +125 -5
- gemcode-0.3.4/src/gemcode/version.py +15 -0
- gemcode-0.3.4/src/gemcode/workspace_hints.py +23 -0
- {gemcode-0.3.1 → gemcode-0.3.4/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode.egg-info/SOURCES.txt +6 -1
- gemcode-0.3.4/tests/test_agent_instruction.py +13 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_tools.py +18 -0
- gemcode-0.3.4/tests/test_workspace_hints.py +16 -0
- gemcode-0.3.1/src/gemcode/__init__.py +0 -3
- {gemcode-0.3.1 → gemcode-0.3.4}/LICENSE +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/MANIFEST.in +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/README.md +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/setup.cfg +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_credentials.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_paths.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_permissions.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.1 → gemcode-0.3.4}/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. Shell: `gemcode list-models` lists candidate ids.
|
|
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.
|
|
@@ -74,11 +91,15 @@ You operate only inside the user's project directory (current working directory)
|
|
|
74
91
|
- **`run_command` rules (critical):**
|
|
75
92
|
- `command` must be a **single executable basename** (e.g. `npm`, `npx`, `mkdir`) — **not** `bash`, `sh`, or `cd foo && ...`.
|
|
76
93
|
- Pass argv as `args` (list). To run a command **inside** a subfolder (e.g. Next app in `testing/`), set **`cwd_subdir`** to that relative path (e.g. `"testing"`) and run `npm run dev` there — **never** simulate `cd` with `bash`.
|
|
77
|
-
- **Scaffolding** (`create-next-app`, etc.): many CLIs require non-interactive mode — pass **`
|
|
94
|
+
- **Scaffolding** (`create-next-app`, etc.): many CLIs require non-interactive mode — pass **`extra_env_keys`** / **`extra_env_values`** as parallel lists (e.g. `["CI"]` and `["1"]`) and/or flags supported by that tool (`--yes` where documented).
|
|
78
95
|
- **Dev servers** (`npm run dev`, `vite`, etc.) run until stopped: use **`background=True`** so the process detaches; otherwise the tool may time out. You cannot open a *new OS terminal window* from here—background start is the supported way to keep running.
|
|
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",
|
|
@@ -120,7 +120,7 @@ You may call tools as follows:
|
|
|
120
120
|
Notes:
|
|
121
121
|
- Prefer `python -m pip ...` (or `python3 -m pip ...`) so installs stay in the active virtualenv.
|
|
122
122
|
- Do not assume sudo/system package manager access.
|
|
123
|
-
- `run_command` supports `cwd_subdir` (relative path under the project) instead of `cd`/`bash`; use `
|
|
123
|
+
- `run_command` supports `cwd_subdir` (relative path under the project) instead of `cd`/`bash`; use parallel `extra_env_keys` / `extra_env_values` (e.g. ["CI"] and ["1"]) for non-interactive installers; use `background=true` for long-running dev servers.
|
|
124
124
|
|
|
125
125
|
Optional capability tools:
|
|
126
126
|
- Deep research built-ins are {'ON' if deep_research_on else 'OFF'}.
|
|
@@ -7,7 +7,6 @@ import re
|
|
|
7
7
|
import shutil
|
|
8
8
|
import subprocess
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
11
10
|
|
|
12
11
|
from google.adk.tools.tool_context import ToolContext
|
|
13
12
|
|
|
@@ -18,12 +17,12 @@ from gemcode.tools.shell_gate import consume_confirmed_shell_if_matches
|
|
|
18
17
|
from gemcode.trust import is_trusted_root
|
|
19
18
|
|
|
20
19
|
|
|
21
|
-
def _merge_child_env(
|
|
22
|
-
"""Merge
|
|
20
|
+
def _merge_child_env(keys: list[str], values: list[str]) -> dict[str, str]:
|
|
21
|
+
"""Merge parallel key/value lists into os.environ (Gemini API rejects dict-typed tool params)."""
|
|
23
22
|
out = {**os.environ}
|
|
24
|
-
if not
|
|
23
|
+
if not keys and not values:
|
|
25
24
|
return out
|
|
26
|
-
for k, v in
|
|
25
|
+
for k, v in zip(keys, values):
|
|
27
26
|
if not isinstance(k, str) or not isinstance(v, str):
|
|
28
27
|
continue
|
|
29
28
|
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", k):
|
|
@@ -45,7 +44,8 @@ def make_run_command(cfg: GemCodeConfig):
|
|
|
45
44
|
tool_context: ToolContext | None = None,
|
|
46
45
|
cwd_subdir: str = ".",
|
|
47
46
|
background: bool = False,
|
|
48
|
-
|
|
47
|
+
extra_env_keys: list[str] | None = None,
|
|
48
|
+
extra_env_values: list[str] | None = None,
|
|
49
49
|
) -> dict:
|
|
50
50
|
"""
|
|
51
51
|
Run an allowlisted executable with arguments.
|
|
@@ -57,8 +57,9 @@ def make_run_command(cfg: GemCodeConfig):
|
|
|
57
57
|
For long-running servers (e.g. `npm run dev`), set `background=True` to start
|
|
58
58
|
a detached process and return its PID (non-interactive; no TTY for the child).
|
|
59
59
|
|
|
60
|
-
Optional `
|
|
61
|
-
|
|
60
|
+
Optional `extra_env_keys` / `extra_env_values` are parallel lists (same length)
|
|
61
|
+
merged into the child environment (e.g. keys ["CI"], values ["1"] for
|
|
62
|
+
non-interactive scaffolding tools). Omit both to use the default environment.
|
|
62
63
|
"""
|
|
63
64
|
if not trusted:
|
|
64
65
|
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
@@ -111,7 +112,17 @@ def make_run_command(cfg: GemCodeConfig):
|
|
|
111
112
|
)
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
ek = list(extra_env_keys or [])
|
|
116
|
+
ev = list(extra_env_values or [])
|
|
117
|
+
if len(ek) != len(ev):
|
|
118
|
+
return {
|
|
119
|
+
"error": (
|
|
120
|
+
"extra_env_keys and extra_env_values must be parallel lists of the same length "
|
|
121
|
+
"(one value per key)."
|
|
122
|
+
),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
child_env = _merge_child_env(ek, ev)
|
|
115
126
|
|
|
116
127
|
if background:
|
|
117
128
|
try:
|
|
@@ -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),
|
|
@@ -178,9 +182,9 @@ async def run_gemcode_tui(
|
|
|
178
182
|
|
|
179
183
|
def _set_input_prompt() -> None:
|
|
180
184
|
if pending_confirm.get("future") is not None:
|
|
181
|
-
input_box.prompt = "perm
|
|
185
|
+
input_box.prompt = "⎿ perm "
|
|
182
186
|
else:
|
|
183
|
-
input_box.prompt = "
|
|
187
|
+
input_box.prompt = "❯ "
|
|
184
188
|
|
|
185
189
|
def _input_help_text():
|
|
186
190
|
if pending_confirm.get("future") is not None:
|
|
@@ -272,7 +276,10 @@ async def run_gemcode_tui(
|
|
|
272
276
|
return s[: max(0, w - 1)] + "…"
|
|
273
277
|
return s + (" " * (w - len(s)))
|
|
274
278
|
|
|
275
|
-
mid_title = "│" + pad(
|
|
279
|
+
mid_title = "│" + pad(
|
|
280
|
+
f" GemCode v{os.environ.get('GEMCODE_VERSION', get_version())}",
|
|
281
|
+
width - 2,
|
|
282
|
+
) + "│"
|
|
276
283
|
|
|
277
284
|
welcome = f"Welcome back {_uname()}!"
|
|
278
285
|
bot = [
|
|
@@ -571,7 +578,11 @@ async def run_gemcode_tui(
|
|
|
571
578
|
name = getattr(fc, "name", "") or ""
|
|
572
579
|
if name == REQUEST_CONFIRMATION_FC:
|
|
573
580
|
continue
|
|
574
|
-
|
|
581
|
+
extra = format_tool_call_extras(fc)
|
|
582
|
+
if extra:
|
|
583
|
+
_box("tool", [name, extra])
|
|
584
|
+
else:
|
|
585
|
+
_box("tool", [name])
|
|
575
586
|
|
|
576
587
|
# Token-budget reset matches invoke.run_turn behavior.
|
|
577
588
|
state_delta = None
|
|
@@ -639,7 +650,7 @@ async def run_gemcode_tui(
|
|
|
639
650
|
if not confirmation_fcs:
|
|
640
651
|
# Now that we know no confirmation is needed, render buffered text.
|
|
641
652
|
if buffered:
|
|
642
|
-
append_inline("GemCode: ")
|
|
653
|
+
append_inline("⎿ GemCode: ")
|
|
643
654
|
await typewrite("".join(buffered))
|
|
644
655
|
break
|
|
645
656
|
|
|
@@ -690,6 +701,27 @@ async def run_gemcode_tui(
|
|
|
690
701
|
if not assistant_started:
|
|
691
702
|
append_inline("(no text output)")
|
|
692
703
|
append("") # newline after assistant turn
|
|
704
|
+
if os.environ.get("GEMCODE_TUI_TURN_FOOTER", "1").lower() in (
|
|
705
|
+
"1",
|
|
706
|
+
"true",
|
|
707
|
+
"yes",
|
|
708
|
+
"on",
|
|
709
|
+
):
|
|
710
|
+
sid = session_state["id"]
|
|
711
|
+
sid_short = sid[:8] if len(sid) >= 8 else sid
|
|
712
|
+
model = getattr(cfg, "model", "") or ""
|
|
713
|
+
append(f"\033[2m · {model} · session {sid_short}\033[0m")
|
|
714
|
+
if os.environ.get("GEMCODE_TUI_TURN_RULE", "1").lower() in (
|
|
715
|
+
"1",
|
|
716
|
+
"true",
|
|
717
|
+
"yes",
|
|
718
|
+
"on",
|
|
719
|
+
):
|
|
720
|
+
try:
|
|
721
|
+
cw = app.output.get_size().columns
|
|
722
|
+
except Exception:
|
|
723
|
+
cw = 80
|
|
724
|
+
append("\033[2m" + ("─" * max(40, min(cw - 2, 200))) + "\033[0m")
|
|
693
725
|
except Exception as e:
|
|
694
726
|
append(f"GemCode: error: {e}\n")
|
|
695
727
|
|
|
@@ -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)
|
|
@@ -263,6 +355,7 @@ async def run_gemcode_scrollback_tui(
|
|
|
263
355
|
|
|
264
356
|
while True:
|
|
265
357
|
events: list = []
|
|
358
|
+
assistant_wrote_text = False
|
|
266
359
|
kwargs = dict(
|
|
267
360
|
user_id="local", session_id=current_session_id, new_message=current_message
|
|
268
361
|
)
|
|
@@ -281,10 +374,17 @@ async def run_gemcode_scrollback_tui(
|
|
|
281
374
|
for part in ev.content.parts:
|
|
282
375
|
delta = getattr(part, "text", None)
|
|
283
376
|
if delta:
|
|
377
|
+
assistant_wrote_text = True
|
|
284
378
|
await typewrite(delta)
|
|
285
379
|
except Exception:
|
|
286
380
|
continue
|
|
287
381
|
|
|
382
|
+
if not assistant_wrote_text and _events_had_non_confirmation_tools(events):
|
|
383
|
+
await typewrite(
|
|
384
|
+
f"{ansi.dim}(Tools ran without a text reply in this step; "
|
|
385
|
+
f"the run may continue in the background. Ask a follow-up if you need more.){ansi.reset}"
|
|
386
|
+
)
|
|
387
|
+
|
|
288
388
|
confirmation_fcs = _get_confirmation_fcs(events)
|
|
289
389
|
if not confirmation_fcs:
|
|
290
390
|
break
|
|
@@ -331,5 +431,25 @@ async def run_gemcode_scrollback_tui(
|
|
|
331
431
|
do_reset = False
|
|
332
432
|
|
|
333
433
|
print("")
|
|
434
|
+
if os.environ.get("GEMCODE_TUI_TURN_FOOTER", "1").lower() in (
|
|
435
|
+
"1",
|
|
436
|
+
"true",
|
|
437
|
+
"yes",
|
|
438
|
+
"on",
|
|
439
|
+
):
|
|
440
|
+
sid = (
|
|
441
|
+
current_session_id[:8]
|
|
442
|
+
if len(current_session_id) >= 8
|
|
443
|
+
else current_session_id
|
|
444
|
+
)
|
|
445
|
+
model = getattr(cfg, "model", "") or ""
|
|
446
|
+
print(f"{ansi.dim} · {model} · session {sid}{ansi.reset}")
|
|
447
|
+
if os.environ.get("GEMCODE_TUI_TURN_RULE", "1").lower() in (
|
|
448
|
+
"1",
|
|
449
|
+
"true",
|
|
450
|
+
"yes",
|
|
451
|
+
"on",
|
|
452
|
+
):
|
|
453
|
+
print(f"{ansi.dim}{_hr(ch='─')}{ansi.reset}")
|
|
334
454
|
print("")
|
|
335
455
|
|
|
@@ -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,13 @@
|
|
|
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 "list-models" in text
|
|
13
|
+
assert "GEMCODE_MODEL" in text
|
|
@@ -118,6 +118,24 @@ def test_run_command_background_returns_pid(tmp_path: Path, monkeypatch) -> None
|
|
|
118
118
|
assert isinstance(out.get("pid"), int)
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
def test_run_command_extra_env_merges(tmp_path: Path, monkeypatch) -> None:
|
|
122
|
+
monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
|
|
123
|
+
trust_root(tmp_path, trusted=True)
|
|
124
|
+
cfg = GemCodeConfig(project_root=tmp_path)
|
|
125
|
+
run_command = make_run_command(cfg)
|
|
126
|
+
ctx = MagicMock()
|
|
127
|
+
ctx.state = {HITL_STICKY_SESSION_KEY: True}
|
|
128
|
+
out = run_command(
|
|
129
|
+
"python3",
|
|
130
|
+
["-c", "import os; print(os.environ.get('GEMCODE_TEST_EXTRA', ''))"],
|
|
131
|
+
extra_env_keys=["GEMCODE_TEST_EXTRA"],
|
|
132
|
+
extra_env_values=["ok"],
|
|
133
|
+
tool_context=ctx,
|
|
134
|
+
)
|
|
135
|
+
assert out.get("exit_code") == 0
|
|
136
|
+
assert "ok" in (out.get("stdout") or "")
|
|
137
|
+
|
|
138
|
+
|
|
121
139
|
def test_delete_file(tmp_path: Path, monkeypatch) -> None:
|
|
122
140
|
monkeypatch.setenv("GEMCODE_HOME", str(tmp_path / ".gemstate"))
|
|
123
141
|
trust_root(tmp_path, trusted=True)
|
|
@@ -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
|