nanoPyCodeAgent 0.5.0__tar.gz → 0.5.1__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.
- nanopycodeagent-0.5.1/.github/workflows/ci.yml +41 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/PKG-INFO +1 -1
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/changelogs/0.5.x.md +9 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/src/nanopycodeagent/agent.py +15 -1
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/tests/helpers.py +5 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/tests/test_agent.py +24 -0
- nanopycodeagent-0.5.1/tests/test_line_editing.py +169 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/.agents/skills/land-pr/SKILL.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/.agents/skills/release/SKILL.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/.agents/skills/release/scripts/verify-release.sh +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/.github/workflows/release.yml +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/.gitignore +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/.ignore +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/.python-version +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/.zed/debug.json +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/AGENTS.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/CLAUDE.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/LICENSE +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/README.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/README.zh-CN.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/RELEASING.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/changelogs/0.1.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/changelogs/0.2.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/changelogs/0.3.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/changelogs/0.4.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/changelogs/README.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/dev_notes/README.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/dev_notes/en/0.1.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/dev_notes/en/0.2.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/dev_notes/en/0.3.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/dev_notes/en/0.4.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/dev_notes/en/0.5.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/dev_notes/zh-CN/0.1.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/dev_notes/zh-CN/0.2.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/dev_notes/zh-CN/0.3.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/dev_notes/zh-CN/0.4.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/dev_notes/zh-CN/0.5.x.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/research/README.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/research/agent_tools.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/research/read_tool.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/superpowers/plans/2026-06-21-release-skills.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/superpowers/specs/2026-06-21-release-skill-design.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/superpowers/specs/2026-06-29-config-file-support-design.md +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/pyproject.toml +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/src/nanopycodeagent/__init__.py +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/src/nanopycodeagent/bash_tool.py +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/src/nanopycodeagent/read_tool.py +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/src/nanopycodeagent/settings.py +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/src/nanopycodeagent/terminal.py +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/tests/conftest.py +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/tests/test_bash_tool.py +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/tests/test_read_tool.py +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/tests/test_settings.py +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/tests/test_terminal.py +0 -0
- {nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/uv.lock +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
# Run the test suite on every pull request, and again on main once a change
|
|
4
|
+
# has landed. Read-only: this workflow never publishes anything, so it holds
|
|
5
|
+
# no write scopes of any kind.
|
|
6
|
+
on:
|
|
7
|
+
pull_request:
|
|
8
|
+
push:
|
|
9
|
+
branches:
|
|
10
|
+
- main
|
|
11
|
+
|
|
12
|
+
# A new push to a branch supersedes the run still in flight for it.
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ci-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
test:
|
|
22
|
+
name: pytest (Python ${{ matrix.python-version }})
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
strategy:
|
|
25
|
+
fail-fast: false # one version failing should not hide the other's result
|
|
26
|
+
matrix:
|
|
27
|
+
# The lowest version the project supports, and the newest it can run
|
|
28
|
+
# on, so a break in either direction shows up here.
|
|
29
|
+
python-version: ["3.13", "3.14"]
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v6
|
|
32
|
+
with:
|
|
33
|
+
fetch-depth: 0 # full history + tags so hatch-vcs can derive the version
|
|
34
|
+
- name: Install uv
|
|
35
|
+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
36
|
+
with:
|
|
37
|
+
python-version: ${{ matrix.python-version }}
|
|
38
|
+
- name: Run tests
|
|
39
|
+
# --frozen installs exactly what uv.lock pins and fails if the lock has
|
|
40
|
+
# drifted from pyproject.toml, so CI tests the declared dependencies.
|
|
41
|
+
run: uv run --frozen pytest
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nanoPyCodeAgent
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: A nano code agent built from scratch in pure Python.
|
|
5
5
|
Project-URL: Homepage, https://github.com/minixalpha/nanoPyCodeAgent
|
|
6
6
|
Project-URL: Repository, https://github.com/minixalpha/nanoPyCodeAgent
|
|
@@ -4,6 +4,15 @@ All notable changes in the **0.5.x** release series are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.1] - 2026-07-30
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Backspacing a double-width character (CJK, emoji) at the `You>` prompt no
|
|
11
|
+
longer leaves half of its glyph on screen. The prompt now goes through
|
|
12
|
+
`readline`, which redraws the line, instead of letting the terminal erase
|
|
13
|
+
one column per backspace for a character that occupies two. Arrow-key
|
|
14
|
+
editing and in-session history come with it.
|
|
15
|
+
|
|
7
16
|
## [0.5.0] - 2026-07-29
|
|
8
17
|
|
|
9
18
|
### Added
|
|
@@ -14,6 +14,16 @@ preserved at the ``hardened-agent-loop`` tag.
|
|
|
14
14
|
import os
|
|
15
15
|
from importlib.metadata import PackageNotFoundError, version
|
|
16
16
|
|
|
17
|
+
try:
|
|
18
|
+
# Importing readline routes input() through a line editor that redraws the
|
|
19
|
+
# whole line. Without it the tty erases one column per backspace, which
|
|
20
|
+
# leaves half of a double-width character (CJK, emoji) on screen even
|
|
21
|
+
# though it is gone from the buffer. Editing and history come along for
|
|
22
|
+
# the ride. Not available on every platform, so the import is optional.
|
|
23
|
+
import readline # noqa: F401
|
|
24
|
+
except ImportError: # pragma: no cover - platform without readline
|
|
25
|
+
pass
|
|
26
|
+
|
|
17
27
|
import anthropic
|
|
18
28
|
from anthropic.types import MessageParam, ToolResultBlockParam, ToolUseBlock
|
|
19
29
|
|
|
@@ -105,7 +115,11 @@ def run() -> None:
|
|
|
105
115
|
messages: list[MessageParam] = []
|
|
106
116
|
while True:
|
|
107
117
|
try:
|
|
108
|
-
|
|
118
|
+
# The blank line before the prompt is printed separately: readline
|
|
119
|
+
# measures the prompt to place the cursor, and a newline inside it
|
|
120
|
+
# throws that off.
|
|
121
|
+
print()
|
|
122
|
+
user_input = input("You> ").strip()
|
|
109
123
|
except (EOFError, KeyboardInterrupt):
|
|
110
124
|
print()
|
|
111
125
|
break
|
|
@@ -100,15 +100,20 @@ def patch_client_and_input(monkeypatch, *, client, inputs):
|
|
|
100
100
|
The config file is isolated by the autouse ``_isolate_config`` fixture in
|
|
101
101
|
conftest.py, so ``run()`` sees no config file unless a test writes one to
|
|
102
102
|
``settings.SETTINGS_PATH``.
|
|
103
|
+
|
|
104
|
+
Returns the list that records the prompt of each ``input()`` call.
|
|
103
105
|
"""
|
|
104
106
|
monkeypatch.setattr(anthropic, "Anthropic", lambda *a, **k: client)
|
|
105
107
|
|
|
106
108
|
answers = iter(inputs)
|
|
109
|
+
prompts = []
|
|
107
110
|
|
|
108
111
|
def fake_input(prompt=""):
|
|
112
|
+
prompts.append(prompt)
|
|
109
113
|
try:
|
|
110
114
|
return next(answers)
|
|
111
115
|
except StopIteration as exc: # safety net: behaves like Ctrl-D
|
|
112
116
|
raise EOFError from exc
|
|
113
117
|
|
|
114
118
|
monkeypatch.setattr("builtins.input", fake_input)
|
|
119
|
+
return prompts
|
|
@@ -5,6 +5,8 @@ never spend tokens. We drive ``run()`` by scripting ``input()`` and the client,
|
|
|
5
5
|
then assert on captured stdout and the recorded calls.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
8
10
|
from nanopycodeagent import agent
|
|
9
11
|
|
|
10
12
|
from helpers import (
|
|
@@ -145,6 +147,28 @@ def test_multi_turn_accumulates_history(monkeypatch, capsys):
|
|
|
145
147
|
]
|
|
146
148
|
|
|
147
149
|
|
|
150
|
+
def test_readline_is_imported_for_line_editing():
|
|
151
|
+
# Without readline, input() leaves erasing to the tty, which clears one
|
|
152
|
+
# column per backspace and so strands half of a double-width character.
|
|
153
|
+
pytest.importorskip("readline")
|
|
154
|
+
assert hasattr(agent, "readline")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_prompt_carries_no_embedded_newline(monkeypatch, capsys):
|
|
158
|
+
messages = FakeMessages([])
|
|
159
|
+
client = FakeClient(messages)
|
|
160
|
+
prompts = patch_client_and_input(monkeypatch, client=client, inputs=["/exit"])
|
|
161
|
+
|
|
162
|
+
agent.run()
|
|
163
|
+
|
|
164
|
+
# readline measures the prompt to place the cursor, so the blank line
|
|
165
|
+
# between turns is printed on its own rather than embedded in the prompt.
|
|
166
|
+
assert prompts == ["You> "]
|
|
167
|
+
# The banner's last line is still followed by that blank line. (input() is
|
|
168
|
+
# faked here, so the prompt itself never reaches stdout.)
|
|
169
|
+
assert "or /exit to quit.\n\n" in capsys.readouterr().out
|
|
170
|
+
|
|
171
|
+
|
|
148
172
|
def test_blank_input_is_skipped(monkeypatch, capsys):
|
|
149
173
|
messages = FakeMessages([])
|
|
150
174
|
client = FakeClient(messages)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""End-to-end check that backspace erases wide characters cleanly.
|
|
2
|
+
|
|
3
|
+
Erasing a double-width character (CJK, emoji) used to leave half its glyph on
|
|
4
|
+
screen: the tty clears one column per backspace while the character occupies
|
|
5
|
+
two, so the buffer emptied but the display did not. Showing that needs a real
|
|
6
|
+
terminal, so this test drives the agent over a pty and replays everything it
|
|
7
|
+
writes onto a model of a terminal line — the assertion is on what the user
|
|
8
|
+
would see, not on the escape sequences a particular readline build emits.
|
|
9
|
+
|
|
10
|
+
The child never reaches the API: the session ends at ``/exit``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import locale
|
|
14
|
+
import os
|
|
15
|
+
import select
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import unicodedata
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ctype_is_utf8() -> bool:
|
|
24
|
+
"""Whether this environment's character type is UTF-8.
|
|
25
|
+
|
|
26
|
+
readline only treats a multibyte character as a single unit under a UTF-8
|
|
27
|
+
ctype, so under, say, an explicit ``LC_ALL=C`` there is nothing sensible
|
|
28
|
+
to assert about erasing a CJK character.
|
|
29
|
+
"""
|
|
30
|
+
if not hasattr(locale, "nl_langinfo"): # not POSIX
|
|
31
|
+
return False
|
|
32
|
+
return "utf-8" in locale.nl_langinfo(locale.CODESET).replace("_", "-").lower()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
pytestmark = [
|
|
36
|
+
pytest.mark.skipif(not hasattr(os, "fork"), reason="needs a pty (POSIX only)"),
|
|
37
|
+
pytest.mark.skipif(not _ctype_is_utf8(), reason="needs a UTF-8 locale"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
CHILD = "from nanopycodeagent.agent import run; run()"
|
|
41
|
+
PROMPT = b"You> "
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def render(stream: str) -> str:
|
|
45
|
+
"""Replay a terminal write stream and return the last line it leaves.
|
|
46
|
+
|
|
47
|
+
Enough of a terminal to judge this: printable characters (one cell wide,
|
|
48
|
+
or two for East Asian wide/fullwidth ones), carriage return, backspace,
|
|
49
|
+
and the two escapes a line editor redraws with — ``ESC[<n>G`` to move to
|
|
50
|
+
an absolute column and ``ESC[K`` to erase to end of line.
|
|
51
|
+
"""
|
|
52
|
+
cells: list[str] = [] # one entry per column; "" continues a wide glyph
|
|
53
|
+
col = 0
|
|
54
|
+
i = 0
|
|
55
|
+
while i < len(stream):
|
|
56
|
+
char = stream[i]
|
|
57
|
+
if char == "\x1b" and stream[i + 1 : i + 2] == "[":
|
|
58
|
+
end = i + 2
|
|
59
|
+
while end < len(stream) and not stream[end].isalpha():
|
|
60
|
+
end += 1
|
|
61
|
+
params, final = stream[i + 2 : end], stream[end : end + 1]
|
|
62
|
+
if final == "G":
|
|
63
|
+
col = max(int(params or "1") - 1, 0)
|
|
64
|
+
elif final == "K":
|
|
65
|
+
del cells[col:]
|
|
66
|
+
i = end + 1
|
|
67
|
+
continue
|
|
68
|
+
if char == "\r":
|
|
69
|
+
col = 0
|
|
70
|
+
elif char == "\n":
|
|
71
|
+
cells, col = [], 0 # only the line the prompt sits on matters
|
|
72
|
+
elif char == "\b":
|
|
73
|
+
col = max(col - 1, 0)
|
|
74
|
+
else:
|
|
75
|
+
width = 2 if unicodedata.east_asian_width(char) in "WF" else 1
|
|
76
|
+
cells.extend(" " for _ in range(col + width - len(cells)))
|
|
77
|
+
cells[col] = char
|
|
78
|
+
for offset in range(1, width):
|
|
79
|
+
cells[col + offset] = ""
|
|
80
|
+
col += width
|
|
81
|
+
i += 1
|
|
82
|
+
return "".join(cells).rstrip()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def drive(keystrokes: bytes, home) -> str:
|
|
86
|
+
"""Run the agent on a pty and type ``keystrokes`` at the prompt.
|
|
87
|
+
|
|
88
|
+
Returns everything the child wrote up to that point — the snapshot is
|
|
89
|
+
taken before ``/exit`` is sent, so it is exactly the screen the user is
|
|
90
|
+
looking at mid-edit.
|
|
91
|
+
"""
|
|
92
|
+
import pty
|
|
93
|
+
import termios
|
|
94
|
+
|
|
95
|
+
# The autouse _isolate_config fixture has already cleared the ambient
|
|
96
|
+
# ANTHROPIC_* vars; HOME is redirected so the child cannot read the
|
|
97
|
+
# developer's ~/.nanoPyCodeAgent/settings.json either.
|
|
98
|
+
env = {**os.environ, "HOME": str(home), "ANTHROPIC_API_KEY": "sk-test"}
|
|
99
|
+
|
|
100
|
+
pid, fd = pty.fork()
|
|
101
|
+
if pid == 0: # child: replaced by exec, or dies trying
|
|
102
|
+
try:
|
|
103
|
+
os.execve(sys.executable, [sys.executable, "-c", CHILD], env)
|
|
104
|
+
finally:
|
|
105
|
+
os._exit(1)
|
|
106
|
+
|
|
107
|
+
attrs = termios.tcgetattr(fd)
|
|
108
|
+
attrs[0] |= termios.IUTF8 # multibyte-aware ERASE, as a real terminal has
|
|
109
|
+
termios.tcsetattr(fd, termios.TCSANOW, attrs)
|
|
110
|
+
|
|
111
|
+
output = b""
|
|
112
|
+
|
|
113
|
+
def drain(seconds: float) -> bool:
|
|
114
|
+
"""Read what is ready within ``seconds``; False once the child is done."""
|
|
115
|
+
nonlocal output
|
|
116
|
+
if not select.select([fd], [], [], seconds)[0]:
|
|
117
|
+
return True # nothing to read yet, but the child is still up
|
|
118
|
+
try:
|
|
119
|
+
chunk = os.read(fd, 4096)
|
|
120
|
+
except OSError: # child exited and closed its end
|
|
121
|
+
return False
|
|
122
|
+
if not chunk:
|
|
123
|
+
return False
|
|
124
|
+
output += chunk
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
def wait_for(marker: bytes, timeout: float = 10.0) -> None:
|
|
128
|
+
deadline = time.monotonic() + timeout
|
|
129
|
+
while marker not in output and time.monotonic() < deadline:
|
|
130
|
+
if not drain(0.1):
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
def settle(quiet: float = 0.3, timeout: float = 5.0) -> None:
|
|
134
|
+
"""Read until the child has written nothing for ``quiet`` seconds."""
|
|
135
|
+
deadline = time.monotonic() + timeout
|
|
136
|
+
while time.monotonic() < deadline:
|
|
137
|
+
before = len(output)
|
|
138
|
+
if not drain(quiet) or len(output) == before:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
wait_for(PROMPT)
|
|
143
|
+
assert PROMPT in output, f"never reached the prompt: {output!r}"
|
|
144
|
+
os.write(fd, keystrokes)
|
|
145
|
+
settle()
|
|
146
|
+
screen = output.decode("utf-8", "replace")
|
|
147
|
+
os.write(fd, b"/exit\r")
|
|
148
|
+
wait_for(b"Bye!", timeout=5.0)
|
|
149
|
+
finally:
|
|
150
|
+
os.close(fd)
|
|
151
|
+
os.waitpid(pid, 0)
|
|
152
|
+
return screen
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_backspace_clears_a_wide_character(tmp_path):
|
|
156
|
+
# Type two CJK characters, then erase both.
|
|
157
|
+
screen = drive("你好".encode() + b"\x7f\x7f", tmp_path)
|
|
158
|
+
|
|
159
|
+
# Nothing of either character is left beside the prompt. Before the fix
|
|
160
|
+
# two backspaces cleared two columns of the four they had filled, so
|
|
161
|
+
# "You> 你" stayed on screen after the input was already empty.
|
|
162
|
+
assert render(screen) == PROMPT.decode().rstrip()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_backspace_keeps_the_characters_it_should(tmp_path):
|
|
166
|
+
# Erasing one of three leaves the other two intact and correctly placed.
|
|
167
|
+
screen = drive("中文字".encode() + b"\x7f", tmp_path)
|
|
168
|
+
|
|
169
|
+
assert render(screen) == "You> 中文"
|
|
File without changes
|
|
File without changes
|
{nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/.agents/skills/release/scripts/verify-release.sh
RENAMED
|
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
|
{nanopycodeagent-0.5.0 → nanopycodeagent-0.5.1}/docs/superpowers/plans/2026-06-21-release-skills.md
RENAMED
|
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
|