gemcode 0.3.41__py3-none-any.whl → 0.3.43__py3-none-any.whl

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.
@@ -41,35 +41,19 @@ INTENT_DESCRIPTIONS: dict[str, str] = {
41
41
  INTENT_ANALYSIS: "Systematic audit or summarisation — thorough tool sweep, then synthesise.",
42
42
  }
43
43
 
44
- # One-line summaries for the TUI — same visual lane as ∴ Thinking (collapsed)
45
- INTENT_THINKING_SUMMARY: dict[str, str] = {
46
- INTENT_GREETING: "Greeting / chitchat — no tools",
47
- INTENT_CONCEPT: "General knowledge — answer without repo reads if possible",
48
- INTENT_PROJECT_QUESTION: "About this repo — a few read-only tools, then answer",
49
- INTENT_ENGINEERING_TASK: "Engineering task — orient → plan → execute → verify",
50
- INTENT_ANALYSIS: "Deep analysis — systematic read / grep / synthesise",
51
- }
52
-
53
44
  # How the intent was determined (for TUI suffix)
54
45
  SOURCE_LOCAL = "local" # obvious greeting / heuristic, no classifier API call
55
46
  SOURCE_LLM = "llm" # gemini-2.5-flash-lite classifier
56
47
  SOURCE_OFF = "off" # classifier disabled
57
48
 
58
49
 
59
- def format_intent_thinking_line(intent: str, source: str) -> str | None:
50
+ def show_intentifying_line(source: str) -> bool:
60
51
  """
61
- Single line of text after ``∴ Intent`` in the TUI (same visual lane as
62
- collapsed ``∴ Thinking``). Returns None when the classifier is off and
63
- nothing should be shown.
52
+ Whether to show the minimal ``∴ Intentifying…`` line in the TUI (same lane
53
+ as ``∴ Thinking``). Hidden only when intent classification is disabled
54
+ (``SOURCE_OFF``).
64
55
  """
65
- if source == SOURCE_OFF:
66
- return None
67
- summary = INTENT_THINKING_SUMMARY.get(intent, intent)
68
- if source == SOURCE_LOCAL:
69
- tag = "instant"
70
- else:
71
- tag = "flash-lite classifier"
72
- return f"{intent} — {summary} · {tag}"
56
+ return source != SOURCE_OFF
73
57
 
74
58
  # ── Prompts ───────────────────────────────────────────────────────────────────
