gemcode 0.3.10__tar.gz → 0.3.12__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 (107) hide show
  1. {gemcode-0.3.10/src/gemcode.egg-info → gemcode-0.3.12}/PKG-INFO +1 -1
  2. {gemcode-0.3.10 → gemcode-0.3.12}/pyproject.toml +1 -1
  3. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/cli.py +7 -1
  4. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tui/app.py +157 -51
  5. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tui/scrollback.py +33 -14
  6. gemcode-0.3.12/src/gemcode/tui/spinner.py +150 -0
  7. gemcode-0.3.12/src/gemcode/tui/startup_screen.py +196 -0
  8. {gemcode-0.3.10 → gemcode-0.3.12/src/gemcode.egg-info}/PKG-INFO +1 -1
  9. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode.egg-info/SOURCES.txt +2 -0
  10. {gemcode-0.3.10 → gemcode-0.3.12}/LICENSE +0 -0
  11. {gemcode-0.3.10 → gemcode-0.3.12}/MANIFEST.in +0 -0
  12. {gemcode-0.3.10 → gemcode-0.3.12}/README.md +0 -0
  13. {gemcode-0.3.10 → gemcode-0.3.12}/setup.cfg +0 -0
  14. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/__init__.py +0 -0
  15. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/__main__.py +0 -0
  16. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/agent.py +0 -0
  17. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/audit.py +0 -0
  18. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/autocompact.py +0 -0
  19. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/callbacks.py +0 -0
  20. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/capability_routing.py +0 -0
  21. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/compaction.py +0 -0
  22. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/computer_use/__init__.py +0 -0
  23. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/computer_use/browser_computer.py +0 -0
  24. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/config.py +0 -0
  25. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/context_budget.py +0 -0
  26. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/context_warning.py +0 -0
  27. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/credentials.py +0 -0
  28. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/hitl_session.py +0 -0
  29. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/interactions.py +0 -0
  30. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/invoke.py +0 -0
  31. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/kairos_daemon.py +0 -0
  32. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/limits.py +0 -0
  33. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/live_audio_engine.py +0 -0
  34. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/logging_config.py +0 -0
  35. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/mcp_loader.py +0 -0
  36. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/memory/__init__.py +0 -0
  37. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/memory/embedding_memory_service.py +0 -0
  38. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/memory/file_memory_service.py +0 -0
  39. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/modality_tools.py +0 -0
  40. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/model_errors.py +0 -0
  41. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/model_routing.py +0 -0
  42. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/paths.py +0 -0
  43. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/permissions.py +0 -0
  44. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/plugins/__init__.py +0 -0
  45. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  46. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  47. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/prompt_suggestions.py +0 -0
  48. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/__init__.py +0 -0
  49. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/config.py +0 -0
  50. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/deps.py +0 -0
  51. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/engine.py +0 -0
  52. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/stop_hooks.py +0 -0
  53. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/token_budget.py +0 -0
  54. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/transitions.py +0 -0
  55. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/repl_commands.py +0 -0
  56. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/repl_slash.py +0 -0
  57. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/session_runtime.py +0 -0
  58. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/slash_commands.py +0 -0
  59. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/thinking.py +0 -0
  60. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tool_prompt_manifest.py +0 -0
  61. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tool_registry.py +0 -0
  62. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/__init__.py +0 -0
  63. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/edit.py +0 -0
  64. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/filesystem.py +0 -0
  65. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/search.py +0 -0
  66. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/shell.py +0 -0
  67. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/shell_gate.py +0 -0
  68. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/todo.py +0 -0
  69. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools_inspector.py +0 -0
  70. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/trust.py +0 -0
  71. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/version.py +0 -0
  72. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/vertex.py +0 -0
  73. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/web/__init__.py +0 -0
  74. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/web/claude_sse_adapter.py +0 -0
  75. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/web/terminal_repl.py +0 -0
  76. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/workspace_hints.py +0 -0
  77. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode.egg-info/dependency_links.txt +0 -0
  78. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode.egg-info/entry_points.txt +0 -0
  79. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode.egg-info/requires.txt +0 -0
  80. {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode.egg-info/top_level.txt +0 -0
  81. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_agent_instruction.py +0 -0
  82. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_autocompact.py +0 -0
  83. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_capability_routing.py +0 -0
  84. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_claude_web_adapter_sse.py +0 -0
  85. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_cli_init.py +0 -0
  86. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_computer_use_permissions.py +0 -0
  87. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_context_budget.py +0 -0
  88. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_context_warning.py +0 -0
  89. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_credentials.py +0 -0
  90. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_interactive_permission_ask.py +0 -0
  91. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_kairos_scheduler.py +0 -0
  92. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_modality_tools.py +0 -0
  93. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_model_error_retry.py +0 -0
  94. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_model_errors.py +0 -0
  95. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_model_routing.py +0 -0
  96. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_paths.py +0 -0
  97. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_permissions.py +0 -0
  98. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_prompt_suggestions.py +0 -0
  99. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_repl_commands.py +0 -0
  100. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_repl_slash.py +0 -0
  101. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_slash_commands.py +0 -0
  102. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_thinking_config.py +0 -0
  103. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_token_budget.py +0 -0
  104. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_tool_context_circulation.py +0 -0
  105. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_tools.py +0 -0
  106. {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_tools_inspector.py +0 -0
  107. {gemcode-0.3.10 → gemcode-0.3.12}/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.10
3
+ Version: 0.3.12
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.10"
7
+ version = "0.3.12"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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
@@ -237,7 +241,9 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
237
241
  )
238
242
  else:
239
243
  try:
240
- style = os.environ.get("GEMCODE_TUI_STYLE", "scrollback").strip().lower()
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()
241
247
  if style in ("scrollback", "claude", "claude-like"):
242
248
  from gemcode.tui.scrollback import run_gemcode_scrollback_tui
243
249
 
@@ -77,6 +77,12 @@ async def run_gemcode_tui(
77
77
 
78
78
  interrupted = {"flag": False}
79
79
 
80
+ # Import spinner utilities
81
+ from gemcode.tui.spinner import SpinnerState, format_spinner_line, format_idle_status
82
+
83
+ # Initialize spinner state
84
+ spinner_state = SpinnerState()
85
+
80
86
  def append(text: str) -> None:
81
87
  output.buffer.insert_text(text)
82
88
  if not text.endswith("\n"):
@@ -124,25 +130,36 @@ async def run_gemcode_tui(
124
130
 
125
131
  def header_text():
126
132
  model = getattr(cfg, "model", "") or ""
133
+ # Clean up model display
134
+ if "gemini-" in model:
135
+ model_display = model.replace("gemini-", "Gemini ").replace("-", " ").title()
136
+ else:
137
+ model_display = model or "Gemini 2.0 Flash"
138
+
127
139
  mode = (
128
- "yes"
140
+ "auto"
129
141
  if getattr(cfg, "yes_to_all", False)
130
142
  else "ask"
131
143
  if getattr(cfg, "interactive_permission_ask", False)
132
- else "ro"
144
+ else "read-only"
133
145
  )
134
146
  root = str(getattr(cfg, "project_root", "") or "")
147
+ # Shorten root path if too long
148
+ if len(root) > 40:
149
+ root = "..." + root[-37:]
150
+
135
151
  now = datetime.now().strftime("%a %b %d %H:%M")
136
152
  # Shift+Enter isn't reliably distinguishable across terminals, so we
137
153
  # provide a portable newline binding (Ctrl+J).
138
154
  tips = "Enter=send | Ctrl+J=newline | Esc=interrupt | Ctrl+D=exit"
139
155
  return [
140
- ("class:brand", " GemCode "),
141
- ("", f" model={model or '<auto>'} perm={mode} root={root} {now}\n"),
156
+ ("class:brand", " GemCode "),
157
+ ("", f" {model_display} · perm={mode} · {now}\n"),
158
+ ("class:muted", f" {root}\n"),
142
159
  ("class:muted", f" {tips}"),
143
160
  ]
144
161
 
145
- header = Window(height=2, content=FormattedTextControl(header_text), dont_extend_height=True)
162
+ header = Window(height=3, content=FormattedTextControl(header_text), dont_extend_height=True)
146
163
 
147
164
  _git_cache = {"t": 0.0, "branch": ""}
148
165
 
@@ -180,6 +197,12 @@ async def run_gemcode_tui(
180
197
  # Non-modal permission prompt state. Modal dialogs can corrupt a full-screen TUI.
181
198
  pending_confirm: dict[str, object] = {"future": None, "tool": "", "hint": ""}
182
199
  assistant_busy: dict[str, bool] = {"value": False}
200
+ # Claude-style: hide thinking by default, reveal via Ctrl+O.
201
+ show_thinking: dict[str, bool] = {"value": False}
202
+ thinking_active: dict[str, bool] = {"value": False}
203
+ thinking_start_ts: dict[str, float] = {"value": 0.0}
204
+ pending_thinking_text: dict[str, str | None] = {"value": None}
205
+ pending_thinking_insert_pos: dict[str, int | None] = {"value": None}
183
206
  spinner_idx: dict[str, int] = {"value": 0}
184
207
 
185
208
  def _set_input_prompt() -> None:
@@ -203,7 +226,7 @@ async def run_gemcode_tui(
203
226
  ]
204
227
  return [
205
228
  ("class:muted", " "),
206
- ("class:muted", "Type your message below. Enter=send · Ctrl+J=newline · Ctrl+O=home"),
229
+ ("class:muted", "Type your message below. Enter=send · Ctrl+J=newline · Ctrl+O=show thinking"),
207
230
  ]
208
231
 
209
232
  def _status_text():
@@ -212,44 +235,62 @@ async def run_gemcode_tui(
212
235
  tool = str(pending_confirm.get("tool") or "tool")
213
236
  return [
214
237
  ("class:muted", " "),
215
- ("class:pill", f"Permission: {tool}"),
238
+ ("class:pill", f"Permission: {tool}"),
216
239
  ("class:muted", " "),
217
240
  ("class:accent", "y=approve"),
218
241
  ("class:muted", " "),
219
242
  ("class:accent", "n=deny"),
220
243
  ("class:muted", " "),
221
- ("class:muted", "(Esc cancels)"),
244
+ ("class:muted", "(Esc to cancel)"),
222
245
  ]
223
246
  if assistant_busy.get("value"):
224
- frames = ["|", "/", "-", "\\"]
225
- fr = frames[spinner_idx.get("value", 0) % len(frames)]
247
+ # Calculate elapsed time
248
+ elapsed_s = max(0, int(time.time() - float(thinking_start_ts.get("value", 0.0) or 0.0)))
249
+
250
+ if not show_thinking.get("value") and thinking_active.get("value"):
251
+ # Thinking but hidden - show duration and hint
252
+ return [
253
+ ("class:muted", " "),
254
+ ("class:pill", f"💭 Thinking for {elapsed_s}s"),
255
+ ("class:muted", " "),
256
+ ("class:accent", "Ctrl+O to show"),
257
+ ("class:muted", " · "),
258
+ ("class:muted", "Esc=interrupt"),
259
+ ]
260
+
261
+ # Active spinner - use OpenClaude-style Braille frames
262
+ frame = spinner_state.get_frame()
263
+ verb = spinner_state.verb
226
264
  return [
227
265
  ("class:muted", " "),
228
- ("class:pill", f"thinking {fr}"),
229
- ("class:muted", " "),
230
- ("class:muted", "Tip: Esc=interrupt"),
266
+ ("class:accent", f"{frame}"),
267
+ ("class:muted", f" {verb}…"),
268
+ ("class:muted", " · "),
269
+ ("class:muted", "Esc=interrupt"),
231
270
  ]
271
+
272
+ # Idle status - use OpenClaude format
273
+ git_br = _git_branch()
232
274
  return [
233
275
  ("class:muted", " "),
234
- ("class:pill", f"🌿 {_git_branch()}" if _git_branch() else "📁 no-git"),
235
- ("class:muted", " "),
236
- ("class:muted", "Tip: Ctrl+J=newline Esc=interrupt Ctrl+D=exit"),
276
+ ("class:pill", f"🌿 {git_br}" if git_br else "📁 workspace"),
277
+ ("class:muted", " · "),
278
+ ("class:muted", "Ready · Ctrl+K=home · Ctrl+O=thinking"),
237
279
  ]
238
280
 
239
281
  status.content = FormattedTextControl(_status_text)
240
282
  _set_input_prompt()
241
283
 
242
284
  async def _spin_status() -> None:
243
- frames = ["|", "/", "-", "\\"]
244
- i = 0
285
+ """Animate spinner matching OpenClaude's exact behavior."""
245
286
  while assistant_busy.get("value"):
246
- spinner_idx["value"] = i % len(frames)
247
- i += 1
287
+ spinner_state.advance()
288
+ spinner_idx["value"] = spinner_state.frame_index
248
289
  try:
249
290
  app.invalidate()
250
291
  except Exception:
251
292
  pass
252
- await asyncio.sleep(0.12)
293
+ await asyncio.sleep(0.12) # 120ms like OpenClaude
253
294
 
254
295
  input_help = Window(
255
296
  height=1,
@@ -278,8 +319,11 @@ async def run_gemcode_tui(
278
319
  def _model_display() -> str:
279
320
  m = getattr(cfg, "model", "") or ""
280
321
  if not m:
281
- return "GemCode"
282
- return m.replace("gemini-", "Gemini ").replace("-", ".")
322
+ return "Gemini 2.0 Flash"
323
+ # Clean up model name for display
324
+ if "gemini-" in m:
325
+ return m.replace("gemini-", "Gemini ").replace("-", " ").title()
326
+ return m
283
327
 
284
328
  def _render_home_text():
285
329
  # Recompute with current terminal width for a "dashboard" feel.
@@ -311,10 +355,10 @@ async def run_gemcode_tui(
311
355
 
312
356
  # Tiny "gem" ASCII mark (kept simple for terminal portability).
313
357
  art = [
314
- " ▄▄▄▄ ",
315
- " ▐█ █▌ ",
316
- " ▐█ █▌ ",
317
- " ▀▀▀▀ ",
358
+ " ▄▄▄▄▄▄▄ ",
359
+ " ▐█████████▌ ",
360
+ " ▐█████████▌ ",
361
+ " ▀▀▀▀▀▀▀ ",
318
362
  ]
319
363
  art_w = max(len(x) for x in art)
320
364
 
@@ -333,8 +377,9 @@ async def run_gemcode_tui(
333
377
 
334
378
  tips = [
335
379
  "Tips for getting started",
336
- "First run: trust folder, API key, then .gemcode/",
337
- "Note: Use perm=ask to approve tools",
380
+ "Use /help to see all commands",
381
+ "Ctrl+O toggles thinking display",
382
+ "Ctrl+K toggles this home screen",
338
383
  ]
339
384
  activity = ["Recent activity", "No recent activity"]
340
385
 
@@ -358,9 +403,9 @@ async def run_gemcode_tui(
358
403
  lines.append("│ " + l + " │ " + r + " │")
359
404
  lines.append("│" + (" " * (width - 2)) + "│")
360
405
  lines.append("└" + ("─" * (width - 2)) + "┘")
361
- lines.append("↑ GemCode Pro now supports larger contexts · faster streaming")
406
+ lines.append("↑ GemCode now supports larger contexts · faster streaming · better tools")
362
407
  lines.append("")
363
- lines.append(" ? for shortcuts".ljust(max(0, width - 12)) + "Ctrl+O home")
408
+ lines.append(" ? for shortcuts".ljust(max(0, width - 12)) + "Ctrl+K home")
364
409
 
365
410
  # Prevent overflow: clamp to available rows (leave space for header/input/status).
366
411
  max_lines = max(6, min(len(lines), max(6, rows - 7)))
@@ -413,8 +458,46 @@ async def run_gemcode_tui(
413
458
 
414
459
  # Note: do NOT bind y/n globally. Permission answers are typed into the
415
460
  # input field (perm>) and submitted with Enter, Claude-style.
461
+ async def _flush_pending_thinking() -> None:
462
+ # Insert hidden thinking back into the transcript (before the already-rendered final),
463
+ # so ctrl+o behaves like Claude's "expand" (best-effort).
464
+ if not show_thinking.get("value"):
465
+ return
466
+ t = pending_thinking_text.get("value")
467
+ pos = pending_thinking_insert_pos.get("value")
468
+ if not t or pos is None:
469
+ return
470
+ try:
471
+ output.buffer.cursor_position = int(pos)
472
+ output.buffer.insert_text(t)
473
+ output.buffer.insert_text("\n\n")
474
+ output.buffer.cursor_position = len(output.text)
475
+ pending_thinking_text["value"] = None
476
+ pending_thinking_insert_pos["value"] = None
477
+ try:
478
+ app.invalidate()
479
+ except Exception:
480
+ pass
481
+ except Exception:
482
+ return
483
+
416
484
  @kb.add("c-o")
485
+ def _toggle_thinking(event) -> None:
486
+ show_thinking["value"] = not show_thinking.get("value")
487
+ try:
488
+ event.app.invalidate()
489
+ except Exception:
490
+ pass
491
+ # If we just turned thinking on, flush the most recent pending thinking.
492
+ if show_thinking.get("value") and pending_thinking_text.get("value"):
493
+ try:
494
+ event.app.create_background_task(_flush_pending_thinking())
495
+ except Exception:
496
+ pass
497
+
498
+ @kb.add("c-k")
417
499
  def _toggle_home(event) -> None:
500
+ # Repurposed Ctrl+O for thinking reveal; Ctrl+K keeps the home dashboard toggle.
418
501
  show_home["value"] = not show_home["value"]
419
502
  try:
420
503
  event.app.invalidate()
@@ -570,6 +653,22 @@ async def run_gemcode_tui(
570
653
  cfg.model = pick_effective_model(cfg, prompt)
571
654
 
572
655
  assistant_busy["value"] = True
656
+ spinner_idx["value"] = 0
657
+ try:
658
+ app.invalidate()
659
+ except Exception:
660
+ pass
661
+ # Make the start of a turn visually obvious in the transcript, like
662
+ # OpenClaude's "thinking" row next to the spinner. We still keep the
663
+ # *content* of the thought hidden until Ctrl+O is pressed; this is just
664
+ # a dim/italic label so the user always sees that the model is busy.
665
+ append("\033[2m\033[3m⎿ ∴ Thinking…\033[0m")
666
+
667
+ # Mark thinking start for spinner (OpenClaude-style)
668
+ spinner_state.start_thinking()
669
+ thinking_active["value"] = True
670
+ thinking_start_ts["value"] = time.time()
671
+
573
672
  spinner_task = asyncio.create_task(_spin_status())
574
673
 
575
674
  try:
@@ -665,11 +764,12 @@ async def run_gemcode_tui(
665
764
  # into a separate buffer.
666
765
  buffered_thought: list[str] = []
667
766
  buffered_final: list[str] = []
668
- # Show Claude-like "thinking" section immediately.
669
- # We fill the thought content at the end of the pass (and can omit
670
- # identical-thought/final cases), but the label itself should appear
671
- # right away so there's a visible loading cue.
672
- append_inline(" GemCode (thinking): ")
767
+ # Claude-like thinking header is implicit in the spinner; we only
768
+ # print the full thinking block when the user asks for it via Ctrl+O.
769
+ thinking_active["value"] = True
770
+ thinking_start_ts["value"] = time.time()
771
+ pending_thinking_text["value"] = None
772
+ pending_thinking_insert_pos["value"] = None
673
773
  kwargs = dict(
674
774
  user_id="local",
675
775
  session_id=session_state["id"],
@@ -714,22 +814,26 @@ async def run_gemcode_tui(
714
814
  confirmation_fcs = _get_confirmation_fcs(events)
715
815
  if not confirmation_fcs:
716
816
  # Now that we know no confirmation is needed, render buffered
717
- # thinking + final response separately.
817
+ # thinking + final response. By default we keep thinking hidden
818
+ # (Claude-style); Ctrl+O can later expand it from the buffer.
718
819
  thought_text = "".join(buffered_thought)
719
820
  final_text = "".join(buffered_final)
720
821
  if buffered_thought:
721
822
  # If Gemini returns the same content for both "thought" and
722
823
  # final text, don't repeat it (Claude typically doesn't).
723
824
  if buffered_final and _normalize_ws(thought_text) == _normalize_ws(final_text):
724
- await typewrite("(omitted: identical to final response)")
725
- append("")
825
+ pending_thinking_text["value"] = "(omitted: identical to final response)"
726
826
  else:
727
- await typewrite(thought_text)
728
- # Ensure visual separation before the final response section.
729
- append("")
730
- else:
731
- await typewrite("(no thinking output)")
732
- append("")
827
+ pending_thinking_text["value"] = thought_text
828
+ # If the user already asked to see thinking, insert it now;
829
+ # otherwise remember the insertion point so Ctrl+O can expand.
830
+ if show_thinking.get("value"):
831
+ insert_pos = len(output.text)
832
+ pending_thinking_insert_pos["value"] = insert_pos
833
+ await _flush_pending_thinking()
834
+ else:
835
+ pending_thinking_insert_pos["value"] = len(output.text)
836
+ thinking_active["value"] = False
733
837
  if buffered_final:
734
838
  append_inline("⎿ GemCode: ")
735
839
  await typewrite("".join(buffered_final))
@@ -809,6 +913,8 @@ async def run_gemcode_tui(
809
913
  append(f"GemCode: error: {e}\n")
810
914
  finally:
811
915
  assistant_busy["value"] = False
916
+ thinking_active["value"] = False
917
+ spinner_state.stop_thinking() # Mark thinking end (OpenClaude-style)
812
918
  try:
813
919
  spinner_task.cancel()
814
920
  except Exception:
@@ -838,12 +944,12 @@ async def run_gemcode_tui(
838
944
 
839
945
  style = Style.from_dict(
840
946
  {
841
- "brand": "bold #60a5fa",
842
- "accent": "bold #3b82f6",
843
- "muted": "#6b7280",
844
- "sep": "#1f2937",
845
- "pill": "bold #93c5fd",
846
- "inputframe": "bg:#071426 #e5e7eb",
947
+ "brand": "bold #f0945c", # Sunset orange
948
+ "accent": "bold #f09464", # Warm accent
949
+ "muted": "#786452", # Warm gray
950
+ "sep": "#64503d", # Dark border
951
+ "pill": "bold #d97757", # Warm pill
952
+ "inputframe": "bg:#1a0f0a #e5d5c5", # Warm dark bg with cream text
847
953
  }
848
954
  )
849
955
 
@@ -117,25 +117,28 @@ class _Ansi:
117
117
 
118
118
  @property
119
119
  def blue(self) -> str:
120
- # ANSI 256-color bright-ish blue.
121
- return self.esc("38;5;75")
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
- # Slightly deeper blue for secondary accents.
126
- return self.esc("38;5;33")
125
+ # Deeper warm color
126
+ return self.esc("38;5;209")
127
127
 
128
128
  @property
129
129
  def blue_ok(self) -> str:
130
- return self.esc("38;5;81")
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
- return self.esc("38;5;39")
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
- return self.esc("38;5;69")
140
+ # Warm tool color
141
+ return self.esc("38;5;180")
139
142
 
140
143
 
141
144
  def _term_width(default: int = 100) -> int:
@@ -176,16 +179,18 @@ def _dashboard(cfg) -> str:
176
179
  "",
177
180
  f"Welcome back {user}!",
178
181
  "",
179
- " ▐▛███▜▌",
180
- " ▝▜█████▛▘",
181
- " ▘▘ ▝▝",
182
+ " ▄▄▄▄▄▄▄",
183
+ " ▐█████████▌",
184
+ " ▐█████████▌",
185
+ " ▀▀▀▀▀▀▀",
182
186
  "",
183
- f"{model or 'GemCode'} · Local session",
187
+ f"{model or 'Gemini 2.0 Flash'} · Local session",
184
188
  root,
185
189
  ]
186
190
  right = [
187
191
  "Tips for getting started",
188
192
  "First run creates .gemcode/ (trust + API key)",
193
+ "Use /help to see available commands",
189
194
  "",
190
195
  "Recent activity",
191
196
  "No recent activity",
@@ -202,7 +207,7 @@ def _dashboard(cfg) -> str:
202
207
  lines.append("│" + pad(f" {nt}", w - 2) + "│")
203
208
  lines.append(box_bot)
204
209
  lines.append("")
205
- lines.append(" ↑ GemCode Pro now supports larger contexts · faster streaming")
210
+ lines.append(" ↑ GemCode now supports larger contexts · faster streaming · better tools")
206
211
  lines.append("")
207
212
  return "\n".join(lines)
208
213
 
@@ -247,6 +252,7 @@ async def run_gemcode_scrollback_tui(
247
252
  print(dash)
248
253
 
249
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}")
250
256
  print("")
251
257
 
252
258
  char_delay_ms = int(os.environ.get("GEMCODE_TUI_CHAR_DELAY_MS", "0") or "0")
@@ -417,14 +423,27 @@ async def run_gemcode_scrollback_tui(
417
423
  final_text = "".join(buffered_final)
418
424
  if buffered_thought:
419
425
  if buffered_final and _normalize_ws(thought_text) == _normalize_ws(final_text):
420
- print(f" ⎿ {ansi.dim}{ansi.bold}GemCode{ansi.reset} (thinking): {ansi.reset}(omitted: identical to final response)")
426
+ print(
427
+ f" ⎿ {ansi.dim}{ansi.bold}\u2234 Thinking{ansi.reset}: "
428
+ f"{ansi.reset}(omitted: identical to final response)"
429
+ )
421
430
  print("")
422
431
  else:
423
- sys.stdout.write(f" ⎿ {ansi.dim}{ansi.bold}GemCode{ansi.reset} (thinking): ")
432
+ sys.stdout.write(
433
+ f" ⎿ {ansi.dim}{ansi.bold}\u2234 Thinking{ansi.reset}: "
434
+ )
424
435
  sys.stdout.flush()
425
436
  await typewrite(thought_text)
426
437
  sys.stdout.write("\n")
427
438
  sys.stdout.flush()
439
+ else:
440
+ # Keep thinking visible even if the model produced no separate
441
+ # "thought" channel output.
442
+ print(
443
+ f" ⎿ {ansi.dim}{ansi.bold}\u2234 Thinking{ansi.reset}: "
444
+ "(no thinking output)"
445
+ )
446
+ print("")
428
447
 
429
448
  if buffered_final:
430
449
  sys.stdout.write(f" ⎿ {ansi.bold}GemCode{ansi.reset}: ")
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.10
3
+ Version: 0.3.12
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -71,6 +71,8 @@ src/gemcode/tools/shell_gate.py
71
71
  src/gemcode/tools/todo.py
72
72
  src/gemcode/tui/app.py
73
73
  src/gemcode/tui/scrollback.py
74
+ src/gemcode/tui/spinner.py
75
+ src/gemcode/tui/startup_screen.py
74
76
  src/gemcode/web/__init__.py
75
77
  src/gemcode/web/claude_sse_adapter.py
76
78
  src/gemcode/web/terminal_repl.py
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