nanocode-cli 0.9.4__tar.gz → 0.9.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.9.4
3
+ Version: 0.9.6
4
4
  Summary: A small terminal coding agent written in Python
5
5
  Author-email: hit9 <hit9@icloud.com>
6
6
  License-Expression: BSD-3-Clause
@@ -76,7 +76,7 @@ except ImportError: # pragma: no cover - optional highlighting dependency
76
76
  pygments = None
77
77
  Token = None # keep the name defined so class-body/token lookups don't NameError
78
78
 
79
- __version__ = "0.9.4"
79
+ __version__ = "0.9.6"
80
80
 
81
81
  Json = dict[str, Any]
82
82
 
@@ -3210,8 +3210,10 @@ class BashTool(Tool):
3210
3210
  threading.Thread(target=drain_pipe, args=(stderr_pipe,), daemon=True).start()
3211
3211
  partial_stdout = "".join(stdout_parts)
3212
3212
  partial_stderr = "".join(stderr_parts)
3213
- note = f"backgrounded after {self.session.settings.bash_wait_timeout}s; still running as {job_id}. Use Job(action=\"wait\"|\"status\"|\"kill\", job=\"{job_id}\")."
3214
- partial_stderr = (partial_stderr + ("\n" if partial_stderr else "") + note)
3213
+ note = (
3214
+ f'backgrounded after {self.session.settings.bash_wait_timeout}s; still running as {job_id}. Use Job(action="wait"|"status"|"kill", job="{job_id}").'
3215
+ )
3216
+ partial_stderr = partial_stderr + ("\n" if partial_stderr else "") + note
3215
3217
  return self.process_result("BashToolResult", -1, partial_stdout, partial_stderr)
3216
3218
 
3217
3219
  def drain_selector(self, selector: selectors.BaseSelector, stdout_parts: list[str], stderr_parts: list[str]) -> None:
@@ -6286,7 +6288,7 @@ REVIEW MODE:
6286
6288
  FINAL:
6287
6289
  - Be concise: lead with the result, often 1-3 lines, no preamble/recap/filler.
6288
6290
  - Note changed files and checks run (or not run).
6289
- - Use GitHub-flavored Markdown: flat lists (`1. 2. 3.`), backticks for code/paths, info strings on code blocks, clickable file links `[app.py](/abs/path/app.py:12)` without backticks or file://, vscode://, https://.
6291
+ - Use GitHub-flavored Markdown: flat lists (`1. 2. 3.`), backticks for code/paths, info strings on code blocks, clickable file links `[app.py](/abs/path/app.py:12)` without backticks or file://, vscode://, https://. Write http(s) URLs bare (terminal auto-links them); `[text](url)` prints as `text (url)` here.
6290
6292
  - No emoji/em dash unless asked; no "X rather than Y" framing; no trailing "If you want".
6291
6293
  - The user doesn't see raw outputs; summarize when asked. If you couldn't do something, say so.
6292
6294
  - LANGUAGE (strict): write in the user's current natural language, detected per turn. Keep code, identifiers, paths, shell commands, and tool/API names verbatim — translate only prose.
@@ -6609,6 +6611,20 @@ class UiPrinter:
6609
6611
  # as a solid band. We track the SGR bg state per token and only strip whitespace rendered with
6610
6612
  # bg off.
6611
6613
  SGR_RE: ClassVar[re.Pattern[str]] = re.compile(r"\x1b\[([0-9;]*)m")
6614
+ # OSC / APC / DCS / SOS / PM sequences are terminal control strings that prompt_toolkit's ANSI
6615
+ # parser doesn't recognize. When they slip through Rich's output (OSC 8 hyperlinks were the
6616
+ # historical culprit, iTerm image escapes / Kitty graphics / shell-integration marks are
6617
+ # potential future ones), pt eats the ESC framing but leaks the payload as visible garbage
6618
+ # (e.g. `8;id=…;https://…;;` for OSC 8). Strip these up front so pt only ever sees CSI escapes.
6619
+ # The trade is that any legitimate uses of these (clickable hyperlinks, inline images) never
6620
+ # reach the terminal — but they weren't working through pt anyway; better clean than garbled.
6621
+ NON_CSI_ESCAPE_RE: ClassVar[re.Pattern[str]] = re.compile(
6622
+ r"\x1b[\]_PX^][^\x07\x1b]*(?:\x07|\x1b\\)"
6623
+ )
6624
+
6625
+ @classmethod
6626
+ def strip_unknown_escapes(cls, text: str) -> str:
6627
+ return cls.NON_CSI_ESCAPE_RE.sub("", text)
6612
6628
 
