gemcode 0.3.11__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.
- {gemcode-0.3.11/src/gemcode.egg-info → gemcode-0.3.12}/PKG-INFO +1 -1
- {gemcode-0.3.11 → gemcode-0.3.12}/pyproject.toml +1 -1
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/cli.py +7 -1
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tui/app.py +83 -42
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tui/scrollback.py +18 -12
- gemcode-0.3.12/src/gemcode/tui/spinner.py +150 -0
- gemcode-0.3.12/src/gemcode/tui/startup_screen.py +196 -0
- {gemcode-0.3.11 → gemcode-0.3.12/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode.egg-info/SOURCES.txt +2 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/LICENSE +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/MANIFEST.in +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/README.md +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/setup.cfg +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/agent.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/config.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/version.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_credentials.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_paths.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_permissions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_tools.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.11 → gemcode-0.3.12}/tests/test_workspace_hints.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
"
|
|
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 "
|
|
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"
|
|
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=
|
|
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
|
|
|
@@ -218,55 +235,62 @@ async def run_gemcode_tui(
|
|
|
218
235
|
tool = str(pending_confirm.get("tool") or "tool")
|
|
219
236
|
return [
|
|
220
237
|
("class:muted", " "),
|
|
221
|
-
("class:pill", f"Permission: {tool}"),
|
|
238
|
+
("class:pill", f"⚠ Permission: {tool}"),
|
|
222
239
|
("class:muted", " "),
|
|
223
240
|
("class:accent", "y=approve"),
|
|
224
241
|
("class:muted", " "),
|
|
225
242
|
("class:accent", "n=deny"),
|
|
226
243
|
("class:muted", " "),
|
|
227
|
-
("class:muted", "(Esc
|
|
244
|
+
("class:muted", "(Esc to cancel)"),
|
|
228
245
|
]
|
|
229
246
|
if assistant_busy.get("value"):
|
|
247
|
+
# Calculate elapsed time
|
|
248
|
+
elapsed_s = max(0, int(time.time() - float(thinking_start_ts.get("value", 0.0) or 0.0)))
|
|
249
|
+
|
|
230
250
|
if not show_thinking.get("value") and thinking_active.get("value"):
|
|
231
|
-
|
|
251
|
+
# Thinking but hidden - show duration and hint
|
|
232
252
|
return [
|
|
233
253
|
("class:muted", " "),
|
|
234
|
-
("class:pill", f"
|
|
254
|
+
("class:pill", f"💭 Thinking for {elapsed_s}s"),
|
|
235
255
|
("class:muted", " "),
|
|
236
|
-
("class:
|
|
256
|
+
("class:accent", "Ctrl+O to show"),
|
|
257
|
+
("class:muted", " · "),
|
|
258
|
+
("class:muted", "Esc=interrupt"),
|
|
237
259
|
]
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
verb = verbs[i % len(verbs)]
|
|
260
|
+
|
|
261
|
+
# Active spinner - use OpenClaude-style Braille frames
|
|
262
|
+
frame = spinner_state.get_frame()
|
|
263
|
+
verb = spinner_state.verb
|
|
243
264
|
return [
|
|
244
265
|
("class:muted", " "),
|
|
245
|
-
("class:
|
|
246
|
-
("class:muted", "
|
|
247
|
-
("class:muted", "
|
|
266
|
+
("class:accent", f"{frame}"),
|
|
267
|
+
("class:muted", f" {verb}…"),
|
|
268
|
+
("class:muted", " · "),
|
|
269
|
+
("class:muted", "Esc=interrupt"),
|
|
248
270
|
]
|
|
271
|
+
|
|
272
|
+
# Idle status - use OpenClaude format
|
|
273
|
+
git_br = _git_branch()
|
|
249
274
|
return [
|
|
250
275
|
("class:muted", " "),
|
|
251
|
-
("class:pill", f"🌿 {
|
|
252
|
-
("class:muted", " "),
|
|
253
|
-
("class:muted", "
|
|
276
|
+
("class:pill", f"🌿 {git_br}" if git_br else "📁 workspace"),
|
|
277
|
+
("class:muted", " · "),
|
|
278
|
+
("class:muted", "Ready · Ctrl+K=home · Ctrl+O=thinking"),
|
|
254
279
|
]
|
|
255
280
|
|
|
256
281
|
status.content = FormattedTextControl(_status_text)
|
|
257
282
|
_set_input_prompt()
|
|
258
283
|
|
|
259
284
|
async def _spin_status() -> None:
|
|
260
|
-
|
|
261
|
-
i = 0
|
|
285
|
+
"""Animate spinner matching OpenClaude's exact behavior."""
|
|
262
286
|
while assistant_busy.get("value"):
|
|
263
|
-
|
|
264
|
-
|
|
287
|
+
spinner_state.advance()
|
|
288
|
+
spinner_idx["value"] = spinner_state.frame_index
|
|
265
289
|
try:
|
|
266
290
|
app.invalidate()
|
|
267
291
|
except Exception:
|
|
268
292
|
pass
|
|
269
|
-
await asyncio.sleep(0.12)
|
|
293
|
+
await asyncio.sleep(0.12) # 120ms like OpenClaude
|
|
270
294
|
|
|
271
295
|
input_help = Window(
|
|
272
296
|
height=1,
|
|
@@ -295,8 +319,11 @@ async def run_gemcode_tui(
|
|
|
295
319
|
def _model_display() -> str:
|
|
296
320
|
m = getattr(cfg, "model", "") or ""
|
|
297
321
|
if not m:
|
|
298
|
-
return "
|
|
299
|
-
|
|
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
|
|
300
327
|
|
|
301
328
|
def _render_home_text():
|
|
302
329
|
# Recompute with current terminal width for a "dashboard" feel.
|
|
@@ -328,10 +355,10 @@ async def run_gemcode_tui(
|
|
|
328
355
|
|
|
329
356
|
# Tiny "gem" ASCII mark (kept simple for terminal portability).
|
|
330
357
|
art = [
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"
|
|
334
|
-
"
|
|
358
|
+
" ▄▄▄▄▄▄▄ ",
|
|
359
|
+
" ▐█████████▌ ",
|
|
360
|
+
" ▐█████████▌ ",
|
|
361
|
+
" ▀▀▀▀▀▀▀ ",
|
|
335
362
|
]
|
|
336
363
|
art_w = max(len(x) for x in art)
|
|
337
364
|
|
|
@@ -350,8 +377,9 @@ async def run_gemcode_tui(
|
|
|
350
377
|
|
|
351
378
|
tips = [
|
|
352
379
|
"Tips for getting started",
|
|
353
|
-
"
|
|
354
|
-
"
|
|
380
|
+
"Use /help to see all commands",
|
|
381
|
+
"Ctrl+O toggles thinking display",
|
|
382
|
+
"Ctrl+K toggles this home screen",
|
|
355
383
|
]
|
|
356
384
|
activity = ["Recent activity", "No recent activity"]
|
|
357
385
|
|
|
@@ -375,9 +403,9 @@ async def run_gemcode_tui(
|
|
|
375
403
|
lines.append("│ " + l + " │ " + r + " │")
|
|
376
404
|
lines.append("│" + (" " * (width - 2)) + "│")
|
|
377
405
|
lines.append("└" + ("─" * (width - 2)) + "┘")
|
|
378
|
-
lines.append("↑ GemCode
|
|
406
|
+
lines.append("↑ GemCode now supports larger contexts · faster streaming · better tools")
|
|
379
407
|
lines.append("")
|
|
380
|
-
lines.append(" ? for shortcuts".ljust(max(0, width - 12)) + "Ctrl+
|
|
408
|
+
lines.append(" ? for shortcuts".ljust(max(0, width - 12)) + "Ctrl+K home")
|
|
381
409
|
|
|
382
410
|
# Prevent overflow: clamp to available rows (leave space for header/input/status).
|
|
383
411
|
max_lines = max(6, min(len(lines), max(6, rows - 7)))
|
|
@@ -630,6 +658,17 @@ async def run_gemcode_tui(
|
|
|
630
658
|
app.invalidate()
|
|
631
659
|
except Exception:
|
|
632
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
|
+
|
|
633
672
|
spinner_task = asyncio.create_task(_spin_status())
|
|
634
673
|
|
|
635
674
|
try:
|
|
@@ -874,6 +913,8 @@ async def run_gemcode_tui(
|
|
|
874
913
|
append(f"GemCode: error: {e}\n")
|
|
875
914
|
finally:
|
|
876
915
|
assistant_busy["value"] = False
|
|
916
|
+
thinking_active["value"] = False
|
|
917
|
+
spinner_state.stop_thinking() # Mark thinking end (OpenClaude-style)
|
|
877
918
|
try:
|
|
878
919
|
spinner_task.cancel()
|
|
879
920
|
except Exception:
|
|
@@ -903,12 +944,12 @@ async def run_gemcode_tui(
|
|
|
903
944
|
|
|
904
945
|
style = Style.from_dict(
|
|
905
946
|
{
|
|
906
|
-
"brand": "bold #
|
|
907
|
-
"accent": "bold #
|
|
908
|
-
"muted": "#
|
|
909
|
-
"sep": "#
|
|
910
|
-
"pill": "bold #
|
|
911
|
-
"inputframe": "bg:#
|
|
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
|
|
912
953
|
}
|
|
913
954
|
)
|
|
914
955
|
|
|
@@ -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:
|
|
@@ -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 '
|
|
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
|
|
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")
|
|
@@ -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)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|