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.
- {gemcode-0.3.10/src/gemcode.egg-info → gemcode-0.3.12}/PKG-INFO +1 -1
- {gemcode-0.3.10 → gemcode-0.3.12}/pyproject.toml +1 -1
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/cli.py +7 -1
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tui/app.py +157 -51
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tui/scrollback.py +33 -14
- 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.10 → gemcode-0.3.12/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode.egg-info/SOURCES.txt +2 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/LICENSE +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/MANIFEST.in +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/README.md +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/setup.cfg +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/agent.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/config.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/version.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_credentials.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_paths.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_permissions.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_tools.py +0 -0
- {gemcode-0.3.10 → gemcode-0.3.12}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.10 → 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
|
|
|
@@ -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=
|
|
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
|
|
244
|
+
("class:muted", "(Esc to cancel)"),
|
|
222
245
|
]
|
|
223
246
|
if assistant_busy.get("value"):
|
|
224
|
-
|
|
225
|
-
|
|
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:
|
|
229
|
-
("class:muted", "
|
|
230
|
-
("class:muted", "
|
|
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"🌿 {
|
|
235
|
-
("class:muted", " "),
|
|
236
|
-
("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"),
|
|
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
|
-
|
|
244
|
-
i = 0
|
|
285
|
+
"""Animate spinner matching OpenClaude's exact behavior."""
|
|
245
286
|
while assistant_busy.get("value"):
|
|
246
|
-
|
|
247
|
-
|
|
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 "
|
|
282
|
-
|
|
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
|
-
"
|
|
337
|
-
"
|
|
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
|
|
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+
|
|
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
|
-
#
|
|
669
|
-
#
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
|
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
|
-
|
|
725
|
-
append("")
|
|
825
|
+
pending_thinking_text["value"] = "(omitted: identical to final response)"
|
|
726
826
|
else:
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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 #
|
|
842
|
-
"accent": "bold #
|
|
843
|
-
"muted": "#
|
|
844
|
-
"sep": "#
|
|
845
|
-
"pill": "bold #
|
|
846
|
-
"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
|
|
847
953
|
}
|
|
848
954
|
)
|
|
849
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")
|
|
@@ -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(
|
|
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(
|
|
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)
|
|
@@ -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
|