6613
6629
  @classmethod
6614
6630
  def strip_trailing_pad(cls, text: str) -> str:
@@ -6662,7 +6678,7 @@ class UiPrinter:
6662
6678
  console = Console(force_terminal=True, width=shutil.get_terminal_size().columns)
6663
6679
  with console.capture() as capture:
6664
6680
  self.render_message(console, text, role, rule, indent)
6665
- print_formatted_text(ANSI(self.strip_trailing_pad(capture.get())), end="", flush=True)
6681
+ print_formatted_text(ANSI(self.strip_unknown_escapes(self.strip_trailing_pad(capture.get()))), end="", flush=True)
6666
6682
 
6667
6683
  @staticmethod
6668
6684
  def indent_message(text: str, role: str = "", indent: int = 0) -> str:
@@ -6678,13 +6694,13 @@ class UiPrinter:
6678
6694
  console.print("")
6679
6695
  console.print(Padding(RichText(UiPrinter.USER_LOG_PREFIX + text, style=self.user_log_style()), (0, 0, 0, len(margin))))
6680
6696
  elif role == "assistant":
6681
- content = RichText(text, style="red") if error else Markdown(text)
6697
+ content = RichText(text, style="red") if error else Markdown(text, hyperlinks=False)
6682
6698
  console.print(Padding(content, (0, 0, 0, len(margin))))
6683
6699
  else:
6684
6700
  if role:
6685
6701
  label = RichText(role + ":", style=self.MESSAGE_ROLE_STYLES.get(role, "bright_black"))
6686
6702
  console.print(Padding(label, (0, 0, 0, len(margin))))
6687
- content = RichText(text, style="red") if error else Markdown(text)
6703
+ content = RichText(text, style="red") if error else Markdown(text, hyperlinks=False)
6688
6704
  console.print(Padding(content, (0, 0, 0, len(margin))))
6689
6705
 
6690
6706
  def emit_markdown(self, text: str) -> None:
@@ -6696,8 +6712,8 @@ class UiPrinter:
6696
6712
  return
6697
6713
  console = Console(force_terminal=True, width=shutil.get_terminal_size().columns)
6698
6714
  with console.capture() as capture:
6699
- console.print(Markdown(text))
6700
- print_formatted_text(ANSI(self.strip_trailing_pad(capture.get())), end="", flush=True)
6715
+ console.print(Markdown(text, hyperlinks=False))
6716
+ print_formatted_text(ANSI(self.strip_unknown_escapes(self.strip_trailing_pad(capture.get()))), end="", flush=True)
6701
6717
 
6702
6718
  @staticmethod
6703
6719
  def tab_segments(titles: tuple[str, ...], active: int) -> list[tuple[str, str]]:
@@ -6904,18 +6920,35 @@ class UiPrinter:
6904
6920
  DIFF_GUTTER_WIDTH: ClassVar[int] = 12
6905
6921
 
6906
6922
  def diff_segments(self, text: str, row_width: int | None = None) -> list[tuple[str, str]]:
6923
+ return self._diff_segments(text, row_width=row_width, live=False)
6924
+
6925
+ def diff_segments_live(self, text: str, row_width: int | None = None) -> list[tuple[str, str]]:
6926
+ """Same as diff_segments, but pads the bg band to the current pane width. Only for live
6927
+ full-screen renderers that repaint on resize (the `/diff` viewer). Scrollback callers must
6928
+ NOT use this — baked-in wide padding wraps on a later pane shrink and drops the bg color on
6929
+ the wrapped continuation, which looks broken."""
6930
+ return self._diff_segments(text, row_width=row_width, live=True)
6931
+
6932
+ def _diff_segments(self, text: str, *, row_width: int | None, live: bool) -> list[tuple[str, str]]:
6907
6933
  segments: list[tuple[str, str]] = []
6908
6934
  old_line: int | None = None
6909
6935
  new_line: int | None = None
6910
6936
  lines = text.splitlines()
