Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion firstcoder/agent/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from firstcoder.skills.session import replay_loaded_skills


DEFAULT_BASE_RULES = "你是 FirstCoder,一个本地 AI coding agent。请遵守项目规则并优先保持上下文可恢复。"
DEFAULT_BASE_RULES = "你是 PytestPilot,一个面向 Python/pytest 项目的本地 AI coding agent。请遵守项目规则并优先保持上下文可恢复。"


@dataclass(slots=True)
Expand Down
10 changes: 5 additions & 5 deletions firstcoder/app/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class CurrentSessionLike(Protocol):

@dataclass(slots=True)
class FirstCoderTuiConfig:
title: str = "FirstCoder"
title: str = "PytestPilot"
provider_name: str | None = None
provider_model: str | None = None
project_name: str | None = None
Expand Down Expand Up @@ -165,7 +165,7 @@ def on_mount(self) -> None:
self.title = self.config.title
self._refresh_session_subtitle()
self._write_line(
"FirstCoder ready. Commands: /sessions, /session, /resume, /share, /rename, "
"PytestPilot ready. Commands: /sessions, /session, /resume, /share, /rename, "
"/context, /compact status, /compact",
classes="message system-message",
)
Expand Down Expand Up @@ -443,7 +443,7 @@ def _topbar_width(self) -> int | None:
def _topbar_text(self, *, session_id: str | None = None, width: int | None = None) -> str:
if session_id is None and self.current_session is not None:
session_id = self.current_session.session_id
brand = "[#7bba55]FirstCoder[/]"
brand = "[#7bba55]PytestPilot[/]"
status = _activity_markup(self._activity_text)
metadata_parts = [f"[#6e6d72]{escape(_short_session_id(session_id) if session_id else 'no session')}[/]"]
if self.config.provider_name or self.config.provider_model:
Expand Down Expand Up @@ -841,7 +841,7 @@ def _append_stream_text(self, text: str) -> None:
self._schedule_stream_flush()
return
if hasattr(output, "write"):
prefix = "FirstCoder:\n" if self._stream_text_buffer == text else ""
prefix = "PytestPilot:\n" if self._stream_text_buffer == text else ""
output.write(f"{prefix}{text}")

