python-agent-harness 1.5.6.6__tar.gz → 1.5.6.7__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 (85) hide show
  1. {python_agent_harness-1.5.6.6/python_agent_harness.egg-info → python_agent_harness-1.5.6.7}/PKG-INFO +1 -1
  2. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/pyproject.toml +1 -1
  3. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/__init__.py +1 -1
  4. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/core/models.py +17 -0
  5. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/core/token_estimator.py +4 -2
  6. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/llm/client.py +15 -8
  7. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/session/config.py +5 -0
  8. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tui/core.py +56 -10
  9. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tui/input.py +31 -1
  10. python_agent_harness-1.5.6.7/python_agent_harness/tui/mouse.py +406 -0
  11. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tui/render.py +544 -24
  12. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7/python_agent_harness.egg-info}/PKG-INFO +1 -1
  13. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness.egg-info/SOURCES.txt +1 -0
  14. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/LICENSE +0 -0
  15. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/README.md +0 -0
  16. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/__main__.py +0 -0
  17. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/core/__init__.py +0 -0
  18. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/core/agent.py +0 -0
  19. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/core/context_manager.py +0 -0
  20. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/core/planmode.py +0 -0
  21. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/core/subagent.py +0 -0
  22. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/core/tool_runner.py +0 -0
  23. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/entry/__init__.py +0 -0
  24. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/entry/cli.py +0 -0
  25. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/entry/controller.py +0 -0
  26. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/entry/headless.py +0 -0
  27. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/entry/restore.py +0 -0
  28. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/entry/server.py +0 -0
  29. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/entry/view.py +0 -0
  30. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/io/__init__.py +0 -0
  31. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/io/attachments.py +0 -0
  32. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/io/diffrender.py +0 -0
  33. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/io/persistence.py +0 -0
  34. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/io/text_filter.py +0 -0
  35. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/llm/__init__.py +0 -0
  36. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/mcp/__init__.py +0 -0
  37. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/mcp/client.py +0 -0
  38. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/mcp/config.py +0 -0
  39. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/mcp/manager.py +0 -0
  40. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/__init__.py +0 -0
  41. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/agent.md +0 -0
  42. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/agents/reviewer.md +0 -0
  43. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/build-switch.md +0 -0
  44. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/commands/explain.md +0 -0
  45. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/compact.md +0 -0
  46. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/core.py +0 -0
  47. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/initialize.md +0 -0
  48. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/plan-mode.md +0 -0
  49. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/plan.md +0 -0
  50. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/review.md +0 -0
  51. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/subagent.md +0 -0
  52. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/summary.md +0 -0
  53. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/task-completion-rules.md +0 -0
  54. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/prompts/title.md +0 -0
  55. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/session/__init__.py +0 -0
  56. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/session/commands.py +0 -0
  57. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/session/session.py +0 -0
  58. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/__init__.py +0 -0
  59. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/agent_tool.py +0 -0
  60. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/base.py +0 -0
  61. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/bash.py +0 -0
  62. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/diffapply.py +0 -0
  63. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/edit.py +0 -0
  64. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/edit_mac.py +0 -0
  65. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/filesystem.py +0 -0
  66. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/glob.py +0 -0
  67. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/glob_mac.py +0 -0
  68. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/grep.py +0 -0
  69. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/grep_mac.py +0 -0
  70. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/mcp.py +0 -0
  71. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/planexit.py +0 -0
  72. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/question.py +0 -0
  73. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/read.py +0 -0
  74. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/skill.py +0 -0
  75. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/todo.py +0 -0
  76. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tools/write.py +0 -0
  77. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tui/__init__.py +0 -0
  78. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness/tui/commands.py +0 -0
  79. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness.egg-info/dependency_links.txt +0 -0
  80. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness.egg-info/entry_points.txt +0 -0
  81. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness.egg-info/requires.txt +0 -0
  82. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/python_agent_harness.egg-info/top_level.txt +0 -0
  83. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/setup.cfg +0 -0
  84. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/tests/test_invariants.py +0 -0
  85. {python_agent_harness-1.5.6.6 → python_agent_harness-1.5.6.7}/tests/test_scenarios.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-agent-harness
