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.
Files changed (109) hide show
  1. {gemcode-0.3.12/src/gemcode.egg-info → gemcode-0.3.15}/PKG-INFO +2 -4
  2. {gemcode-0.3.12 → gemcode-0.3.15}/pyproject.toml +2 -2
  3. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/cli.py +12 -32
  4. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tui/scrollback.py +16 -88
  5. gemcode-0.3.15/src/gemcode/tui/welcome_banner.py +127 -0
  6. gemcode-0.3.15/src/gemcode/tui/welcome_rich.py +135 -0
  7. {gemcode-0.3.12 → gemcode-0.3.15/src/gemcode.egg-info}/PKG-INFO +2 -4
  8. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode.egg-info/SOURCES.txt +2 -2
  9. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode.egg-info/requires.txt +1 -4
  10. gemcode-0.3.12/src/gemcode/tui/app.py +0 -968
  11. gemcode-0.3.12/src/gemcode/tui/startup_screen.py +0 -196
  12. {gemcode-0.3.12 → gemcode-0.3.15}/LICENSE +0 -0
  13. {gemcode-0.3.12 → gemcode-0.3.15}/MANIFEST.in +0 -0
  14. {gemcode-0.3.12 → gemcode-0.3.15}/README.md +0 -0
  15. {gemcode-0.3.12 → gemcode-0.3.15}/setup.cfg +0 -0
  16. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/__init__.py +0 -0
  17. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/__main__.py +0 -0
  18. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/agent.py +0 -0
  19. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/audit.py +0 -0
  20. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/autocompact.py +0 -0
  21. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/callbacks.py +0 -0
  22. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/capability_routing.py +0 -0
  23. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/compaction.py +0 -0
  24. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/computer_use/__init__.py +0 -0
  25. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/computer_use/browser_computer.py +0 -0
  26. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/config.py +0 -0
  27. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/context_budget.py +0 -0
  28. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/context_warning.py +0 -0
  29. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/credentials.py +0 -0
  30. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/hitl_session.py +0 -0
  31. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/interactions.py +0 -0
  32. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/invoke.py +0 -0
  33. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/kairos_daemon.py +0 -0
  34. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/limits.py +0 -0
  35. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/live_audio_engine.py +0 -0
  36. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/logging_config.py +0 -0
  37. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/mcp_loader.py +0 -0
  38. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/memory/__init__.py +0 -0
  39. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/memory/embedding_memory_service.py +0 -0
  40. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/memory/file_memory_service.py +0 -0
  41. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/modality_tools.py +0 -0
  42. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/model_errors.py +0 -0
  43. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/model_routing.py +0 -0
  44. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/paths.py +0 -0
  45. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/permissions.py +0 -0
  46. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/plugins/__init__.py +0 -0
  47. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  48. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  49. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/prompt_suggestions.py +0 -0
  50. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/__init__.py +0 -0
  51. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/config.py +0 -0
  52. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/deps.py +0 -0
  53. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/engine.py +0 -0
  54. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/stop_hooks.py +0 -0
  55. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/token_budget.py +0 -0
  56. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/query/transitions.py +0 -0
  57. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/repl_commands.py +0 -0
  58. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/repl_slash.py +0 -0
  59. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/session_runtime.py +0 -0
  60. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/slash_commands.py +0 -0
  61. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/thinking.py +0 -0
  62. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tool_prompt_manifest.py +0 -0
  63. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tool_registry.py +0 -0
  64. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/__init__.py +0 -0
  65. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/edit.py +0 -0
  66. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/filesystem.py +0 -0
  67. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/search.py +0 -0
  68. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/shell.py +0 -0
  69. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/shell_gate.py +0 -0
  70. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools/todo.py +0 -0
  71. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tools_inspector.py +0 -0
  72. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/trust.py +0 -0
  73. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/tui/spinner.py +0 -0
  74. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/version.py +0 -0
  75. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/vertex.py +0 -0
  76. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/web/__init__.py +0 -0
  77. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/web/claude_sse_adapter.py +0 -0
  78. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/web/terminal_repl.py +0 -0
  79. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode/workspace_hints.py +0 -0
  80. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode.egg-info/dependency_links.txt +0 -0
  81. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode.egg-info/entry_points.txt +0 -0
  82. {gemcode-0.3.12 → gemcode-0.3.15}/src/gemcode.egg-info/top_level.txt +0 -0
  83. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_agent_instruction.py +0 -0
  84. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_autocompact.py +0 -0
  85. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_capability_routing.py +0 -0
  86. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_claude_web_adapter_sse.py +0 -0
  87. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_cli_init.py +0 -0
  88. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_computer_use_permissions.py +0 -0
  89. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_context_budget.py +0 -0
  90. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_context_warning.py +0 -0
  91. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_credentials.py +0 -0
  92. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_interactive_permission_ask.py +0 -0
  93. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_kairos_scheduler.py +0 -0
  94. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_modality_tools.py +0 -0
  95. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_model_error_retry.py +0 -0
  96. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_model_errors.py +0 -0
  97. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_model_routing.py +0 -0
  98. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_paths.py +0 -0
  99. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_permissions.py +0 -0
  100. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_prompt_suggestions.py +0 -0
  101. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_repl_commands.py +0 -0
  102. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_repl_slash.py +0 -0
  103. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_slash_commands.py +0 -0
  104. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_thinking_config.py +0 -0
  105. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_token_budget.py +0 -0
  106. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_tool_context_circulation.py +0 -0
  107. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_tools.py +0 -0
  108. {gemcode-0.3.12 → gemcode-0.3.15}/tests/test_tools_inspector.py +0 -0
  109. {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.12
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.12"
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 TUI. Claude-like default is "scrollback" (no internal scrolling).
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
- # Default to the full-screen TUI (OpenClaude-like). Scrollback is still
245
- # available for terminals that can't handle alternate screen UIs.
246
- style = os.environ.get("GEMCODE_TUI_STYLE", "full").strip().lower()
247
- if style in ("scrollback", "claude", "claude-like"):
248
- from gemcode.tui.scrollback import run_gemcode_scrollback_tui
249
-
250
- await run_gemcode_scrollback_tui(
251
- cfg=cfg,
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
- f"[tui] failed to start: {type(e).__name__}: {e} (falling back to plain REPL). "
271
- "Install extras with: pip install 'gemcode[tui]'",
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.version import get_version
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
- # Sunset orange/warm color instead of blue
121
- return self.esc("38;5;215")
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
- # Deeper warm color
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
- # Warm green for success
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
- # Warm amber for warnings
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
- # Warm tool color
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
- dash = _dashboard(cfg)
239
- if ansi.enabled:
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.12
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/startup_screen.py
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
@@ -1,6 +1,7 @@
1
1
  google-adk>=1.0.0
2
2
  google-genai>=1.0.0
3
3
  python-dotenv>=1.0.0
4
+ rich>=13.0.0
4
5
 
5
6
  [dev]
6
7
  pytest>=8.0.0
@@ -8,7 +9,3 @@ pytest-asyncio>=0.24.0
8
9
 
9
10
  [mcp]
10
11
  mcp>=1.0.0
11
-
12
- [tui]
13
- prompt_toolkit>=3.0.0
14
- rich>=13.0.0