6911
- # Pad the bg band to the current pane width for a solid edge-to-edge look. Scrollback bakes
6912
- # the width in, so a later resize can zigzag same tradeoff we already accept for Rich
6913
- # syntax-highlighted code blocks that come out of the strip_trailing_pad path. Callers pass
6914
- # `row_width` when they know the width available AFTER their own outer prefix (nested log
6915
- # trees, tree edges) so the padding doesn't overflow and wrap onto a phantom second row.
6937
+ natural_changed_width = max(
6938
+ (get_cwidth(line) for line in lines if line.startswith(("+", "-")) and not line.startswith(("+++", "---"))),
6939
+ default=1,
6940
+ )
6916
6941
  if row_width is None:
6917
6942
  row_width = shutil.get_terminal_size((120, 20)).columns - 3
6918
- changed_width = max(1, row_width - self.DIFF_GUTTER_WIDTH)
6943
+ available_changed_width = max(1, row_width - self.DIFF_GUTTER_WIDTH)
6944
+ # Scrollback path: cap the bg band at the widest actual changed line, and drop bg entirely
6945
+ # if that would already exceed the pane — a later resize can't wrap what wasn't padded. Live
6946
+ # path (the /diff viewer): fill edge-to-edge with the current pane width; the viewer repaints
6947
+ # on resize, so wide padding stays fresh.
6948
+ if live:
6949
+ changed_width: int | None = available_changed_width
6950
+ else:
6951
+ changed_width = natural_changed_width if natural_changed_width <= available_changed_width else None
6919
6952
 
6920
6953
  # Determine the target file path from the diff header. The `+++` line
6921
6954
  # names the resulting file; for created files `---` is /dev/null.
@@ -6973,7 +7006,7 @@ class UiPrinter:
6973
7006
  for style, piece in content_hl:
6974
7007
  segments.append((styled(style), piece))
6975
7008
  width = get_cwidth(prefix) + sum(get_cwidth(piece) for _style, piece in content_hl)
6976
- padding = " " * max(0, changed_width - width) if background else ""
7009
+ padding = " " * max(0, changed_width - width) if background and changed_width is not None else ""
6977
7010
  segments.append((background if padding else "", padding + suffix))
6978
7011
 
6979
7012
  for index, line in enumerate(lines):
@@ -7578,6 +7611,8 @@ class CommandLoop:
7578
7611
  "Scaffold a fresh config with `nanocode --init-config`.",
7579
7612
  "Launch with `--yolo` to skip confirmations; `/debug` shows recent diagnostics.",
7580
7613
  'Filter MCP servers at launch with `--mcp "name*,!exclude"`.',
7614
+ # Rendering behavior
7615
+ "Markdown links render as `text (url)` — clickable hyperlinks are disabled so the URL stays visible; most terminals auto-link the bare URL.",
7581
7616
  )
7582
7617
 
7583
7618
  def startup_tip(self) -> str:
@@ -7789,9 +7824,7 @@ Tools:
7789
7824
  "class:divider.glow4",
7790
7825
  )
7791
7826
 
7792
- def sweep_divider_fragments(
7793
- self, label: str, width: int | None = None, prefix: list[tuple[str, str]] | None = None
7794
- ) -> list[tuple[str, str]]:
7827
+ def sweep_divider_fragments(self, label: str, width: int | None = None, prefix: list[tuple[str, str]] | None = None) -> list[tuple[str, str]]:
7795
7828
  prefix = prefix or []
7796
7829
  prefix_len = sum(len(text) for _style, text in prefix)
7797
7830
  cols = shutil.get_terminal_size((80, 20)).columns
@@ -8863,7 +8896,7 @@ Tools:
8863
8896
  status, path, diff = sections[state.file]
8864
8897
  parts.append(("", "\n"))
8865
8898
  parts.append(("ansicyan", f" {status.title()} · {path}\n"))
8866
- lines = self.ui.segment_lines(self.ui.diff_segments(diff))
8899
+ lines = self.ui.segment_lines(self.ui.diff_segments_live(diff))
8867
8900
  visible = state.view.visible(lines, viewport())
8868
8901
  for line in visible:
8869
8902
  parts.extend(line)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nanocode-cli
3
- Version: 0.9.4
3
+ Version: 0.9.6
4
4
  Summary: A small terminal coding agent written in Python
5
5
  Author-email: hit9 <hit9@icloud.com>
6
6
  License-Expression: BSD-3-Clause
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nanocode-cli"
7
- version = "0.9.4"
7
+ version = "0.9.6"
8
8
  description = "A small terminal coding agent written in Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes