gemcode 0.3.11__tar.gz → 0.3.14__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.11/src/gemcode.egg-info → gemcode-0.3.14}/PKG-INFO +2 -4
- {gemcode-0.3.11 → gemcode-0.3.14}/pyproject.toml +2 -2
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/cli.py +15 -26
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/tui/scrollback.py +20 -82
- gemcode-0.3.14/src/gemcode/tui/spinner.py +150 -0
- gemcode-0.3.14/src/gemcode/tui/startup_screen.py +196 -0
- gemcode-0.3.14/src/gemcode/tui/welcome_banner.py +127 -0
- gemcode-0.3.14/src/gemcode/tui/welcome_rich.py +40 -0
- {gemcode-0.3.11 → gemcode-0.3.14/src/gemcode.egg-info}/PKG-INFO +2 -4
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode.egg-info/SOURCES.txt +4 -1
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode.egg-info/requires.txt +1 -4
- gemcode-0.3.11/src/gemcode/tui/app.py +0 -927
- {gemcode-0.3.11 → gemcode-0.3.14}/LICENSE +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/MANIFEST.in +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/README.md +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/setup.cfg +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/agent.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/config.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/version.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_credentials.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_paths.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_permissions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_tools.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.14}/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.14
|
|
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.14"
|
|
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,6 +183,10 @@ 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
|
extra: list = []
|
|
187
191
|
if use_mcp:
|
|
188
192
|
from gemcode.mcp_loader import load_mcp_toolsets
|
|
@@ -225,11 +229,10 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
225
229
|
except EOFError:
|
|
226
230
|
pass
|
|
227
231
|
|
|
228
|
-
# Optional
|
|
232
|
+
# Optional terminal UI: single scrollback-style REPL (terminal history, no alt-screen app).
|
|
229
233
|
tui_enabled = os.environ.get("GEMCODE_TUI", "1").lower() in ("1", "true", "yes", "on")
|
|
230
234
|
if tui_enabled:
|
|
231
235
|
term = (os.environ.get("TERM") or "").strip().lower()
|
|
232
|
-
# Guardrails: Prompt Toolkit needs a real interactive terminal.
|
|
233
236
|
if not sys.stdin.isatty() or not sys.stdout.isatty() or term in ("", "dumb", "unknown"):
|
|
234
237
|
print(
|
|
235
238
|
f"[tui] disabled (stdin/stdout isatty={sys.stdin.isatty()}/{sys.stdout.isatty()}, TERM={term or '<unset>'}); using plain REPL",
|
|
@@ -237,33 +240,19 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
237
240
|
)
|
|
238
241
|
else:
|
|
239
242
|
try:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
extra_tools=extra or None,
|
|
249
|
-
)
|
|
250
|
-
else:
|
|
251
|
-
from gemcode.tui.app import run_gemcode_tui
|
|
252
|
-
|
|
253
|
-
await run_gemcode_tui(
|
|
254
|
-
cfg=cfg,
|
|
255
|
-
runner=runner,
|
|
256
|
-
session_id=session_id,
|
|
257
|
-
extra_tools=extra or None,
|
|
258
|
-
)
|
|
243
|
+
from gemcode.tui.scrollback import run_gemcode_scrollback_tui
|
|
244
|
+
|
|
245
|
+
await run_gemcode_scrollback_tui(
|
|
246
|
+
cfg=cfg,
|
|
247
|
+
runner=runner,
|
|
248
|
+
session_id=session_id,
|
|
249
|
+
extra_tools=extra or None,
|
|
250
|
+
)
|
|
259
251
|
return
|
|
260
252
|
except Exception as e:
|
|
261
|
-
# Dependency missing or terminal doesn't support full-screen.
|
|
262
|
-
# Print one line so users know how to fix it.
|
|
263
253
|
print(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
file=sys.stderr,
|
|
254
|
+
f"[tui] failed to start: {type(e).__name__}: {e} (falling back to plain REPL).",
|
|
255
|
+
file=sys.stderr,
|
|
267
256
|
)
|
|
268
257
|
|
|
269
258
|
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,25 +117,28 @@ class _Ansi:
|
|
|
117
117
|
|
|
118
118
|
@property
|
|
119
119
|
def blue(self) -> str:
|
|
120
|
-
#
|
|
121
|
-
return self.esc("38;5;
|
|
120
|
+
# Sunset orange/warm color instead of blue
|
|
121
|
+
return self.esc("38;5;215")
|
|
122
122
|
|
|
123
123
|
@property
|
|
124
124
|
def blue2(self) -> str:
|
|
125
|
-
#
|
|
126
|
-
return self.esc("38;5;
|
|
125
|
+
# Deeper warm color
|
|
126
|
+
return self.esc("38;5;209")
|
|
127
127
|
|
|
128
128
|
@property
|
|
129
129
|
def blue_ok(self) -> str:
|
|
130
|
-
|
|
130
|
+
# Warm green for success
|
|
131
|
+
return self.esc("38;5;150")
|
|
131
132
|
|
|
132
133
|
@property
|
|
133
134
|
def blue_warn(self) -> str:
|
|
134
|
-
|
|
135
|
+
# Warm amber for warnings
|
|
136
|
+
return self.esc("38;5;221")
|
|
135
137
|
|
|
136
138
|
@property
|
|
137
139
|
def blue_tool(self) -> str:
|
|
138
|
-
|
|
140
|
+
# Warm tool color
|
|
141
|
+
return self.esc("38;5;180")
|
|
139
142
|
|
|
140
143
|
|
|
141
144
|
def _term_width(default: int = 100) -> int:
|
|
@@ -151,62 +154,6 @@ def _hr(ch: str = "─") -> str:
|
|
|
151
154
|
return ch * _term_width()
|
|
152
155
|
|
|
153
156
|
|
|
154
|
-
def _dashboard(cfg) -> str:
|
|
155
|
-
w = _term_width()
|
|
156
|
-
title = f" GemCode v{os.environ.get('GEMCODE_VERSION', get_version())} "
|
|
157
|
-
left_w = (w - 4) * 2 // 3
|
|
158
|
-
right_w = (w - 4) - left_w
|
|
159
|
-
|
|
160
|
-
def pad(s: str, ww: int) -> str:
|
|
161
|
-
s = s.replace("\n", " ")
|
|
162
|
-
if len(s) > ww:
|
|
163
|
-
return s[: ww - 1] + "…"
|
|
164
|
-
return s + (" " * (ww - len(s)))
|
|
165
|
-
|
|
166
|
-
user = (os.environ.get("USER") or os.environ.get("LOGNAME") or "there").strip()
|
|
167
|
-
model = getattr(cfg, "model", "") or ""
|
|
168
|
-
root = str(getattr(cfg, "project_root", "") or "")
|
|
169
|
-
|
|
170
|
-
box_top = "╭" + ("─" * (w - 2)) + "╮"
|
|
171
|
-
box_bot = "╰" + ("─" * (w - 2)) + "╯"
|
|
172
|
-
lines: list[str] = [box_top]
|
|
173
|
-
lines.append("│" + pad(title, w - 2) + "│")
|
|
174
|
-
lines.append("│" + (" " * (w - 2)) + "│")
|
|
175
|
-
left = [
|
|
176
|
-
"",
|
|
177
|
-
f"Welcome back {user}!",
|
|
178
|
-
"",
|
|
179
|
-
" ▐▛███▜▌",
|
|
180
|
-
" ▝▜█████▛▘",
|
|
181
|
-
" ▘▘ ▝▝",
|
|
182
|
-
"",
|
|
183
|
-
f"{model or 'GemCode'} · Local session",
|
|
184
|
-
root,
|
|
185
|
-
]
|
|
186
|
-
right = [
|
|
187
|
-
"Tips for getting started",
|
|
188
|
-
"First run creates .gemcode/ (trust + API key)",
|
|
189
|
-
"",
|
|
190
|
-
"Recent activity",
|
|
191
|
-
"No recent activity",
|
|
192
|
-
]
|
|
193
|
-
h = max(len(left), len(right))
|
|
194
|
-
left += [""] * (h - len(left))
|
|
195
|
-
right += [""] * (h - len(right))
|
|
196
|
-
for i in range(h):
|
|
197
|
-
lines.append(
|
|
198
|
-
"│ " + pad(left[i], left_w) + " │ " + pad(right[i], right_w) + " │"
|
|
199
|
-
)
|
|
200
|
-
nt = narrow_workspace_tip(getattr(cfg, "project_root"))
|
|
201
|
-
if nt:
|
|
202
|
-
lines.append("│" + pad(f" {nt}", w - 2) + "│")
|
|
203
|
-
lines.append(box_bot)
|
|
204
|
-
lines.append("")
|
|
205
|
-
lines.append(" ↑ GemCode Pro now supports larger contexts · faster streaming")
|
|
206
|
-
lines.append("")
|
|
207
|
-
return "\n".join(lines)
|
|
208
|
-
|
|
209
|
-
|
|
210
157
|
async def run_gemcode_scrollback_tui(
|
|
211
158
|
*, cfg, runner, session_id: str, extra_tools=None
|
|
212
159
|
) -> None:
|
|
@@ -229,24 +176,15 @@ async def run_gemcode_scrollback_tui(
|
|
|
229
176
|
)
|
|
230
177
|
)
|
|
231
178
|
|
|
179
|
+
console = Console(
|
|
180
|
+
width=_term_width(),
|
|
181
|
+
force_terminal=bool(sys.stdout.isatty()),
|
|
182
|
+
no_color=not ansi.enabled,
|
|
183
|
+
highlight=False,
|
|
184
|
+
)
|
|
232
185
|
if os.environ.get("GEMCODE_TUI_SHOW_DASHBOARD", "1").lower() in ("1", "true", "yes", "on"):
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
# Color title + the ASCII mark.
|
|
236
|
-
lines = dash.splitlines()
|
|
237
|
-
if len(lines) >= 2:
|
|
238
|
-
lines[1] = (
|
|
239
|
-
lines[1]
|
|
240
|
-
.replace("GemCode", f"{ansi.blue}{ansi.bold}GemCode{ansi.reset}")
|
|
241
|
-
.replace("v", f"{ansi.dim}v{ansi.reset}")
|
|
242
|
-
)
|
|
243
|
-
for i, ln in enumerate(lines):
|
|
244
|
-
if "▐▛███▜▌" in ln or "▝▜█████▛▘" in ln or "▘▘ ▝▝" in ln:
|
|
245
|
-
lines[i] = f"{ansi.blue2}{ln}{ansi.reset}"
|
|
246
|
-
dash = "\n".join(lines)
|
|
247
|
-
print(dash)
|
|
248
|
-
|
|
249
|
-
print(f"{ansi.dim} ? for shortcuts{ansi.reset}")
|
|
186
|
+
print_welcome_dashboard(cfg, console=console)
|
|
187
|
+
print_shortcuts_hint(console=console)
|
|
250
188
|
print("")
|
|
251
189
|
|
|
252
190
|
char_delay_ms = int(os.environ.get("GEMCODE_TUI_CHAR_DELAY_MS", "0") or "0")
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spinner component matching OpenClaude's exact implementation.
|
|
3
|
+
Includes thinking status, tips, and animated spinner.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
# Spinner frames - Braille patterns like OpenClaude
|
|
14
|
+
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
15
|
+
|
|
16
|
+
# Spinner verbs - matching OpenClaude's variety
|
|
17
|
+
SPINNER_VERBS = [
|
|
18
|
+
"Thinking",
|
|
19
|
+
"Analyzing",
|
|
20
|
+
"Planning",
|
|
21
|
+
"Writing",
|
|
22
|
+
"Checking",
|
|
23
|
+
"Reviewing",
|
|
24
|
+
"Processing",
|
|
25
|
+
"Evaluating",
|
|
26
|
+
"Considering",
|
|
27
|
+
"Examining",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Tips - matching OpenClaude's helpful hints
|
|
31
|
+
SPINNER_TIPS = [
|
|
32
|
+
"Use /help to see all available commands",
|
|
33
|
+
"Press Ctrl+O to toggle thinking display",
|
|
34
|
+
"Press Ctrl+K to toggle home screen",
|
|
35
|
+
"Use /clear to start fresh when switching topics",
|
|
36
|
+
"Type /tools to see available tools",
|
|
37
|
+
"Use Esc to interrupt the current operation",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SpinnerState:
|
|
42
|
+
"""Manages spinner animation state."""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self.frame_index = 0
|
|
46
|
+
self.verb = random.choice(SPINNER_VERBS)
|
|
47
|
+
self.thinking_start: Optional[float] = None
|
|
48
|
+
self.thinking_duration: Optional[float] = None
|
|
49
|
+
self.thinking_display_until: Optional[float] = None
|
|
50
|
+
self.last_update = time.time()
|
|
51
|
+
|
|
52
|
+
def get_frame(self) -> str:
|
|
53
|
+
"""Get current spinner frame."""
|
|
54
|
+
return SPINNER_FRAMES[self.frame_index % len(SPINNER_FRAMES)]
|
|
55
|
+
|
|
56
|
+
def advance(self) -> None:
|
|
57
|
+
"""Advance to next frame."""
|
|
58
|
+
now = time.time()
|
|
59
|
+
# Update every 120ms like OpenClaude
|
|
60
|
+
if now - self.last_update >= 0.12:
|
|
61
|
+
self.frame_index += 1
|
|
62
|
+
self.last_update = now
|
|
63
|
+
|
|
64
|
+
def start_thinking(self) -> None:
|
|
65
|
+
"""Mark start of thinking phase."""
|
|
66
|
+
if self.thinking_start is None:
|
|
67
|
+
self.thinking_start = time.time()
|
|
68
|
+
self.thinking_duration = None
|
|
69
|
+
self.thinking_display_until = None
|
|
70
|
+
|
|
71
|
+
def stop_thinking(self) -> None:
|
|
72
|
+
"""Mark end of thinking phase."""
|
|
73
|
+
if self.thinking_start is not None:
|
|
74
|
+
duration = time.time() - self.thinking_start
|
|
75
|
+
self.thinking_duration = duration
|
|
76
|
+
# Display "thought for Xs" for minimum 2 seconds
|
|
77
|
+
self.thinking_display_until = time.time() + 2.0
|
|
78
|
+
self.thinking_start = None
|
|
79
|
+
|
|
80
|
+
def get_thinking_status(self) -> Optional[str]:
|
|
81
|
+
"""Get thinking status text."""
|
|
82
|
+
now = time.time()
|
|
83
|
+
|
|
84
|
+
# Currently thinking
|
|
85
|
+
if self.thinking_start is not None:
|
|
86
|
+
elapsed = int(now - self.thinking_start)
|
|
87
|
+
if elapsed >= 1:
|
|
88
|
+
return f"💭 Thinking for {elapsed}s"
|
|
89
|
+
return "💭 Thinking"
|
|
90
|
+
|
|
91
|
+
# Show duration after thinking (for 2s minimum)
|
|
92
|
+
if self.thinking_duration is not None and self.thinking_display_until is not None:
|
|
93
|
+
if now < self.thinking_display_until:
|
|
94
|
+
duration = int(self.thinking_duration)
|
|
95
|
+
return f"Thought for {duration}s"
|
|
96
|
+
# Clear after display period
|
|
97
|
+
self.thinking_duration = None
|
|
98
|
+
self.thinking_display_until = None
|
|
99
|
+
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def should_show_tip(self, elapsed_seconds: float) -> bool:
|
|
103
|
+
"""Determine if we should show a tip based on elapsed time."""
|
|
104
|
+
# Show tips after 30 seconds like OpenClaude
|
|
105
|
+
return elapsed_seconds > 30
|
|
106
|
+
|
|
107
|
+
def get_tip(self) -> str:
|
|
108
|
+
"""Get a random tip."""
|
|
109
|
+
return random.choice(SPINNER_TIPS)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def format_spinner_line(
|
|
113
|
+
spinner_state: SpinnerState,
|
|
114
|
+
mode: str = "working",
|
|
115
|
+
elapsed_seconds: float = 0,
|
|
116
|
+
show_tip: bool = True,
|
|
117
|
+
) -> tuple[str, Optional[str]]:
|
|
118
|
+
"""
|
|
119
|
+
Format spinner line matching OpenClaude's exact style.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Tuple of (main_line, tip_line)
|
|
123
|
+
"""
|
|
124
|
+
frame = spinner_state.get_frame()
|
|
125
|
+
verb = spinner_state.verb
|
|
126
|
+
|
|
127
|
+
# Check for thinking status
|
|
128
|
+
thinking_status = spinner_state.get_thinking_status()
|
|
129
|
+
|
|
130
|
+
if thinking_status:
|
|
131
|
+
# Show thinking status instead of regular spinner
|
|
132
|
+
main_line = f" {thinking_status} · Esc=interrupt"
|
|
133
|
+
else:
|
|
134
|
+
# Regular spinner with verb
|
|
135
|
+
main_line = f" {frame} {verb}… · Esc=interrupt"
|
|
136
|
+
|
|
137
|
+
# Tip line (shown after 30s)
|
|
138
|
+
tip_line = None
|
|
139
|
+
if show_tip and spinner_state.should_show_tip(elapsed_seconds):
|
|
140
|
+
tip = spinner_state.get_tip()
|
|
141
|
+
tip_line = f" Tip: {tip}"
|
|
142
|
+
|
|
143
|
+
return main_line, tip_line
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def format_idle_status(git_branch: Optional[str] = None) -> str:
|
|
147
|
+
"""Format idle status line matching OpenClaude."""
|
|
148
|
+
if git_branch:
|
|
149
|
+
return f" 🌿 {git_branch} · Ready · Ctrl+K=home · Ctrl+O=thinking"
|
|
150
|
+
return " 📁 workspace · Ready · Ctrl+K=home · Ctrl+O=thinking"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GemCode startup screen with gradient ASCII art logo.
|
|
3
|
+
Inspired by OpenClaude's beautiful startup experience.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from typing import NamedTuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RGB(NamedTuple):
|
|
14
|
+
r: int
|
|
15
|
+
g: int
|
|
16
|
+
b: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
ESC = "\x1b["
|
|
20
|
+
RESET = f"{ESC}0m"
|
|
21
|
+
DIM = f"{ESC}2m"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def rgb(r: int, g: int, b: int) -> str:
|
|
25
|
+
return f"{ESC}38;2;{r};{g};{b}m"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def lerp(a: RGB, b: RGB, t: float) -> RGB:
|
|
29
|
+
return RGB(
|
|
30
|
+
round(a.r + (b.r - a.r) * t),
|
|
31
|
+
round(a.g + (b.g - a.g) * t),
|
|
32
|
+
round(a.b + (b.b - a.b) * t),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def grad_at(stops: list[RGB], t: float) -> RGB:
|
|
37
|
+
c = max(0.0, min(1.0, t))
|
|
38
|
+
s = c * (len(stops) - 1)
|
|
39
|
+
i = int(s)
|
|
40
|
+
if i >= len(stops) - 1:
|
|
41
|
+
return stops[-1]
|
|
42
|
+
return lerp(stops[i], stops[i + 1], s - i)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def paint_line(text: str, stops: list[RGB], line_t: float) -> str:
|
|
46
|
+
out = ""
|
|
47
|
+
for i, ch in enumerate(text):
|
|
48
|
+
t = line_t * 0.5 + (i / (len(text) - 1)) * 0.5 if len(text) > 1 else line_t
|
|
49
|
+
color = grad_at(stops, t)
|
|
50
|
+
out += f"{rgb(color.r, color.g, color.b)}{ch}"
|
|
51
|
+
return out + RESET
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Color palette - sunset gradient
|
|
55
|
+
SUNSET_GRAD: list[RGB] = [
|
|
56
|
+
RGB(255, 180, 100),
|
|
57
|
+
RGB(240, 140, 80),
|
|
58
|
+
RGB(217, 119, 87),
|
|
59
|
+
RGB(193, 95, 60),
|
|
60
|
+
RGB(160, 75, 55),
|
|
61
|
+
RGB(130, 60, 50),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
ACCENT = RGB(240, 148, 100)
|
|
65
|
+
CREAM = RGB(220, 195, 170)
|
|
66
|
+
DIMCOL = RGB(120, 100, 82)
|
|
67
|
+
BORDER = RGB(100, 80, 65)
|
|
68
|
+
|
|
69
|
+
# Filled block text logo
|
|
70
|
+
LOGO_GEM = [
|
|
71
|
+
" ██████╗ ███████╗███╗ ███╗",
|
|
72
|
+
" ██╔════╝ ██╔════╝████╗ ████║",
|
|
73
|
+
" ██║ ███╗█████╗ ██╔████╔██║",
|
|
74
|
+
" ██║ ██║██╔══╝ ██║╚██╔╝██║",
|
|
75
|
+
" ╚██████╔╝███████╗██║ ╚═╝ ██║",
|
|
76
|
+
" ╚═════╝ ╚══════╝╚═╝ ╚═╝",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
LOGO_CODE = [
|
|
80
|
+
" ██████╗ ██████╗ ██████╗ ███████╗",
|
|
81
|
+
" ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
82
|
+
" ██║ ██║ ██║██║ ██║█████╗ ",
|
|
83
|
+
" ██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
84
|
+
" ╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
85
|
+
" ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def detect_provider() -> dict[str, str | bool]:
|
|
90
|
+
"""Detect current provider configuration."""
|
|
91
|
+
# Check for Gemini (default for GemCode)
|
|
92
|
+
api_key = os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY")
|
|
93
|
+
model = os.environ.get("GEMINI_MODEL") or os.environ.get("GEMCODE_MODEL") or "gemini-2.0-flash-exp"
|
|
94
|
+
base_url = os.environ.get("GEMINI_BASE_URL") or "https://generativelanguage.googleapis.com"
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"name": "Google Gemini",
|
|
98
|
+
"model": model,
|
|
99
|
+
"base_url": base_url,
|
|
100
|
+
"is_local": False,
|
|
101
|
+
"has_key": bool(api_key),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def box_row(content: str, width: int, raw_len: int) -> str:
|
|
106
|
+
"""Create a box row with proper padding."""
|
|
107
|
+
pad = max(0, width - 2 - raw_len)
|
|
108
|
+
return f"{rgb(BORDER.r, BORDER.g, BORDER.b)}│{RESET}{content}{' ' * pad}{rgb(BORDER.r, BORDER.g, BORDER.b)}│{RESET}"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def print_startup_screen() -> None:
|
|
112
|
+
"""Print the beautiful gradient startup screen."""
|
|
113
|
+
# Skip in non-interactive / CI environments
|
|
114
|
+
if os.environ.get("CI") or not sys.stdout.isatty():
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# Skip if explicitly disabled
|
|
118
|
+
if os.environ.get("GEMCODE_NO_STARTUP_SCREEN", "").lower() in ("1", "true", "yes", "on"):
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
p = detect_provider()
|
|
122
|
+
W = 62
|
|
123
|
+
out: list[str] = []
|
|
124
|
+
|
|
125
|
+
out.append("")
|
|
126
|
+
|
|
127
|
+
# Gradient logo
|
|
128
|
+
all_logo = LOGO_GEM + [""] + LOGO_CODE
|
|
129
|
+
total = len(all_logo)
|
|
130
|
+
for i, line in enumerate(all_logo):
|
|
131
|
+
t = i / (total - 1) if total > 1 else 0
|
|
132
|
+
if line == "":
|
|
133
|
+
out.append("")
|
|
134
|
+
else:
|
|
135
|
+
out.append(paint_line(line, SUNSET_GRAD, t))
|
|
136
|
+
|
|
137
|
+
out.append("")
|
|
138
|
+
|
|
139
|
+
# Tagline
|
|
140
|
+
out.append(
|
|
141
|
+
f" {rgb(ACCENT.r, ACCENT.g, ACCENT.b)}✦{RESET} "
|
|
142
|
+
f"{rgb(CREAM.r, CREAM.g, CREAM.b)}Gemini-powered coding agent. Fast. Capable. Local.{RESET} "
|
|
143
|
+
f"{rgb(ACCENT.r, ACCENT.g, ACCENT.b)}✦{RESET}"
|
|
144
|
+
)
|
|
145
|
+
out.append("")
|
|
146
|
+
|
|
147
|
+
# Provider info box
|
|
148
|
+
out.append(f"{rgb(BORDER.r, BORDER.g, BORDER.b)}╔{'═' * (W - 2)}╗{RESET}")
|
|
149
|
+
|
|
150
|
+
def lbl(k: str, v: str, c: RGB = CREAM) -> tuple[str, int]:
|
|
151
|
+
pad_k = k.ljust(9)
|
|
152
|
+
return (
|
|
153
|
+
f" {DIM}{rgb(DIMCOL.r, DIMCOL.g, DIMCOL.b)}{pad_k}{RESET} {rgb(c.r, c.g, c.b)}{v}{RESET}",
|
|
154
|
+
len(f" {pad_k} {v}"),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
prov_c = RGB(130, 175, 130) if p["is_local"] else ACCENT
|
|
158
|
+
r, l = lbl("Provider", str(p["name"]), prov_c)
|
|
159
|
+
out.append(box_row(r, W, l))
|
|
160
|
+
|
|
161
|
+
r, l = lbl("Model", str(p["model"]))
|
|
162
|
+
out.append(box_row(r, W, l))
|
|
163
|
+
|
|
164
|
+
ep = str(p["base_url"])
|
|
165
|
+
if len(ep) > 38:
|
|
166
|
+
ep = ep[:35] + "..."
|
|
167
|
+
r, l = lbl("Endpoint", ep)
|
|
168
|
+
out.append(box_row(r, W, l))
|
|
169
|
+
|
|
170
|
+
out.append(f"{rgb(BORDER.r, BORDER.g, BORDER.b)}╠{'═' * (W - 2)}╣{RESET}")
|
|
171
|
+
|
|
172
|
+
# Status line
|
|
173
|
+
s_c = RGB(130, 175, 130) if p["is_local"] else ACCENT
|
|
174
|
+
s_l = "local" if p["is_local"] else "cloud"
|
|
175
|
+
status_text = (
|
|
176
|
+
f" {rgb(s_c.r, s_c.g, s_c.b)}●{RESET} "
|
|
177
|
+
f"{DIM}{rgb(DIMCOL.r, DIMCOL.g, DIMCOL.b)}{s_l}{RESET} "
|
|
178
|
+
f"{DIM}{rgb(DIMCOL.r, DIMCOL.g, DIMCOL.b)}Ready — type {RESET}"
|
|
179
|
+
f"{rgb(ACCENT.r, ACCENT.g, ACCENT.b)}/help{RESET}"
|
|
180
|
+
f"{DIM}{rgb(DIMCOL.r, DIMCOL.g, DIMCOL.b)} to begin{RESET}"
|
|
181
|
+
)
|
|
182
|
+
s_len = len(f" ● {s_l} Ready — type /help to begin")
|
|
183
|
+
out.append(box_row(status_text, W, s_len))
|
|
184
|
+
|
|
185
|
+
out.append(f"{rgb(BORDER.r, BORDER.g, BORDER.b)}╚{'═' * (W - 2)}╝{RESET}")
|
|
186
|
+
|
|
187
|
+
# Version
|
|
188
|
+
from gemcode.version import get_version
|
|
189
|
+
version = os.environ.get("GEMCODE_VERSION", get_version())
|
|
190
|
+
out.append(
|
|
191
|
+
f" {DIM}{rgb(DIMCOL.r, DIMCOL.g, DIMCOL.b)}gemcode {RESET}"
|
|
192
|
+
f"{rgb(ACCENT.r, ACCENT.g, ACCENT.b)}v{version}{RESET}"
|
|
193
|
+
)
|
|
194
|
+
out.append("")
|
|
195
|
+
|
|
196
|
+
print("\n".join(out), flush=True)
|