75
59
  _CLASSIFY_PROMPT = """\
@@ -110,6 +94,16 @@ _OBVIOUS_GREETINGS: frozenset[str] = frozenset({
110
94
  "what's up", "whats up", "wassup",
111
95
  })
112
96
 
97
+ # Single-token "control" messages users often type after transient model errors.
98
+ # Treat these as "resume the last task", not greetings.
99
+ _RESUME_WORDS: frozenset[str] = frozenset({
100
+ "continue",
101
+ "go on",
102
+ "resume",
103
+ "retry",
104
+ "try again",
105
+ })
106
+
113
107
 
114
108
  def _classifier_enabled() -> bool:
115
109
  v = os.environ.get("GEMCODE_INTENT_CLASSIFY_ENABLED", "1")
@@ -144,6 +138,8 @@ async def classify_intent_with_source(message: str) -> tuple[str, str]:
144
138
 
145
139
  # Fast local check for unambiguously short greetings — saves an API round-trip.
146
140
  lower = stripped.lower()
141
+ if lower in _RESUME_WORDS:
142
+ return INTENT_ENGINEERING_TASK, SOURCE_LOCAL
147
143
  if lower in _OBVIOUS_GREETINGS or (len(lower) <= 3 and lower.isalpha()):
148
144
  return INTENT_GREETING, SOURCE_LOCAL
149
145
 
gemcode/tui/scrollback.py CHANGED
@@ -251,6 +251,10 @@ async def run_gemcode_scrollback_tui(
251
251
  get_cfg=lambda: cfg,
252
252
  )
253
253
 
254
+ # Turn-scoped monitors/state.
255
+ # Declared up-front so nested render helpers can update them via `nonlocal`.
256
+ last_tool_error: dict | None = None
257
+
254
258
  async def typewrite(text: str) -> None:
255
259
  if not text:
256
260
  return
@@ -386,6 +390,7 @@ async def run_gemcode_scrollback_tui(
386
390
 
387
391
  def _render_tool_results(ev) -> None:
388
392
  """Show a one-line result summary; transition spinner to 'Querying…'."""
393
+ nonlocal last_tool_error
389
394
  try:
390
395
  frs: list = []
391
396
  try:
@@ -409,6 +414,28 @@ async def run_gemcode_scrollback_tui(
409
414
  summary = _fmt_tool_result(resp)
410
415
  if summary:
411
416
  print(f" ⎿ {ansi.dim}\u21b3 {summary}{ansi.reset}")
417
+ # Capture the most recent tool error (best-effort) so we can offer to fix it.
418
+ try:
419
+ d = resp if isinstance(resp, dict) else {}
420
+ inner = d.get("result", d)
421
+ if not isinstance(inner, dict):
422
+ inner = d
423
+ err = inner.get("error") or d.get("error")
424
+ exit_code = inner.get("exit_code")
425
+ stderr = inner.get("stderr") or ""
426
+ if err or (isinstance(exit_code, int) and exit_code != 0):
427
+ full = ""
428
+ if isinstance(err, str) and err.strip():
429
+ full = err.strip()
430
+ elif isinstance(stderr, str) and stderr.strip():
431
+ full = stderr.strip()
432
+ last_tool_error = {
433
+ "tool": getattr(fr, "name", "") or "tool",
434
+ "summary": (summary or str(err) or str(exit_code) or "error")[:120],
435
+ "full": full[:2000],
436
+ }
437
+ except Exception:
438
+ pass
412
439
  # Restart spinner while model re-queries with the tool outputs.
413
440
  _start_anim("Querying\u2026")
414
441
  except Exception:
@@ -446,18 +473,24 @@ async def run_gemcode_scrollback_tui(
446
473
  )
447
474
 
448
475
  current_session_id = session_id
476
+ pending_prompt: str | None = None
477
+ last_user_prompt: str | None = None
449
478
 
450
479
  while True:
451
- try:
452
- prompt = await input_handler.prompt_async()
453
- except EOFError:
454
- print("")
480
+ if pending_prompt:
481
+ prompt = pending_prompt
482
+ pending_prompt = None
483
+ else:
455
484
  try:
456
- from gemcode.hooks import run_session_stop_hook
457
- run_session_stop_hook(cfg.project_root, model=getattr(cfg, "model", "") or "")
458
- except Exception:
459
- pass
460
- return
485
+ prompt = await input_handler.prompt_async()
486
+ except EOFError:
487
+ print("")
488
+ try:
489
+ from gemcode.hooks import run_session_stop_hook
490
+ run_session_stop_hook(cfg.project_root, model=getattr(cfg, "model", "") or "")
491
+ except Exception:
492
+ pass
493
+ return
461
494
  if not prompt:
462
495
  continue
463
496
  if prompt in (":q", "quit", "exit", "/exit"):
@@ -468,6 +501,13 @@ async def run_gemcode_scrollback_tui(
468
501
  pass
469
502
  return
470
503
 
504
+ # Plain-text resume command: rerun the last user message (useful after 503s).
505
+ if (prompt or "").strip().lower() in ("continue", "resume", "retry", "try again", "go on"):
506
+ if last_user_prompt:
507
+ print(f" ⎿ {ansi.dim}↻ Continuing last request…{ansi.reset}")
508
+ print("")
509
+ prompt = last_user_prompt
510
+
471
511
  old_model = getattr(cfg, "model", "")
472
512
  old_model_overridden = bool(getattr(cfg, "model_overridden", False))
473
513
 
@@ -515,21 +555,28 @@ async def run_gemcode_scrollback_tui(
515
555
  continue
516
556
  prompt = slash.model_prompt or prompt
517
557
 
558
+ # Track the last real user request so "continue" can rerun it later.
559
+ try:
560
+ pnorm = (prompt or "").strip()
561
+ if pnorm:
562
+ last_user_prompt = pnorm
563
+ except Exception:
564
+ pass
565
+
518
566
  # ── LLM intent pre-classifier ────────────────────────────────────────────
519
567
  # gemini-2.5-flash-lite classifies the message (same lane as Thinking)
520
568
  try:
521
569
  from gemcode.intent_classifier import (
522
570
  classify_intent_with_source,
523
571
  generate_greeting_reply,
524
- format_intent_thinking_line,
572
+ show_intentifying_line,
525
573
  INTENT_GREETING,
526
574
  INTENT_DESCRIPTIONS,
527
575
  )
528
576
  _intent, _intent_src = await classify_intent_with_source(prompt)
529
- _intent_line = format_intent_thinking_line(_intent, _intent_src)
530
- if _intent_line:
577
+ if show_intentifying_line(_intent_src):
531
578
  print(
532
- f" \u23bf {ansi.dim}\u2234 Intent {_intent_line}{ansi.reset}"
579
+ f" \u23bf {ansi.dim}\u2234 Intentifying\u2026{ansi.reset}"
533
580
  )
534
581
  print("")
535
582
 
@@ -611,6 +658,7 @@ async def run_gemcode_scrollback_tui(
611
658
  assistant_wrote_text = False
612
659
  buffered_thought: list[str] = []
613
660
  buffered_final: list[str] = []
661
+ last_tool_error = None
614
662
  kwargs = dict(
615
663
  user_id="local", session_id=current_session_id, new_message=current_message
616
664
  )
@@ -708,6 +756,40 @@ async def run_gemcode_scrollback_tui(
708
756
  console.print(
709
757
  _RichPadding(_RichMarkdown(final_text), (0, 0, 0, 4)),
710
758
  )
759
+
760
+ # If a tool error occurred during this turn, ask whether to resolve it.
761
+ if last_tool_error:
762
+ try:
763
+ tool_name = last_tool_error.get("tool") or "tool"
764
+ summary = last_tool_error.get("summary") or "an error"
765
+ full = last_tool_error.get("full") or ""
766
+ print("")
767
+ print(
768
+ f" ⎿ {ansi.blue_warn}{ansi.bold}Detected an error{ansi.reset} "
769
+ f"in {ansi.bold}{tool_name}{ansi.reset}: {ansi.dim}{summary}{ansi.reset}"
770
+ )
771
+ sys.stdout.flush()
772
+ prompt_str = (
773
+ f" ⎿ Try to resolve it now? "
774
+ f"[{ansi.blue_ok}y{ansi.reset} = yes "
775
+ f"{ansi.dim}any other key = no{ansi.reset}] "
776
+ )
777
+ sys.stdout.write(prompt_str)
778
+ sys.stdout.flush()
779
+ ok = await _read_permission_char(asyncio.get_running_loop())
780
+ sys.stdout.write(("y" if ok else "n") + "\n")
781
+ sys.stdout.flush()
782
+ if ok:
783
+ pending_prompt = (
784
+ "We encountered an error during the last turn.\n\n"
785
+ f"Tool: {tool_name}\n"
786
+ f"Summary: {summary}\n\n"
787
+ f"{full}\n\n"
788
+ "Please fix the issue. If a command needs to be run, propose it "
789
+ "and ask for confirmation."
790
+ )
791
+ except Exception:
792
+ pass
711
793
  break
712
794
 
713
795
  interactive_enabled = bool(getattr(cfg, "interactive_permission_ask", False))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.41
3
+ Version: 0.3.43
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -13,7 +13,7 @@ gemcode/context_warning.py,sha256=Q8mg5Vojj7EglPhsGAVL7vb8ROLuHVPgdzw25yw-Q2c,42
13
13
  gemcode/credentials.py,sha256=04v-rLD8_Ams69FQdof2FwcL3ZgsroGUnMcHNQFuBZo,1296
14
14
  gemcode/hitl_session.py,sha256=oNiI7odFJGUcmqPavjKLJOEumZKrgklLvwjjrIG9GPg,281
15
15
  gemcode/hooks.py,sha256=FHz175d_18j-4ByZZVdEIagmdOvLHcjDjo7HD2Cikf4,6339
16
- gemcode/intent_classifier.py,sha256=7nwjb69kJV-Z8AmQ-4sKni7ASExeh06-BDrZVqLQ41s,8967
16
+ gemcode/intent_classifier.py,sha256=YfRVEe8gHeKlRkjuSWef1bZ0MPBwyYMp5jymP5Vig5U,8507
17
17
  gemcode/interactions.py,sha256=B0b3QNE_I2i5_HtiebX4ehhjlc4Nbqjf_XbvcTLyJT0,641
18
18
  gemcode/invoke.py,sha256=qnIBAZeFJQhRYmx-IoaXdDGLTSXYMRn6r6CvKNAWBbA,7553
19
19
  gemcode/kairos_daemon.py,sha256=giINipslAIhBtdbqA0o4RYwt4fBsZjtkiKDjp4zSEvw,6980
@@ -73,16 +73,16 @@ gemcode/tools/think.py,sha256=WrNATR-bi97aLkbSsOFOYYAGxbzihe9AnPDZfw3z5-Q,1704
73
73
  gemcode/tools/todo.py,sha256=d9aXiyT04r1RFZIk6qdVif17-_Oc3oi4ymDnsPBRg68,3143
74
74
  gemcode/tools/web.py,sha256=ULg1e3inG4FjPSUCYI8dVBzTrcCHINNRo76SIU9qw-A,4489
75
75
  gemcode/tui/input_handler.py,sha256=VWF92Fe3WkD3QP9kBDY5zrx333Vj2eShO3UO0x2n6eo,10079
76
- gemcode/tui/scrollback.py,sha256=xia_FN66p-OrLh3r0s9orBysma5MiogayibFMGfHRqE,30229
76
+ gemcode/tui/scrollback.py,sha256=oNUKV2VXFEXxVeam49QZoY0r0WrMjJZgu2bKNsWoeZk,33453
77
77
  gemcode/tui/spinner.py,sha256=AJrApG5od-Sh40-5uWcNM9RHb5ax7gr-NbgAZmTbIYY,4848
78
78
  gemcode/tui/welcome_banner.py,sha256=aocl1lnoyLIM6RN4f65g3i0wRA71RqUlgPrGsXeVLW4,4387
79
79
  gemcode/tui/welcome_rich.py,sha256=8FEZzLXrzqly5JWiDgV9ooRV1LNXDk-CXV1a7K6ua-U,4048
80
80
  gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
81
81
  gemcode/web/claude_sse_adapter.py,sha256=HcNp0Lh4DdBZBLOpstsqa-VzfqAUrRngZ6FSuJ-mIMg,8609
82
82
  gemcode/web/terminal_repl.py,sha256=k2irvFGbCY8gDm_pbirR7b_cakaeafcctoTIvnJkVXk,3902
83
- gemcode-0.3.41.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
84
- gemcode-0.3.41.dist-info/METADATA,sha256=CGtENkiu0yb2-sOTv63EcyXeNzJp61VLDzeUqv6EmhI,23695
85
- gemcode-0.3.41.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
86
- gemcode-0.3.41.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
87
- gemcode-0.3.41.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
88
- gemcode-0.3.41.dist-info/RECORD,,
83
+ gemcode-0.3.43.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
84
+ gemcode-0.3.43.dist-info/METADATA,sha256=qVqS1HsxBO5iLKgSwBRzNoQYlnI3ka4oadbjMxLKuNI,23695
85
+ gemcode-0.3.43.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
86
+ gemcode-0.3.43.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
87
+ gemcode-0.3.43.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
88
+ gemcode-0.3.43.dist-info/RECORD,,