def _close_stream_segment_for_tool(self) -> None:
Expand Down Expand Up @@ -876,7 +876,7 @@ def _flush_stream_text(self) -> bool:
if self._stream_rendered_text == self._stream_text_buffer:
return False
self._stream_rendered_text = self._stream_text_buffer
_observe_markdown_update(self._stream_text_widget.update(f"FirstCoder:\n\n{self._stream_rendered_text}"))
_observe_markdown_update(self._stream_text_widget.update(f"PytestPilot:\n\n{self._stream_rendered_text}"))
output = self.query_one("#output")
self._scroll_output_end_if_pinned(output)
return True
Expand Down
2 changes: 1 addition & 1 deletion firstcoder/app/tui_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class TuiEntryKind(StrEnum):
TuiEntryKind.SYSTEM: "system",
TuiEntryKind.COMMAND: "command",
TuiEntryKind.USER: "you",
TuiEntryKind.ASSISTANT: "FirstCoder",
TuiEntryKind.ASSISTANT: "PytestPilot",
TuiEntryKind.REASONING: "thinking",
TuiEntryKind.TOOL: "tool",
TuiEntryKind.PERMISSION: "permission",
Expand Down
4 changes: 2 additions & 2 deletions firstcoder/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,12 +433,12 @@ def run_repl(
else:
response = chat_runner.run_user_turn(line)

print(f"FirstCoder> {response.content}")
print(f"PytestPilot> {response.content}")
pending = getattr(chat_runner, "last_pending_input", None)
while pending is not None and auto_approve and _pending_kind(pending) == "permission_confirmation":
print("Auto-approve> allow_once")
response = chat_runner.resume_with_user_input(_pending_id(pending), "allow_once")
print(f"FirstCoder> {response.content}")
print(f"PytestPilot> {response.content}")
pending = getattr(chat_runner, "last_pending_input", None)

if pending is not None:
Expand Down
2 changes: 1 addition & 1 deletion firstcoder/context/provider_summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def summarize(self, messages: list[AgentMessage], *, summary_mode: str = "defaul
ChatMessage(
role="system",
content=(
"你是 FirstCoder 的上下文压缩器。请只输出一段中文摘要,"
"你是 PytestPilot 的上下文压缩器。请只输出一段中文摘要,"
"保留用户目标、已完成决策、关键文件、未解决问题和下一步。"
),
),
Expand Down
2 changes: 1 addition & 1 deletion firstcoder/context/system_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def _format_section(title: str, content: str) -> str:

def _agent_behavior_rules() -> str:
return """# Role and operating context
You are FirstCoder, an interactive local coding agent. Use the available tools to help the user with software engineering tasks in the current workspace. User and project instructions override these default rules.
You are PytestPilot, an interactive local coding agent for Python/pytest diagnosis and repair. Use the available tools to help the user with software engineering tasks in the current workspace. User and project instructions override these default rules.

# Working loop
- Classify the request first: answer simple questions directly; use tools for code edits, debugging, tests, repository search, and multi-file work.
Expand Down
4 changes: 3 additions & 1 deletion firstcoder/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ def run(self, command: list[str], cwd: Path, limits: ResourceLimits) -> CommandR
f"--ulimit=fsize={limits.max_file_size_mb * 1024}:{limits.max_file_size_mb * 1024}",
f"--tmpfs=/tmp:rw,noexec,nosuid,nodev,size={limits.tmpfs_mb}m",
"--mount",
f"type=bind,src={root},dst=/workspace,rw",
# Bind mounts are read-write by default; `rw` is not a valid
# standalone field in Docker's `--mount` key=value syntax.
f"type=bind,src={root},dst=/workspace",
"--workdir=/workspace",
]
for key, value in sorted(self.environment.items()):
Expand Down
24 changes: 12 additions & 12 deletions tests/test_app_tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def test_firstcoder_app_topbar_text_includes_session_id() -> None:
app = FirstCoderApp(current_session=FakeSession())

assert app._topbar_text() == (
"[#7bba55]FirstCoder[/] [#303238]·[/] [#7bba55]idle · ready[/] "
"[#7bba55]PytestPilot[/] [#303238]·[/] [#7bba55]idle · ready[/] "
"[#303238]·[/] [#6e6d72]sess_test[/] "
"[#303238]·[/] [#6e6d72]standard[/]"
)
Expand All @@ -167,7 +167,7 @@ def test_firstcoder_app_topbar_text_includes_provider_model_mode_and_cwd() -> No
)

assert app._topbar_text() == (
"[#7bba55]FirstCoder[/] [#303238]·[/] [#7bba55]idle · ready[/] "
"[#7bba55]PytestPilot[/] [#303238]·[/] [#7bba55]idle · ready[/] "
"[#303238]·[/] [#6e6d72]sess_test[/] "
"[#303238]·[/] [#6e6d72]yurenapi/gpt-5.5[/] "
"[#303238]·[/] [#6e6d72]standard[/] [#303238]·[/] [#6e6d72]cwd FirstCoder[/]"
Expand Down Expand Up @@ -214,7 +214,7 @@ def test_firstcoder_app_topbar_uses_spacious_two_sided_layout_when_width_is_know

text = app._topbar_text(width=120)

assert text.startswith("[#7bba55]FirstCoder[/]")
assert text.startswith("[#7bba55]PytestPilot[/]")
assert " [#303238]·[/] [#6e6d72]sess_test[/]" not in text
assert "[#7bba55]idle · ready[/]" in text
assert "[#6e6d72]sess_test[/]" in text
Expand All @@ -230,7 +230,7 @@ class BypassSession(FakeSession):
app = FirstCoderApp(current_session=BypassSession())

assert app._topbar_text() == (
"[#7bba55]FirstCoder[/] [#303238]·[/] [#7bba55]idle · ready[/] "
"[#7bba55]PytestPilot[/] [#303238]·[/] [#7bba55]idle · ready[/] "
"[#303238]·[/] [#6e6d72]sess_c8d401e2[/] "
"[#303238]·[/] [#b28443]bypass[/]"
)
Expand All @@ -257,7 +257,7 @@ def test_tui_transcript_records_structured_entries_with_stable_labels() -> None:
)

assert [entry.id for entry in transcript.entries] == [user.id, assistant.id, tool.id]
assert [entry.label for entry in transcript.entries] == ["you", "FirstCoder", "tool exec_command running"]
assert [entry.label for entry in transcript.entries] == ["you", "PytestPilot", "tool exec_command running"]
assert transcript.entries[-1].status == "running"


Expand Down Expand Up @@ -304,7 +304,7 @@ def test_firstcoder_app_records_rendered_messages_in_transcript(monkeypatch) ->

assert [(entry.kind, entry.label, entry.body) for entry in app.transcript.entries] == [
(TuiEntryKind.USER, "you", "> hello"),
(TuiEntryKind.ASSISTANT, "FirstCoder", "**hi**"),
(TuiEntryKind.ASSISTANT, "PytestPilot", "**hi**"),
]


Expand Down Expand Up @@ -680,7 +680,7 @@ def test_firstcoder_app_streams_text_delta_without_repeating_final_text(monkeypa

assert [type(widget).__name__ for widget in output.mounted] == ["FirstCoderMarkdown"]
assert output.mounted[0].allow_select is False
assert output.mounted[0].updates[-1] == "FirstCoder:\n\nhello"
assert output.mounted[0].updates[-1] == "PytestPilot:\n\nhello"
assert app._stream_text_buffer == "hello"
assert runner.seen == [
ChatStreamEvent(kind="text_delta", text="he"),
Expand All @@ -703,7 +703,7 @@ def test_firstcoder_app_streaming_skips_normalized_duplicate_assistant_line(monk

assert [type(widget).__name__ for widget in output.mounted] == ["FirstCoderMarkdown"]
assert output.mounted[0].allow_select is False
assert output.mounted[0].updates[-1] == "FirstCoder:\n\nhello"
assert output.mounted[0].updates[-1] == "PytestPilot:\n\nhello"


def test_firstcoder_app_paces_stream_markdown_updates(monkeypatch) -> None:
Expand All @@ -720,12 +720,12 @@ def test_firstcoder_app_paces_stream_markdown_updates(monkeypatch) -> None:
markdown = output.mounted[0]
assert type(markdown).__name__ == "FirstCoderMarkdown"
assert markdown.allow_select is False
assert markdown.updates == ["FirstCoder:\n\n我"]
assert markdown.updates == ["PytestPilot:\n\n我"]
assert app._stream_text_buffer == "我在这里"

app._flush_stream_text()

assert markdown.updates[-1] == "FirstCoder:\n\n我在这里"
assert markdown.updates[-1] == "PytestPilot:\n\n我在这里"


def test_firstcoder_app_does_not_scroll_stream_when_render_is_deferred(monkeypatch) -> None:
Expand Down Expand Up @@ -1333,8 +1333,8 @@ def test_firstcoder_app_starts_new_stream_block_after_tool_event(monkeypatch) ->
first_markdown, _, second_markdown = output.mounted
assert first_markdown.allow_select is False
assert second_markdown.allow_select is False
assert first_markdown.updates[-1] == "FirstCoder:\n\n我先看看。"
assert second_markdown.updates[-1] == "FirstCoder:\n\n看完了。"
assert first_markdown.updates[-1] == "PytestPilot:\n\n我先看看。"
assert second_markdown.updates[-1] == "PytestPilot:\n\n看完了。"


def test_permission_requested_tool_event_uses_permission_style() -> None:
Expand Down
6 changes: 3 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def test_run_repl_sends_multiple_user_messages(capsys):
run_repl(runner, ["hello", "continue"])

assert runner.turns == ["hello", "continue"]
assert capsys.readouterr().out == "FirstCoder> first reply\nFirstCoder> second reply\n"
assert capsys.readouterr().out == "PytestPilot> first reply\nPytestPilot> second reply\n"


def test_run_repl_routes_next_line_to_pending_permission(capsys):
Expand All @@ -240,13 +240,13 @@ def test_run_repl_routes_next_line_to_pending_permission(capsys):
assert runner.turns == ["write file"]
assert runner.resumes == [("perm_1", "allow_once")]
assert capsys.readouterr().out == (
"FirstCoder> need permission\n"
"PytestPilot> need permission\n"
"Permission> Allow?\n"
"Choose:\n"
" 1. Deny\n"
" 2. Allow once\n"
" 3. Allow always for same scope\n"
"FirstCoder> done\n"
"PytestPilot> done\n"
)


Expand Down
15 changes: 13 additions & 2 deletions tests/test_context_system_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

def _inputs(**overrides: object) -> SystemPromptInputs:
values = {
"base_rules": "你是 FirstCoder。",
"base_rules": "你是 PytestPilot。",
"agents_md": "项目规则:上下文放在 firstcoder/context。",
"tools": [
ToolDefinition(
Expand Down Expand Up @@ -44,7 +44,18 @@ def test_system_prompt_cache_reuses_prefix_when_fingerprint_matches() -> None:
assert first.fingerprint == second.fingerprint
assert first is second
assert first.messages[0].role == "system"
assert "你是 FirstCoder。" in first.messages[0].content
assert "你是 PytestPilot。" in first.messages[0].content


def test_default_agent_identity_is_pytestpilot() -> None:
from firstcoder.agent.session import DEFAULT_BASE_RULES

entry = SystemPromptBuilder().build(_inputs(base_rules=DEFAULT_BASE_RULES))
content = entry.messages[0].content

assert "PytestPilot" in content
assert "You are PytestPilot" in content
assert "You are FirstCoder" not in content


def test_agents_md_change_invalidates_system_prompt_fingerprint() -> None:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_execution_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def fake_run(command, *, cwd, env, limits, local_resource_limits):
assert "--user=65532:65532" in command
assert "--ulimit=fsize=8192:8192" in command
assert "--tmpfs=/tmp:rw,noexec,nosuid,nodev,size=16m" in command
assert f"type=bind,src={tmp_path.resolve()},dst=/workspace,rw" in command
assert f"type=bind,src={tmp_path.resolve()},dst=/workspace" in command
assert "LANG=C" in command
assert all("secret" not in part for part in command)
assert command[-5:] == ["example/pytest:locked", "python", "-m", "pytest", "-q"]
Expand Down
Loading