3
- Version: 1.5.6.6
3
+ Version: 1.5.6.7
4
4
  Summary: A lightweight, hackable mini-OpenCode written in Python.
5
5
  Author-email: Huming Chen <chenhuming@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-agent-harness"
7
- version = "1.5.6.6"
7
+ version = "1.5.6.7"
8
8
  description = "A lightweight, hackable mini-OpenCode written in Python."
9
9
  authors = [
10
10
  {name = "Huming Chen", email = "chenhuming@gmail.com"}
@@ -7,7 +7,7 @@ from .mcp.config import MCPConfig, MCPServerConfig
7
7
  from .mcp.manager import MCPManager
8
8
  from .session.session import Session
9
9
 
10
- __version__ = "1.5.6.6"
10
+ __version__ = "1.5.6.7"
11
11
 
12
12
  __all__ = [
13
13
  "Client",
@@ -35,6 +35,23 @@ class AgentMode(enum.Enum):
35
35
  PLAN = "plan"
36
36
 
37
37
 
38
+ def extract_reasoning(obj: dict[str, Any]) -> str:
39
+ """Reasoning text from a backend response object, or "".
40
+
41
+ Backends disagree on the field name: most (DeepSeek, GLM, ...) use
42
+ ``reasoning_content``; Qwen uses ``reasoning``. This is the single
43
+ place that knows the variants — callers (the LLM client's streaming
44
+ and non-streaming parsers, token estimation) must not open-code the
45
+ keys. Non-string values (some backends send a list of parts) are
46
+ dropped, not coerced.
47
+ """
48
+ for key in ("reasoning_content", "reasoning"):
49
+ value = obj.get(key)
50
+ if isinstance(value, str) and value:
51
+ return value
52
+ return ""
53
+
54
+
38
55
  @dataclass
39
56
  class TextPart:
40
57
  """A plain-text content part in a multimodal message."""
@@ -12,6 +12,7 @@ import os
12
12
  import re
13
13
 
14
14
  from ..session import config
15
+ from .models import extract_reasoning
15
16
 
16
17
 
17
18
  def is_cjk_char(c: str) -> bool:
@@ -128,8 +129,9 @@ def payload_text(system: object, messages: list[dict], tools: list[dict]) -> str
128
129
  if isinstance(v, str):
129
130
  buf.append(v)
130
131
  break
131
- reasoning = msg.get("reasoning_content")
132
- if isinstance(reasoning, str):
132
+ # currently unreachable: Message.to_api() never emits reasoning (and strips it from content)
133
+ reasoning = extract_reasoning(msg)
134
+ if reasoning:
133
135
  buf.append(reasoning)
134
136
  for tc in msg.get("tool_calls") or []:
135
137
  fn = tc.get("function", {}) if isinstance(tc, dict) else {}
@@ -24,7 +24,15 @@ from typing import Any, Protocol, runtime_checkable
24
24
 
25
25
  import httpx
26
26
 
27
- from ..core.models import ImagePart, Message, TextPart, ToolCall, ToolSpec, Usage
27
+ from ..core.models import (
28
+ ImagePart,
29
+ Message,
30
+ TextPart,
31
+ ToolCall,
32
+ ToolSpec,
33
+ Usage,
34
+ extract_reasoning,
35
+ )
28
36
  from ..session import config
29
37
 
30
38
 
@@ -875,11 +883,12 @@ class Client:
875
883
  content_parts.append(delta["content"])
876
884
  if on_delta:
877
885
  on_delta(delta["content"])
878
- if delta.get("reasoning_content"):
879
- content_parts.append(delta["reasoning_content"])
880
- reasoning_parts.append(delta["reasoning_content"])
886
+ reasoning = extract_reasoning(delta)
887
+ if reasoning:
888
+ content_parts.append(reasoning)
889
+ reasoning_parts.append(reasoning)
881
890
  if on_delta:
882
- on_delta(delta["reasoning_content"])
891
+ on_delta(reasoning)
883
892
  for tc in delta.get("tool_calls") or []:
884
893
  self._absorb_tool_call(
885
894
  tc_index, self._slot_for(tc_index, tc, 0), tc, on_tool_call
@@ -921,7 +930,7 @@ class Client:
921
930
  tc_index: dict[int, dict[str, Any]] = {}
922
931
  choice = (data.get("choices") or [{}])[0]
923
932
  msg = choice.get("message") or {}
924
- reasoning = msg.get("reasoning_content") or ""
933
+ reasoning = extract_reasoning(msg)
925
934
  content = msg.get("content") or ""
926
935
  # some backends return content as a list of parts (multimodal);
927
936
  # normalize to plain text like Message.text() does, so the
@@ -931,8 +940,6 @@ class Client:
931
940
  p.get("text", "") if isinstance(p, dict) else (p if isinstance(p, str) else "")
932
941
  for p in content
933
942
  )
934
- if not isinstance(reasoning, str):
935
- reasoning = ""
936
943
  # mirror the streaming order: reasoning first, then the answer
937
944
  if reasoning:
938
945
  reasoning_parts.append(reasoning)
@@ -203,6 +203,11 @@ SUBAGENT_EXCLUDED_TOOLS = ("Agent", "Question", "PlanExit", "TodoWrite")
203
203
  TOOL_RESULT_PREVIEW_LINES = 5 # max lines of a tool result shown in the TUI
204
204
  TOOL_RESULT_PREVIEW_CHARS = 500 # max chars of that preview (long single lines)
205
205
 
206
+ # Per-argument value cap for a tool call the user expanded: the full
207
+ # command/payload is shown, but a runaway (e.g. a 200KB argument) is
208
+ # still trimmed so one row cannot flood the panel.
209
+ TOOL_EXPANDED_ARG_CHARS = 4000
210
+
206
211
  # ---- default agent prompts -----------------------------------------------------
207
212
  # Ported system prompts (opencode-style) for the main agent and sub-agents,
208
213
  # bundled with the package (prompts/agent.md, prompts/subagent.md).
@@ -32,6 +32,7 @@ from ..session import config
32
32
  from ..session.session import Session
33
33
  from .commands import CommandMixin
34
34
  from .input import InputMixin, SlashCompleter, UiQuestion, _history_path, _make_prompt_session
35
+ from .mouse import MouseSession, apply_typed
35
36
  from .render import RenderMixin, safe_markup
36
37
 
37
38
  # Unexpected UI exceptions in a row (no successful input read in
@@ -81,6 +82,19 @@ class Tui(RenderMixin, InputMixin, CommandMixin):
81
82
  # prompts/agents/ directory; refreshed on each /agent call so files
82
83
  # added at runtime are picked up.
83
84
  self._discovered_agents: dict[str, str] = {}
85
+ # Tool calls the user expanded by clicking their ▶ row: their
86
+ # panel rows carry every argument in full instead of the
87
+ # truncated one-line label.
88
+ self._expanded_tools: set[str] = set()
89
+ # Last rendered frame, so a mouse click can be traced back to
90
+ # the row it landed on.
91
+ self._last_frame: Any = None
92
+ # Mouse session of the run in progress (None outside a run): a
93
+ # click has to ask the terminal where the panel is on screen.
94
+ self._mouse: MouseSession | None = None
95
+ # Text typed while the mouse reader owned the terminal during a
96
+ # run: replayed into the next input prompt instead of dropped.
97
+ self._typed_during_run = ""
84
98
  self.prompt_session = _make_prompt_session(
85
99
  FileHistory(_history_path(str(self._controller.project_dir))),
86
100
  SlashCompleter(lambda: str(self._controller.project_dir)),
@@ -377,6 +391,16 @@ class Tui(RenderMixin, InputMixin, CommandMixin):
377
391
  self.console.print()
378
392
  self._flush()
379
393
 
394
+ def _collect_typed(self, text: str) -> None:
395
+ """Keep text typed while the mouse reader owned the terminal.
396
+
397
+ With mouse reporting on, the run owns stdin in cbreak mode, so
398
+ keystrokes would otherwise be read by the mouse reader and lost.
399
+ They are buffered here and offered as the next prompt's default
400
+ instead (see ``InputMixin._read_multiline``).
401
+ """
402
+ self._typed_during_run = apply_typed(self._typed_during_run, text)
403
+
380
404
  def _abandon_run(self) -> None:
381
405
  """Cancel the active run and release what it holds (UI side)."""
382
406
  self._controller.cancel()
@@ -401,25 +425,47 @@ class Tui(RenderMixin, InputMixin, CommandMixin):
401
425
  the moment new stream text arrives, so the text pushes the
402
426
  scroll immediately instead of waiting for a fixed tick. The
403
427
  short timeout keeps the spinner animating between data bursts.
428
+
429
+ While the panel is live, terminal mouse reporting is on
430
+ (``mouse.MouseSession``) so clicking a ``▶`` tool row expands or
431
+ collapses it. Reporting is switched off whenever the input
432
+ prompt needs the terminal (a pending question) and always on the
433
+ way out, so normal text selection and typing behave as usual
434
+ outside a run.
404
435
  """
436
+ mouse = MouseSession(
437
+ self.console,
438
+ self._on_mouse_click,
439
+ on_text=self._collect_typed,
440
+ )
441
+ self._mouse = mouse
405
442
  with Live(
406
443
  self._render_frame(),
407
444
  console=self.console,
408
445
  refresh_per_second=30,
409
446
  screen=False,
410
447
  ) as live:
411
- while worker.is_alive():
412
- if self.question is not None:
413
- live.stop()
414
- self._ask_question_blocking()
415
- live.start()
416
- continue
417
- self._data_event.wait(timeout=0.1)
418
- self._data_event.clear()
448
+ mouse.enable()
449
+ try:
450
+ while worker.is_alive():
451
+ if self.question is not None:
452
+ live.stop()
453
+ # prompt_toolkit owns stdin here: mouse reports
454
+ # would land in the answer buffer as garbage
455
+ mouse.disable()
456
+ self._ask_question_blocking()
457
+ mouse.enable()
458
+ live.start()
459
+ continue
460
+ self._data_event.wait(timeout=0.1)
461
+ self._data_event.clear()
462
+ live.update(self._render_frame())
463
+ self._flush()
419
464
  live.update(self._render_frame())
420
465
  self._flush()
421
- live.update(self._render_frame())
422
- self._flush()
466
+ finally:
467
+ mouse.disable()
468
+ self._mouse = None
423
469
  # run finished: the last Live frame stays on screen, but the
424
470
  # in-place redraws never reached the scrollback — print the
425
471
  # full conversation so the user can scroll back through it
@@ -381,9 +381,14 @@ class InputMixin:
381
381
  )
382
382
 
383
383
  def _read_multiline(self) -> str | None:
384
+ # Text typed while a run held the terminal (mouse reporting on)
385
+ # is offered as the default buffer content rather than dropped.
386
+ default = self._pending_default()
384
387
  try:
385
388
  with _safe_patch_stdout():
386
- text = self.prompt_session.prompt(self._input_prompt(), multiline=True)
389
+ text = self.prompt_session.prompt(
390
+ self._input_prompt(), multiline=True, default=default
391
+ )
387
392
  except EOFError:
388
393
  # Ctrl-D: quit
389
394
  return None
@@ -393,6 +398,31 @@ class InputMixin:
393
398
  return ""
394
399
  return text
395
400
 
401
+ def _pending_default(self) -> str:
402
+ """Text typed during the last run, consumed once.
403
+
404
+ Line breaks typed during the run are kept between lines, but
405
+ stripped at the ends: pressing Enter while waiting (to "wake"
406
+ the terminal, or to submit) would otherwise seed the prompt
407
+ with blank lines, and leading whitespace would hide a
408
+ ``/command`` from the slash dispatcher.
409
+ """
410
+ typed = getattr(self, "_typed_during_run", "")
411
+ if typed:
412
+ self._typed_during_run = ""
413
+ # Leading blank lines never matter; leading spaces only go in
414
+ # front of a "/command" (they would hide it from dispatch), so
415
+ # pasted indented code keeps its first line's indentation. At
416
+ # the end only line breaks go -- a trailing space is part of a
417
+ # sentence the user goes on typing.
418
+ lines = typed.split("\n")
419
+ while lines and not lines[0].strip():
420
+ lines.pop(0)
421
+ if lines and lines[0].lstrip().startswith("/"):
422
+ lines[0] = lines[0].lstrip()
423
+ typed = "\n".join(lines).rstrip("\n")
424
+ return typed if typed.strip() else ""
425
+
396
426
  def _ask_question_blocking(self) -> None:
397
427
  """Answer the pending question on the main thread.
398
428