gemcode 0.3.4__tar.gz → 0.3.8__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 (105) hide show
  1. {gemcode-0.3.4/src/gemcode.egg-info → gemcode-0.3.8}/PKG-INFO +1 -1
  2. {gemcode-0.3.4 → gemcode-0.3.8}/pyproject.toml +1 -1
  3. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/agent.py +1 -1
  4. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/repl_commands.py +2 -0
  5. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/repl_slash.py +58 -0
  6. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tui/app.py +65 -7
  7. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tui/scrollback.py +31 -10
  8. {gemcode-0.3.4 → gemcode-0.3.8/src/gemcode.egg-info}/PKG-INFO +1 -1
  9. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_agent_instruction.py +0 -1
  10. {gemcode-0.3.4 → gemcode-0.3.8}/LICENSE +0 -0
  11. {gemcode-0.3.4 → gemcode-0.3.8}/MANIFEST.in +0 -0
  12. {gemcode-0.3.4 → gemcode-0.3.8}/README.md +0 -0
  13. {gemcode-0.3.4 → gemcode-0.3.8}/setup.cfg +0 -0
  14. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/__init__.py +0 -0
  15. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/__main__.py +0 -0
  16. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/audit.py +0 -0
  17. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/autocompact.py +0 -0
  18. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/callbacks.py +0 -0
  19. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/capability_routing.py +0 -0
  20. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/cli.py +0 -0
  21. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/compaction.py +0 -0
  22. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/computer_use/__init__.py +0 -0
  23. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/computer_use/browser_computer.py +0 -0
  24. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/config.py +0 -0
  25. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/context_budget.py +0 -0
  26. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/context_warning.py +0 -0
  27. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/credentials.py +0 -0
  28. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/hitl_session.py +0 -0
  29. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/interactions.py +0 -0
  30. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/invoke.py +0 -0
  31. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/kairos_daemon.py +0 -0
  32. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/limits.py +0 -0
  33. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/live_audio_engine.py +0 -0
  34. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/logging_config.py +0 -0
  35. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/mcp_loader.py +0 -0
  36. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/memory/__init__.py +0 -0
  37. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/memory/embedding_memory_service.py +0 -0
  38. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/memory/file_memory_service.py +0 -0
  39. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/modality_tools.py +0 -0
  40. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/model_errors.py +0 -0
  41. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/model_routing.py +0 -0
  42. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/paths.py +0 -0
  43. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/permissions.py +0 -0
  44. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/plugins/__init__.py +0 -0
  45. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  46. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  47. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/prompt_suggestions.py +0 -0
  48. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/query/__init__.py +0 -0
  49. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/query/config.py +0 -0
  50. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/query/deps.py +0 -0
  51. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/query/engine.py +0 -0
  52. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/query/stop_hooks.py +0 -0
  53. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/query/token_budget.py +0 -0
  54. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/query/transitions.py +0 -0
  55. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/session_runtime.py +0 -0
  56. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/slash_commands.py +0 -0
  57. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/thinking.py +0 -0
  58. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tool_prompt_manifest.py +0 -0
  59. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tool_registry.py +0 -0
  60. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tools/__init__.py +0 -0
  61. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tools/edit.py +0 -0
  62. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tools/filesystem.py +0 -0
  63. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tools/search.py +0 -0
  64. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tools/shell.py +0 -0
  65. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tools/shell_gate.py +0 -0
  66. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tools/todo.py +0 -0
  67. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/tools_inspector.py +0 -0
  68. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/trust.py +0 -0
  69. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/version.py +0 -0
  70. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/vertex.py +0 -0
  71. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/web/__init__.py +0 -0
  72. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/web/claude_sse_adapter.py +0 -0
  73. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/web/terminal_repl.py +0 -0
  74. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode/workspace_hints.py +0 -0
  75. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode.egg-info/SOURCES.txt +0 -0
  76. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode.egg-info/dependency_links.txt +0 -0
  77. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode.egg-info/entry_points.txt +0 -0
  78. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode.egg-info/requires.txt +0 -0
  79. {gemcode-0.3.4 → gemcode-0.3.8}/src/gemcode.egg-info/top_level.txt +0 -0
  80. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_autocompact.py +0 -0
  81. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_capability_routing.py +0 -0
  82. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_claude_web_adapter_sse.py +0 -0
  83. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_cli_init.py +0 -0
  84. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_computer_use_permissions.py +0 -0
  85. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_context_budget.py +0 -0
  86. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_context_warning.py +0 -0
  87. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_credentials.py +0 -0
  88. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_interactive_permission_ask.py +0 -0
  89. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_kairos_scheduler.py +0 -0
  90. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_modality_tools.py +0 -0
  91. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_model_error_retry.py +0 -0
  92. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_model_errors.py +0 -0
  93. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_model_routing.py +0 -0
  94. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_paths.py +0 -0
  95. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_permissions.py +0 -0
  96. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_prompt_suggestions.py +0 -0
  97. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_repl_commands.py +0 -0
  98. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_repl_slash.py +0 -0
  99. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_slash_commands.py +0 -0
  100. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_thinking_config.py +0 -0
  101. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_token_budget.py +0 -0
  102. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_tool_context_circulation.py +0 -0
  103. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_tools.py +0 -0
  104. {gemcode-0.3.4 → gemcode-0.3.8}/tests/test_tools_inspector.py +0 -0
  105. {gemcode-0.3.4 → gemcode-0.3.8}/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.4
