gemcode 0.3.12__tar.gz → 0.3.15__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.12/src/gemcode.egg-info → gemcode-0.3.15}/PKG-INFO +2 -4
- {gemcode-0.3.12 → gemcode-0.3.15}/pyproject.toml +2 -2
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/cli.py +12 -32
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tui/scrollback.py +16 -88
- gemcode-0.3.15/src/gemcode/tui/welcome_banner.py +127 -0
- gemcode-0.3.15/src/gemcode/tui/welcome_rich.py +135 -0
- {gemcode-0.3.12 → gemcode-0.3.15/src/gemcode.egg-info}/PKG-INFO +2 -4
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode.egg-info/SOURCES.txt +2 -2
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode.egg-info/requires.txt +1 -4
- gemcode-0.3.12/src/gemcode/tui/app.py +0 -968
- gemcode-0.3.12/src/gemcode/tui/startup_screen.py +0 -196
- {gemcode-0.3.12 → gemcode-0.3.15}/LICENSE +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/MANIFEST.in +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/README.md +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/setup.cfg +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/agent.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/config.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/version.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_credentials.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_paths.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_permissions.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_tools.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_workspace_hints.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemcode
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.15
|
|
4
4
|
Summary: Local-first coding agent on Google Gemini + ADK
|
|
5
5
|
Author: GemCode Contributors
|
|
6
6
|
License: Apache License
|
|
@@ -178,14 +178,12 @@ License-File: LICENSE
|
|
|
178
178
|
Requires-Dist: google-adk>=1.0.0
|
|
179
179
|
Requires-Dist: google-genai>=1.0.0
|
|
180
180
|
Requires-Dist: python-dotenv>=1.0.0
|
|
181
|
+
Requires-Dist: rich>=13.0.0
|
|
181
182
|
Provides-Extra: dev
|
|
182
183
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
183
184
|
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
|
|
184
185
|
Provides-Extra: mcp
|
|
185
186
|
Requires-Dist: mcp>=1.0.0; extra == "mcp"
|
|
186
|
-
Provides-Extra: tui
|
|
187
|
-
Requires-Dist: prompt_toolkit>=3.0.0; extra == "tui"
|
|
188
|
-
Requires-Dist: rich>=13.0.0; extra == "tui"
|
|
189
187
|
Dynamic: license-file
|
|
190
188
|
|
|
191
189
|
# GemCode
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gemcode"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.15"
|
|
8
8
|
description = "Local-first coding agent on Google Gemini + ADK"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -30,6 +30,7 @@ dependencies = [
|
|
|
30
30
|
"google-adk>=1.0.0",
|
|
31
31
|
"google-genai>=1.0.0",
|
|
32
32
|
"python-dotenv>=1.0.0",
|
|
33
|
+
"rich>=13.0.0",
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
[project.urls]
|
|
@@ -40,7 +41,6 @@ Issues = "https://github.com/spiderdev27/GemCode/issues"
|
|
|
40
41
|
[project.optional-dependencies]
|
|
41
42
|
dev = ["pytest>=8.0.0", "pytest-asyncio>=0.24.0"]
|
|
42
43
|
mcp = ["mcp>=1.0.0"]
|
|
43
|
-
tui = ["prompt_toolkit>=3.0.0", "rich>=13.0.0"]
|
|
44
44
|
|
|
45
45
|
[project.scripts]
|
|
46
46
|
gemcode = "gemcode.cli:main"
|
|
@@ -183,10 +183,7 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
183
183
|
_maybe_prompt_google_api_key()
|
|
184
184
|
require_google_api_key()
|
|
185
185
|
_initialize_gemcode_project(cfg)
|
|
186
|
-
|
|
187
|
-
# Show beautiful startup screen before TUI loads
|
|
188
|
-
from gemcode.tui.startup_screen import print_startup_screen
|
|
189
|
-
print_startup_screen()
|
|
186
|
+
|
|
190
187
|
extra: list = []
|
|
191
188
|
if use_mcp:
|
|
192
189
|
from gemcode.mcp_loader import load_mcp_toolsets
|
|
@@ -229,11 +226,10 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
229
226
|
except EOFError:
|
|
230
227
|
pass
|
|
231
228
|
|
|
232
|
-
# Optional
|
|
229
|
+
# Optional terminal UI: single scrollback-style REPL (terminal history, no alt-screen app).
|
|
233
230
|
tui_enabled = os.environ.get("GEMCODE_TUI", "1").lower() in ("1", "true", "yes", "on")
|
|
234
231
|
if tui_enabled:
|
|
235
232
|
term = (os.environ.get("TERM") or "").strip().lower()
|
|
236
|
-
# Guardrails: Prompt Toolkit needs a real interactive terminal.
|
|
237
233
|
if not sys.stdin.isatty() or not sys.stdout.isatty() or term in ("", "dumb", "unknown"):
|
|
238
234
|
print(
|
|
239
235
|
f"[tui] disabled (stdin/stdout isatty={sys.stdin.isatty()}/{sys.stdout.isatty()}, TERM={term or '<unset>'}); using plain REPL",
|
|
@@ -241,35 +237,19 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
241
237
|
)
|
|
242
238
|
else:
|
|
243
239
|
try:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
runner=runner,
|
|
253
|
-
session_id=session_id,
|
|
254
|
-
extra_tools=extra or None,
|
|
255
|
-
)
|
|
256
|
-
else:
|
|
257
|
-
from gemcode.tui.app import run_gemcode_tui
|
|
258
|
-
|
|
259
|
-
await run_gemcode_tui(
|
|
260
|
-
cfg=cfg,
|
|
261
|
-
runner=runner,
|
|
262
|
-
session_id=session_id,
|
|
263
|
-
extra_tools=extra or None,
|
|
264
|
-
)
|
|
240
|
+
from gemcode.tui.scrollback import run_gemcode_scrollback_tui
|
|
241
|
+
|
|
242
|
+
await run_gemcode_scrollback_tui(
|
|
243
|
+
cfg=cfg,
|
|
244
|
+
runner=runner,
|
|
245
|
+
session_id=session_id,
|
|
246
|
+
extra_tools=extra or None,
|
|
247
|
+
)
|
|
265
248
|
return
|
|
266
249
|
except Exception as e:
|
|
267
|
-
# Dependency missing or terminal doesn't support full-screen.
|
|
268
|
-
# Print one line so users know how to fix it.
|
|
269
250
|
print(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
file=sys.stderr,
|
|
251
|
+
f"[tui] failed to start: {type(e).__name__}: {e} (falling back to plain REPL).",
|
|
252
|
+
file=sys.stderr,
|
|
273
253
|
)
|
|
274
254
|
|
|
275
255
|
print(
|
|
@@ -8,13 +8,13 @@ from dataclasses import dataclass
|
|
|
8
8
|
|
|
9
9
|
from google.adk.agents.run_config import RunConfig
|
|
10
10
|
from google.genai import types
|
|
11
|
+
from rich.console import Console
|
|
11
12
|
|
|
12
13
|
from gemcode.capability_routing import apply_capability_routing
|
|
13
14
|
from gemcode.config import load_cli_environment
|
|
14
15
|
from gemcode.model_routing import pick_effective_model
|
|
15
16
|
from gemcode.repl_slash import process_repl_slash
|
|
16
|
-
from gemcode.
|
|
17
|
-
from gemcode.workspace_hints import narrow_workspace_tip
|
|
17
|
+
from gemcode.tui.welcome_rich import print_shortcuts_hint, print_welcome_dashboard
|
|
18
18
|
|
|
19
19
|
_ADK_REQUEST_CONFIRMATION = "adk_request_confirmation"
|
|
20
20
|
|
|
@@ -117,28 +117,24 @@ class _Ansi:
|
|
|
117
117
|
|
|
118
118
|
@property
|
|
119
119
|
def blue(self) -> str:
|
|
120
|
-
#
|
|
121
|
-
return self.esc("38;5;
|
|
120
|
+
# ANSI 256-color bright-ish blue
|
|
121
|
+
return self.esc("38;5;75")
|
|
122
122
|
|
|
123
123
|
@property
|
|
124
124
|
def blue2(self) -> str:
|
|
125
|
-
|
|
126
|
-
return self.esc("38;5;209")
|
|
125
|
+
return self.esc("38;5;33")
|
|
127
126
|
|
|
128
127
|
@property
|
|
129
128
|
def blue_ok(self) -> str:
|
|
130
|
-
|
|
131
|
-
return self.esc("38;5;150")
|
|
129
|
+
return self.esc("38;5;81")
|
|
132
130
|
|
|
133
131
|
@property
|
|
134
132
|
def blue_warn(self) -> str:
|
|
135
|
-
|
|
136
|
-
return self.esc("38;5;221")
|
|
133
|
+
return self.esc("38;5;39")
|
|
137
134
|
|
|
138
135
|
@property
|
|
139
136
|
def blue_tool(self) -> str:
|
|
140
|
-
|
|
141
|
-
return self.esc("38;5;180")
|
|
137
|
+
return self.esc("38;5;69")
|
|
142
138
|
|
|
143
139
|
|
|
144
140
|
def _term_width(default: int = 100) -> int:
|
|
@@ -154,64 +150,6 @@ def _hr(ch: str = "─") -> str:
|
|
|
154
150
|
return ch * _term_width()
|
|
155
151
|
|
|
156
152
|
|
|
157
|
-
def _dashboard(cfg) -> str:
|
|
158
|
-
w = _term_width()
|
|
159
|
-
title = f" GemCode v{os.environ.get('GEMCODE_VERSION', get_version())} "
|
|
160
|
-
left_w = (w - 4) * 2 // 3
|
|
161
|
-
right_w = (w - 4) - left_w
|
|
162
|
-
|
|
163
|
-
def pad(s: str, ww: int) -> str:
|
|
164
|
-
s = s.replace("\n", " ")
|
|
165
|
-
if len(s) > ww:
|
|
166
|
-
return s[: ww - 1] + "…"
|
|
167
|
-
return s + (" " * (ww - len(s)))
|
|
168
|
-
|
|
169
|
-
user = (os.environ.get("USER") or os.environ.get("LOGNAME") or "there").strip()
|
|
170
|
-
model = getattr(cfg, "model", "") or ""
|
|
171
|
-
root = str(getattr(cfg, "project_root", "") or "")
|
|
172
|
-
|
|
173
|
-
box_top = "╭" + ("─" * (w - 2)) + "╮"
|
|
174
|
-
box_bot = "╰" + ("─" * (w - 2)) + "╯"
|
|
175
|
-
lines: list[str] = [box_top]
|
|
176
|
-
lines.append("│" + pad(title, w - 2) + "│")
|
|
177
|
-
lines.append("│" + (" " * (w - 2)) + "│")
|
|
178
|
-
left = [
|
|
179
|
-
"",
|
|
180
|
-
f"Welcome back {user}!",
|
|
181
|
-
"",
|
|
182
|
-
" ▄▄▄▄▄▄▄",
|
|
183
|
-
" ▐█████████▌",
|
|
184
|
-
" ▐█████████▌",
|
|
185
|
-
" ▀▀▀▀▀▀▀",
|
|
186
|
-
"",
|
|
187
|
-
f"{model or 'Gemini 2.0 Flash'} · Local session",
|
|
188
|
-
root,
|
|
189
|
-
]
|
|
190
|
-
right = [
|
|
191
|
-
"Tips for getting started",
|
|
192
|
-
"First run creates .gemcode/ (trust + API key)",
|
|
193
|
-
"Use /help to see available commands",
|
|
194
|
-
"",
|
|
195
|
-
"Recent activity",
|
|
196
|
-
"No recent activity",
|
|
197
|
-
]
|
|
198
|
-
h = max(len(left), len(right))
|
|
199
|
-
left += [""] * (h - len(left))
|
|
200
|
-
right += [""] * (h - len(right))
|
|
201
|
-
for i in range(h):
|
|
202
|
-
lines.append(
|
|
203
|
-
"│ " + pad(left[i], left_w) + " │ " + pad(right[i], right_w) + " │"
|
|
204
|
-
)
|
|
205
|
-
nt = narrow_workspace_tip(getattr(cfg, "project_root"))
|
|
206
|
-
if nt:
|
|
207
|
-
lines.append("│" + pad(f" {nt}", w - 2) + "│")
|
|
208
|
-
lines.append(box_bot)
|
|
209
|
-
lines.append("")
|
|
210
|
-
lines.append(" ↑ GemCode now supports larger contexts · faster streaming · better tools")
|
|
211
|
-
lines.append("")
|
|
212
|
-
return "\n".join(lines)
|
|
213
|
-
|
|
214
|
-
|
|
215
153
|
async def run_gemcode_scrollback_tui(
|
|
216
154
|
*, cfg, runner, session_id: str, extra_tools=None
|
|
217
155
|
) -> None:
|
|
@@ -234,25 +172,15 @@ async def run_gemcode_scrollback_tui(
|
|
|
234
172
|
)
|
|
235
173
|
)
|
|
236
174
|
|
|
175
|
+
console = Console(
|
|
176
|
+
width=_term_width(),
|
|
177
|
+
force_terminal=bool(sys.stdout.isatty()),
|
|
178
|
+
no_color=not ansi.enabled,
|
|
179
|
+
highlight=False,
|
|
180
|
+
)
|
|
237
181
|
if os.environ.get("GEMCODE_TUI_SHOW_DASHBOARD", "1").lower() in ("1", "true", "yes", "on"):
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
# Color title + the ASCII mark.
|
|
241
|
-
lines = dash.splitlines()
|
|
242
|
-
if len(lines) >= 2:
|
|
243
|
-
lines[1] = (
|
|
244
|
-
lines[1]
|
|
245
|
-
.replace("GemCode", f"{ansi.blue}{ansi.bold}GemCode{ansi.reset}")
|
|
246
|
-
.replace("v", f"{ansi.dim}v{ansi.reset}")
|
|
247
|
-
)
|
|
248
|
-
for i, ln in enumerate(lines):
|
|
249
|
-
if "▐▛███▜▌" in ln or "▝▜█████▛▘" in ln or "▘▘ ▝▝" in ln:
|
|
250
|
-
lines[i] = f"{ansi.blue2}{ln}{ansi.reset}"
|
|
251
|
-
dash = "\n".join(lines)
|
|
252
|
-
print(dash)
|
|
253
|
-
|
|
254
|
-
print(f"{ansi.dim} ? for shortcuts{ansi.reset}")
|
|
255
|
-
print(f"{ansi.dim} (Tip: set GEMCODE_TUI_STYLE=full for spinner/status bar){ansi.reset}")
|
|
182
|
+
print_welcome_dashboard(cfg, console=console)
|
|
183
|
+
print_shortcuts_hint(console=console)
|
|
256
184
|
print("")
|
|
257
185
|
|
|
258
186
|
char_delay_ms = int(os.environ.get("GEMCODE_TUI_CHAR_DELAY_MS", "0") or "0")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""ASCII welcome banner + provider box (scrollback + full-screen TUI)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from gemcode.version import get_version
|
|
9
|
+
from gemcode.vertex import vertex_env_active
|
|
10
|
+
from gemcode.workspace_hints import narrow_workspace_tip
|
|
11
|
+
|
|
12
|
+
_GEM_LINES = (
|
|
13
|
+
r" ██████╗ ███████╗███╗ ███╗",
|
|
14
|
+
r" ██╔════╝ ██╔════╝████╗ ████║",
|
|
15
|
+
r" ██║ ███╗█████╗ ██╔████╔██║",
|
|
16
|
+
r" ██║ ██║██╔══╝ ██║╚██╔╝██║",
|
|
17
|
+
r" ╚██████╔╝███████╗██║ ╚═╝ ██║",
|
|
18
|
+
r" ╚═════╝ ╚══════╝╚═╝ ╚═╝",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_CODE_LINES = (
|
|
22
|
+
r" ██████╗ ██████╗ ██████╗ ███████╗",
|
|
23
|
+
r" ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
24
|
+
r" ██║ ██║ ██║██║ ██║█████╗ ",
|
|
25
|
+
r" ██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
26
|
+
r" ╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
27
|
+
r" ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_TAGLINE = "✦ Gemini-powered coding agent. Fast. Capable. Local. ✦"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _banner_width() -> int:
|
|
34
|
+
return max(len(x) for x in _GEM_LINES + _CODE_LINES)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _center(line: str, width: int) -> str:
|
|
38
|
+
if len(line) >= width:
|
|
39
|
+
return line
|
|
40
|
+
pad = width - len(line)
|
|
41
|
+
left = pad // 2
|
|
42
|
+
return " " * left + line + " " * (pad - left)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _provider_model_endpoint(cfg) -> tuple[str, str, str]:
|
|
46
|
+
model = (getattr(cfg, "model", None) or "").strip() or "gemini-2.5-flash"
|
|
47
|
+
if vertex_env_active():
|
|
48
|
+
provider = "Vertex AI"
|
|
49
|
+
loc = (os.environ.get("GOOGLE_CLOUD_LOCATION") or "us-central1").strip()
|
|
50
|
+
proj = (os.environ.get("GOOGLE_CLOUD_PROJECT") or "").strip()
|
|
51
|
+
end = f"vertex://{loc}"
|
|
52
|
+
if proj:
|
|
53
|
+
end = f"{end} · {proj}"
|
|
54
|
+
else:
|
|
55
|
+
provider = "Google Gemini"
|
|
56
|
+
end = "https://generativelanguage.googleapis.com/"
|
|
57
|
+
return provider, model, end
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _kv_line(inner: int, label: str, value: str) -> str:
|
|
61
|
+
"""One row inside the box: leading space + label column + value (total width ``inner``)."""
|
|
62
|
+
lead = " "
|
|
63
|
+
prefix = f"{label:<10}"
|
|
64
|
+
room = inner - len(lead) - len(prefix)
|
|
65
|
+
if room < 4:
|
|
66
|
+
raw = (lead + prefix + value)[:inner]
|
|
67
|
+
return raw.ljust(inner)
|
|
68
|
+
v = value
|
|
69
|
+
if len(v) > room:
|
|
70
|
+
v = v[: max(0, room - 3)] + "..."
|
|
71
|
+
return (lead + prefix + v).ljust(inner)[:inner]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def format_welcome_banner(cfg, *, term_width: int = 100) -> str:
|
|
75
|
+
inner = min(60, max(48, term_width - 4))
|
|
76
|
+
top = "╔" + ("═" * inner) + "╗"
|
|
77
|
+
mid = "╠" + ("═" * inner) + "╣"
|
|
78
|
+
bot = "╚" + ("═" * inner) + "╝"
|
|
79
|
+
|
|
80
|
+
provider, model, endpoint = _provider_model_endpoint(cfg)
|
|
81
|
+
row_p = _kv_line(inner, "Provider", provider)
|
|
82
|
+
row_m = _kv_line(inner, "Model", model)
|
|
83
|
+
row_e = _kv_line(inner, "Endpoint", endpoint)
|
|
84
|
+
|
|
85
|
+
mode = "vertex" if vertex_env_active() else "cloud"
|
|
86
|
+
status_raw = f" ● {mode} Ready — type /help to begin"
|
|
87
|
+
if len(status_raw) > inner:
|
|
88
|
+
status_raw = status_raw[: max(0, inner - 3)] + "..."
|
|
89
|
+
row_s = status_raw.ljust(inner)[:inner]
|
|
90
|
+
|
|
91
|
+
bw = max(_banner_width(), inner + 2)
|
|
92
|
+
lines: list[str] = []
|
|
93
|
+
for row in _GEM_LINES:
|
|
94
|
+
lines.append(_center(row, bw))
|
|
95
|
+
lines.append("")
|
|
96
|
+
for row in _CODE_LINES:
|
|
97
|
+
lines.append(_center(row, bw))
|
|
98
|
+
lines.append("")
|
|
99
|
+
lines.append(_center(f" {_TAGLINE} ", bw))
|
|
100
|
+
lines.append("")
|
|
101
|
+
|
|
102
|
+
box_w = inner + 2
|
|
103
|
+
pad_b = max(0, (bw - box_w) // 2)
|
|
104
|
+
bp = " " * pad_b
|
|
105
|
+
|
|
106
|
+
def box_row(body: str) -> str:
|
|
107
|
+
b = body[:inner].ljust(inner)
|
|
108
|
+
return bp + "│" + b + "│"
|
|
109
|
+
|
|
110
|
+
lines.append(bp + top)
|
|
111
|
+
lines.append(box_row(row_p))
|
|
112
|
+
lines.append(box_row(row_m))
|
|
113
|
+
lines.append(box_row(row_e))
|
|
114
|
+
lines.append(bp + mid)
|
|
115
|
+
lines.append(box_row(row_s))
|
|
116
|
+
lines.append(bp + bot)
|
|
117
|
+
|
|
118
|
+
ver = os.environ.get("GEMCODE_VERSION", get_version())
|
|
119
|
+
lines.append(_center(f" gemcode v{ver} ", bw))
|
|
120
|
+
|
|
121
|
+
root = getattr(cfg, "project_root", None)
|
|
122
|
+
if isinstance(root, Path):
|
|
123
|
+
nt = narrow_workspace_tip(root)
|
|
124
|
+
if nt:
|
|
125
|
+
lines.append(_center(nt, bw))
|
|
126
|
+
lines.append("")
|
|
127
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Rich renderables for the scrollback welcome screen."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from gemcode.tui.welcome_banner import format_welcome_banner
|
|
8
|
+
|
|
9
|
+
# Classic terminal blues (xterm 75 / 33); gradient stays in this range (no violet).
|
|
10
|
+
_BLUE_MAIN = "#5fafd7"
|
|
11
|
+
_BLUE_DEEP = "#0087d7"
|
|
12
|
+
_BLUE_HI = "#87d7ff" # lighter edge of gradient
|
|
13
|
+
# Info box: slate grey frame + labels; cyan-blue values (not saturated “blue” that reads purple).
|
|
14
|
+
_BORDER_GREY = "#64748b"
|
|
15
|
+
_LABEL_GREY = "#94a3b8"
|
|
16
|
+
_VALUE_BLUE = "#5fafd7"
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _hex_rgb(h: str) -> tuple[int, int, int]:
|
|
23
|
+
h = h.removeprefix("#")
|
|
24
|
+
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _lerp_rgb(
|
|
28
|
+
a: tuple[int, int, int], b: tuple[int, int, int], t: float
|
|
29
|
+
) -> tuple[int, int, int]:
|
|
30
|
+
t = max(0.0, min(1.0, t))
|
|
31
|
+
return (
|
|
32
|
+
round(a[0] + (b[0] - a[0]) * t),
|
|
33
|
+
round(a[1] + (b[1] - a[1]) * t),
|
|
34
|
+
round(a[2] + (b[2] - a[2]) * t),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _gradient_logo_line(line: str, *, line_t: float) -> "Text":
|
|
39
|
+
"""Horizontal + mild vertical blend (same idea as OpenClaude startup gradient)."""
|
|
40
|
+
from rich.text import Text
|
|
41
|
+
|
|
42
|
+
lo = _hex_rgb(_BLUE_DEEP)
|
|
43
|
+
hi = _hex_rgb(_BLUE_HI)
|
|
44
|
+
n = len(line)
|
|
45
|
+
out = Text()
|
|
46
|
+
for i, ch in enumerate(line):
|
|
47
|
+
if ch == " ":
|
|
48
|
+
out.append(" ")
|
|
49
|
+
continue
|
|
50
|
+
t_h = i / (n - 1) if n > 1 else 0.0
|
|
51
|
+
blend = line_t * 0.45 + t_h * 0.55
|
|
52
|
+
r, g, b = _lerp_rgb(lo, hi, blend)
|
|
53
|
+
out.append(ch, style=f"bold #{r:02x}{g:02x}{b:02x}")
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_BOX_DRAW = frozenset("╔╗║╚╠╣═")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _info_box_line(line: str) -> "Text":
|
|
61
|
+
"""Grey borders/labels, blue values — avoids an all-blue/violet panel."""
|
|
62
|
+
from rich.text import Text
|
|
63
|
+
|
|
64
|
+
t = Text()
|
|
65
|
+
first = line.find("│")
|
|
66
|
+
last = line.rfind("│")
|
|
67
|
+
if first != -1 and last > first:
|
|
68
|
+
t.append(line[:first])
|
|
69
|
+
t.append("│", style=f"dim {_BORDER_GREY}")
|
|
70
|
+
inner = line[first + 1:last]
|
|
71
|
+
stripped = inner.lstrip()
|
|
72
|
+
if stripped.startswith("●") and "Ready" in inner:
|
|
73
|
+
ready_at = inner.find("Ready")
|
|
74
|
+
if ready_at > 0:
|
|
75
|
+
t.append(inner[:ready_at], style=f"dim {_LABEL_GREY}")
|
|
76
|
+
t.append(inner[ready_at:], style=_VALUE_BLUE)
|
|
77
|
+
else:
|
|
78
|
+
t.append(inner, style=_VALUE_BLUE)
|
|
79
|
+
elif stripped.startswith(("Provider", "Model", "Endpoint")):
|
|
80
|
+
t.append(inner[:1])
|
|
81
|
+
t.append(inner[1:11], style=f"dim {_LABEL_GREY}")
|
|
82
|
+
t.append(inner[11:], style=_VALUE_BLUE)
|
|
83
|
+
else:
|
|
84
|
+
t.append(inner, style=_VALUE_BLUE)
|
|
85
|
+
t.append("│", style=f"dim {_BORDER_GREY}")
|
|
86
|
+
t.append(line[last + 1 :])
|
|
87
|
+
return t
|
|
88
|
+
|
|
89
|
+
i = 0
|
|
90
|
+
while i < len(line) and line[i] == " ":
|
|
91
|
+
t.append(line[i])
|
|
92
|
+
i += 1
|
|
93
|
+
while i < len(line):
|
|
94
|
+
c = line[i]
|
|
95
|
+
if c in _BOX_DRAW:
|
|
96
|
+
t.append(c, style=f"dim {_BORDER_GREY}")
|
|
97
|
+
else:
|
|
98
|
+
t.append(c)
|
|
99
|
+
i += 1
|
|
100
|
+
return t
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def print_welcome_dashboard(cfg, *, console: Console) -> None:
|
|
104
|
+
from rich.console import Group
|
|
105
|
+
from rich.text import Text
|
|
106
|
+
|
|
107
|
+
tw = max(60, console.width or 80)
|
|
108
|
+
raw = format_welcome_banner(cfg, term_width=tw)
|
|
109
|
+
lines = raw.splitlines()
|
|
110
|
+
art_total = max(1, sum(1 for x in lines if "█" in x))
|
|
111
|
+
art_i = 0
|
|
112
|
+
rows: list[Text] = []
|
|
113
|
+
for ln in lines:
|
|
114
|
+
low = ln.lower()
|
|
115
|
+
if "█" in ln:
|
|
116
|
+
line_t = art_i / max(art_total - 1, 1)
|
|
117
|
+
rows.append(_gradient_logo_line(ln, line_t=line_t))
|
|
118
|
+
art_i += 1
|
|
119
|
+
elif any(c in ln for c in "╔╗║╚╠╣═│") and "█" not in ln:
|
|
120
|
+
rows.append(_info_box_line(ln))
|
|
121
|
+
elif "gemcode v" in low:
|
|
122
|
+
rows.append(Text(ln, style=f"bold {_BLUE_MAIN}"))
|
|
123
|
+
elif "✦" in ln:
|
|
124
|
+
rows.append(Text(ln, style=f"dim italic {_LABEL_GREY}"))
|
|
125
|
+
elif ln.strip().startswith("Tip:"):
|
|
126
|
+
rows.append(Text(ln, style=f"dim {_LABEL_GREY}"))
|
|
127
|
+
else:
|
|
128
|
+
rows.append(Text(ln))
|
|
129
|
+
console.print(Group(*rows))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def print_shortcuts_hint(*, console: Console) -> None:
|
|
133
|
+
from rich.text import Text
|
|
134
|
+
|
|
135
|
+
console.print(Text(" ? for shortcuts", style=f"dim {_LABEL_GREY}"))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemcode
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.15
|
|
4
4
|
Summary: Local-first coding agent on Google Gemini + ADK
|
|
5
5
|
Author: GemCode Contributors
|
|
6
6
|
License: Apache License
|
|
@@ -178,14 +178,12 @@ License-File: LICENSE
|
|
|
178
178
|
Requires-Dist: google-adk>=1.0.0
|
|
179
179
|
Requires-Dist: google-genai>=1.0.0
|
|
180
180
|
Requires-Dist: python-dotenv>=1.0.0
|
|
181
|
+
Requires-Dist: rich>=13.0.0
|
|
181
182
|
Provides-Extra: dev
|
|
182
183
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
183
184
|
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
|
|
184
185
|
Provides-Extra: mcp
|
|
185
186
|
Requires-Dist: mcp>=1.0.0; extra == "mcp"
|
|
186
|
-
Provides-Extra: tui
|
|
187
|
-
Requires-Dist: prompt_toolkit>=3.0.0; extra == "tui"
|
|
188
|
-
Requires-Dist: rich>=13.0.0; extra == "tui"
|
|
189
187
|
Dynamic: license-file
|
|
190
188
|
|
|
191
189
|
# GemCode
|
|
@@ -69,10 +69,10 @@ src/gemcode/tools/search.py
|
|
|
69
69
|
src/gemcode/tools/shell.py
|
|
70
70
|
src/gemcode/tools/shell_gate.py
|
|
71
71
|
src/gemcode/tools/todo.py
|
|
72
|
-
src/gemcode/tui/app.py
|
|
73
72
|
src/gemcode/tui/scrollback.py
|
|
74
73
|
src/gemcode/tui/spinner.py
|
|
75
|
-
src/gemcode/tui/
|
|
74
|
+
src/gemcode/tui/welcome_banner.py
|
|
75
|
+
src/gemcode/tui/welcome_rich.py
|
|
76
76
|
src/gemcode/web/__init__.py
|
|
77
77
|
src/gemcode/web/claude_sse_adapter.py
|
|
78
78
|
src/gemcode/web/terminal_repl.py
|