3
+ Version: 0.3.8
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.4"
7
+ version = "0.3.8"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -65,7 +65,7 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
65
65
  model = (getattr(cfg, "model", None) or "").strip() or "(default)"
66
66
  return f"""## Runtime facts (authoritative for this session)
67
67
  - **Project root** — every filesystem tool path is relative to: `{root}`
68
- - **Model id in use:** `{model}`. Changing it requires restarting GemCode with `--model <id>` or env `GEMCODE_MODEL`, or using `/model` in the REPL for routing info. Shell: `gemcode list-models` lists candidate ids.
68
+ - **Model id in use:** `{model}`. In this TUI/REPL you can override it for subsequent turns with `/model use <id>` (use `/model list` to browse IDs). For a full restart you can still use `--model <id>` or env `GEMCODE_MODEL`.
69
69
  - **UI banner** phrases such as "GemCode Pro" are **terminal marketing**, not a separate API tier or model you enable from chat.
70
70
  - **Env toggles** (`GEMCODE_ENABLE_COMPUTER_USE`, `GEMCODE_MODEL`, etc.) affect only the **OS process** that launched `gemcode`. Pasting `VAR=1` in chat does **not** reconfigure a running session—tell the user to export in their shell, use project `.env`, or restart the CLI.
71
71
  - **Working in subfolders** — use tools: e.g. `list_directory("Desktop")`, `glob_files("**/query.ts")`, `read_file("testing/ai-edtech-app/src/app/page.tsx")`, or `run_command` with `cwd_subdir`. Never claim the sandbox cannot reach a subpath unless a tool returned an explicit error."""
@@ -174,6 +174,8 @@ def slash_help_lines() -> list[str]:
174
174
  " /tools List tool inventory for this config",
175
175
  " /doctor Environment sanity check",
176
176
  " /model Show model routing info",
177
+ " /model use <id> Override model for this REPL session",
178
+ " /model list List available Gemini model IDs",
177
179
  " /permissions Show permission / HITL settings",
178
180
  " /memory Show persistent memory settings",
179
181
  " /hooks Show post-turn hook configuration",
@@ -81,7 +81,65 @@ async def process_repl_slash(
81
81
  return ReplSlashResult(skip_model_turn=True)
82
82
 
83
83
  if name in ("model", "models"):
84
+ args = (sc.args or "").strip()
85
+ if not args:
86
+ out("\n".join(format_model_lines(cfg)))
87
+ out()
88
+ return ReplSlashResult(skip_model_turn=True)
89
+
90
+ parts = args.split()
91
+ sub = parts[0].lower()
92
+ if sub in ("use", "set") and len(parts) >= 2:
93
+ new_model = " ".join(parts[1:]).strip()
94
+ if not new_model:
95
+ out("Usage: /model use <model-id>")
96
+ out()
97
+ return ReplSlashResult(skip_model_turn=True)
98
+ # Persist override for this session; pick_effective_model() respects this.
99
+ cfg.model = new_model
100
+ setattr(cfg, "model_overridden", True)
101
+ out(f"model: {cfg.model}")
102
+ out("model_overridden: True")
103
+ out("Note: this applies to subsequent turns in this REPL session.")
104
+ out()
105
+ return ReplSlashResult(skip_model_turn=True)
106
+
107
+ if sub in ("list", "ls", "show"):
108
+ # Best-effort list: query Gemini via the same API used by GemCode.
109
+ show_all = "--show-all" in parts or "--show-all" in args
110
+ try:
111
+ from gemcode.config import load_cli_environment
112
+
113
+ load_cli_environment()
114
+ except Exception:
115
+ pass
116
+ from gemcode.cli import require_google_api_key
117
+
118
+ require_google_api_key()
119
+ from google.genai import Client
120
+
121
+ client = Client(api_key=os.environ["GOOGLE_API_KEY"])
122
+ models = client.models.list()
123
+ out("Available models:")
124
+ for m in models:
125
+ name = getattr(m, "name", None)
126
+ actions = getattr(m, "supported_actions", None)
127
+ if not name:
128
+ continue
129
+ if not show_all and actions and isinstance(actions, list):
130
+ # Keep only models that support generateContent-style generation.
131
+ if "generateContent" not in actions:
132
+ continue
133
+ if actions and isinstance(actions, list):
134
+ out(f" {name}\t{','.join(actions)}")
135
+ else:
136
+ out(f" {name}")
137
+ out()
138
+ return ReplSlashResult(skip_model_turn=True)
139
+
140
+ # Fallback: show current routing info.
84
141
  out("\n".join(format_model_lines(cfg)))
142
+ out("Tip: /model use <model-id> to override for this session.")
85
143
  out()
86
144
  return ReplSlashResult(skip_model_turn=True)
87
145
 
@@ -179,6 +179,8 @@ async def run_gemcode_tui(
179
179
 
180
180
  # Non-modal permission prompt state. Modal dialogs can corrupt a full-screen TUI.
181
181
  pending_confirm: dict[str, object] = {"future": None, "tool": "", "hint": ""}
182
+ assistant_busy: dict[str, bool] = {"value": False}
183
+ spinner_idx: dict[str, int] = {"value": 0}
182
184
 
183
185
  def _set_input_prompt() -> None:
184
186
  if pending_confirm.get("future") is not None:
@@ -218,6 +220,15 @@ async def run_gemcode_tui(
218
220
  ("class:muted", " "),
219
221
  ("class:muted", "(Esc cancels)"),
220
222
  ]
223
+ if assistant_busy.get("value"):
224
+ frames = ["|", "/", "-", "\\"]
225
+ fr = frames[spinner_idx.get("value", 0) % len(frames)]
226
+ return [
227
+ ("class:muted", " "),
228
+ ("class:pill", f"thinking {fr}"),
229
+ ("class:muted", " "),
230
+ ("class:muted", "Tip: Esc=interrupt"),
231
+ ]
221
232
  return [
222
233
  ("class:muted", " "),
223
234
  ("class:pill", f"🌿 {_git_branch()}" if _git_branch() else "📁 no-git"),
@@ -228,6 +239,18 @@ async def run_gemcode_tui(
228
239
  status.content = FormattedTextControl(_status_text)
229
240
  _set_input_prompt()
230
241
 
242
+ async def _spin_status() -> None:
243
+ frames = ["|", "/", "-", "\\"]
244
+ i = 0
245
+ while assistant_busy.get("value"):
246
+ spinner_idx["value"] = i % len(frames)
247
+ i += 1
248
+ try:
249
+ app.invalidate()
250
+ except Exception:
251
+ pass
252
+ await asyncio.sleep(0.12)
253
+
231
254
  input_help = Window(
232
255
  height=1,
233
256
  dont_extend_height=True,
@@ -523,6 +546,9 @@ async def run_gemcode_tui(
523
546
  apply_capability_routing(cfg, prompt, context="prompt")
524
547
  cfg.model = pick_effective_model(cfg, prompt)
525
548
 
549
+ assistant_busy["value"] = True
550
+ spinner_task = asyncio.create_task(_spin_status())
551
+
526
552
  try:
527
553
  REQUEST_CONFIRMATION_FC = "adk_request_confirmation"
528
554
  # Terminal width for stable box rendering.
@@ -602,12 +628,20 @@ async def run_gemcode_tui(
602
628
 
603
629
  assistant_started = False
604
630
 
631
+ def _normalize_ws(s: str) -> str:
632
+ # For Gemini, "thinking" and final text can sometimes be identical.
633
+ # Normalize whitespace so we can detect exact duplicates robustly.
634
+ return " ".join((s or "").split()).strip().lower()
635
+
605
636
  while True:
606
637
  # Stream events from ADK runner.
607
638
  events: list = []
608
- # Buffer assistant text for this pass. If a confirmation is requested,
609
- # we discard buffered text to avoid the noisy "rerun with --yes" spiel.
610
- buffered: list[str] = []
639
+ # Buffer assistant text for this pass.
640
+ # Claude differentiates "thinking" from the final response, and we
641
+ # also do that here by routing streamed parts with `part.thought=True`
642
+ # into a separate buffer.
643
+ buffered_thought: list[str] = []
644
+ buffered_final: list[str] = []
611
645
  kwargs = dict(
612
646
  user_id="local",
613
647
  session_id=session_state["id"],
@@ -637,7 +671,10 @@ async def run_gemcode_tui(
637
671
  if not delta:
638
672
  continue
639
673
  assistant_started = True
640
- buffered.append(delta)
674
+ if getattr(part, "thought", None):
675
+ buffered_thought.append(delta)
676
+ else:
677
+ buffered_final.append(delta)
641
678
  except Exception:
642
679
  continue
643
680
 
@@ -648,10 +685,25 @@ async def run_gemcode_tui(
648
685
  # Handle in-TUI tool confirmations (HITL) Claude-style.
649
686
  confirmation_fcs = _get_confirmation_fcs(events)
650
687
  if not confirmation_fcs:
651
- # Now that we know no confirmation is needed, render buffered text.
652
- if buffered:
688
+ # Now that we know no confirmation is needed, render buffered
689
+ # thinking + final response separately.
690
+ thought_text = "".join(buffered_thought)
691
+ final_text = "".join(buffered_final)
692
+ if buffered_thought:
693
+ # If Gemini returns the same content for both "thought" and
694
+ # final text, don't repeat it (Claude typically doesn't).
695
+ if buffered_final and _normalize_ws(thought_text) == _normalize_ws(final_text):
696
+ append_inline("⎿ GemCode (thinking): ")
697
+ await typewrite("(omitted: identical to final response)")
698
+ append("")
699
+ else:
700
+ append_inline("⎿ GemCode (thinking): ")
701
+ await typewrite(thought_text)
702
+ # Ensure visual separation before the final response section.
703
+ append("")
704
+ if buffered_final:
653
705
  append_inline("⎿ GemCode: ")
654
- await typewrite("".join(buffered))
706
+ await typewrite("".join(buffered_final))
655
707
  break
656
708
 
657
709
  interactive_enabled = bool(getattr(cfg, "interactive_permission_ask", False))
@@ -724,6 +776,12 @@ async def run_gemcode_tui(
724
776
  append("\033[2m" + ("─" * max(40, min(cw - 2, 200))) + "\033[0m")
725
777
  except Exception as e:
726
778
  append(f"GemCode: error: {e}\n")
779
+ finally:
780
+ assistant_busy["value"] = False
781
+ try:
782
+ spinner_task.cancel()
783
+ except Exception:
784
+ pass
727
785
 
728
786
  @kb.add("enter")
729
787
  def _enter(event) -> None:
@@ -346,16 +346,18 @@ async def run_gemcode_scrollback_tui(
346
346
  apply_capability_routing(cfg, prompt, context="prompt")
347
347
  cfg.model = pick_effective_model(cfg, prompt)
348
348
 
349
- # Start streaming assistant output.
350
- sys.stdout.write(f" ⎿ {ansi.bold}GemCode{ansi.reset}: ")
351
- sys.stdout.flush()
352
-
353
349
  current_message = types.Content(role="user", parts=[types.Part(text=prompt)])
354
350
  do_reset = True
351
+ def _normalize_ws(s: str) -> str:
352
+ # Gemini can sometimes return identical content for both "thinking" and
353
+ # final text; normalize whitespace to detect exact duplicates.
354
+ return " ".join((s or "").split()).strip().lower()
355
355
 
356
356
  while True:
357
357
  events: list = []
358
358
  assistant_wrote_text = False
359
+ buffered_thought: list[str] = []
360
+ buffered_final: list[str] = []
359
361
  kwargs = dict(
360
362
  user_id="local", session_id=current_session_id, new_message=current_message
361
363
  )
@@ -373,9 +375,13 @@ async def run_gemcode_scrollback_tui(
373
375
  continue
374
376
  for part in ev.content.parts:
375
377
  delta = getattr(part, "text", None)
376
- if delta:
377
- assistant_wrote_text = True
378
- await typewrite(delta)
378
+ if not delta:
379
+ continue
380
+ assistant_wrote_text = True
381
+ if getattr(part, "thought", None):
382
+ buffered_thought.append(delta)
383
+ else:
384
+ buffered_final.append(delta)
379
385
  except Exception:
380
386
  continue
381
387
 
@@ -387,6 +393,24 @@ async def run_gemcode_scrollback_tui(
387
393
 
388
394
  confirmation_fcs = _get_confirmation_fcs(events)
389
395
  if not confirmation_fcs:
396
+ # Render buffered thinking and final response separately.
397
+ thought_text = "".join(buffered_thought)
398
+ final_text = "".join(buffered_final)
399
+ if buffered_thought:
400
+ if buffered_final and _normalize_ws(thought_text) == _normalize_ws(final_text):
401
+ print(f" ⎿ {ansi.dim}{ansi.bold}GemCode{ansi.reset} (thinking): {ansi.reset}(omitted: identical to final response)")
402
+ print("")
403
+ else:
404
+ sys.stdout.write(f" ⎿ {ansi.dim}{ansi.bold}GemCode{ansi.reset} (thinking): ")
405
+ sys.stdout.flush()
406
+ await typewrite(thought_text)
407
+ sys.stdout.write("\n")
408
+ sys.stdout.flush()
409
+
410
+ if buffered_final:
411
+ sys.stdout.write(f" ⎿ {ansi.bold}GemCode{ansi.reset}: ")
412
+ sys.stdout.flush()
413
+ await typewrite(final_text)
390
414
  break
391
415
 
392
416
  interactive_enabled = bool(getattr(cfg, "interactive_permission_ask", False))
@@ -414,9 +438,6 @@ async def run_gemcode_scrollback_tui(
414
438
  f" ⎿ Allow? ({ansi.blue_ok}y{ansi.reset}/{ansi.dim}N{ansi.reset}) "
415
439
  ).strip().lower()
416
440
  ok = ans in ("y", "yes")
417
- # Resume the assistant indent after permission prompt.
418
- sys.stdout.write(f" ⎿ {ansi.bold}GemCode{ansi.reset}: ")
419
- sys.stdout.flush()
420
441
 
421
442
  parts.append(
422
443
  types.Part(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.4
3
+ Version: 0.3.8
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -9,5 +9,4 @@ def test_instruction_includes_runtime_facts(tmp_path: Path) -> None:
9
9
  text = build_instruction(cfg)
10
10
  assert str(tmp_path.resolve()) in text
11
11
  assert "gemini-2.5-flash" in text
12
- assert "list-models" in text
13
12
  assert "GEMCODE_MODEL" in text
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