diff --git a/CHANGELOG.md b/CHANGELOG.md index 15caae9a..c0b92482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,30 @@ --- +## [1.0.1] - 2026-05-14 + +upstream `six-ddc/ccbot` pending merge 3건을 cherry-pick. 버그픽스 only. + +### Fixed + +- **Interactive UI 버튼 누를 때 중복 메시지 생성 수정** (upstream [`865ab89`](https://github.com/six-ddc/ccbot/commit/865ab89), #67) + - "Message is not modified" BadRequest를 별도 처리: 기존 메시지 유지하고 early return + - 다른 edit 실패 시에는 교체 메시지를 먼저 보내고 원본 삭제 +- **bind 시 사용자가 만든 Telegram 토픽 이름 rename 안 함** (upstream [`350c653`](https://github.com/six-ddc/ccbot/commit/350c653), #73) + - 사용자가 직접 만든 토픽 이름을 ccbot이 자동 변경하지 않음 +- **Write tool result의 line count 정확히 표시** (upstream [`f5ddd7f`](https://github.com/six-ddc/ccbot/commit/f5ddd7f)) + - 기존: Write의 tool_result는 `File created successfully at: ...` 같은 확인 메시지라 line count가 항상 1이었음 + - 변경: 원본 `tool_use.input.content`에서 line count 계산 (trailing newline 보정 포함) + - `_format_tool_result_text`에 `tool_input_data` 인자 추가 (시그니처 변경, 기본값 `None`이라 fork 내부 호출과 호환) + +### Tests + +- `tests/ccbot/test_transcript_parser.py::TestFormatToolResultText` 갱신 + - parametrize에 `tool_input_data` 컬럼 추가, Write 케이스를 새 동작에 맞춰 수정 + - 전체 283/283 통과 + +--- + ## [1.0.0] - 2026-05-14 TejNote fork의 첫 공식 버전. 2026-04-27 이후 누적된 fork 전용 추가 사항을 한 번에 v1.0.0으로 정리합니다 (이전 내부 버전 `0.1.0`). @@ -72,7 +96,9 @@ TejNote fork의 첫 공식 버전. 2026-04-27 이후 누적된 fork 전용 추 ### Pending upstream merges -`six-ddc/ccbot:main`에는 있지만 아직 fork에 reconcile 안 된 commit (후속 PR에서 cherry-pick 예정): +> ✅ 아래 3건은 모두 [1.0.1]에서 reconcile 완료. + +`six-ddc/ccbot:main`에는 있지만 v1.0.0 시점에는 아직 fork에 reconcile 안 된 commit이었음: | Upstream commit | 설명 | | ------------------------------------------------------------------ | -------------------------------------------------------------------- | @@ -80,5 +106,6 @@ TejNote fork의 첫 공식 버전. 2026-04-27 이후 누적된 fork 전용 추 | [`350c653`](https://github.com/six-ddc/ccbot/commit/350c653) (#73) | bind 시 사용자가 만든 Telegram 토픽 이름을 rename하지 않도록 수정 | | [`f5ddd7f`](https://github.com/six-ddc/ccbot/commit/f5ddd7f) | Write tool 결과의 line count 정확히 표시 | -[Unreleased]: https://github.com/TejNote/ccbot/compare/v1.0.0...HEAD +[Unreleased]: https://github.com/TejNote/ccbot/compare/v1.0.1...HEAD +[1.0.1]: https://github.com/TejNote/ccbot/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/TejNote/ccbot/releases/tag/v1.0.0 diff --git a/README.md b/README.md index f23cc2f2..c216fed5 100644 --- a/README.md +++ b/README.md @@ -345,7 +345,7 @@ src/ccbot/ 상세 변경 이력은 [`CHANGELOG.md`](./CHANGELOG.md) 참고. 버전 정책은 [SemVer](https://semver.org/lang/ko/)를 따르고, 포맷은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 기준입니다. -현재 버전: **v1.0.0** (TejNote fork 첫 공식 릴리스, 2026-05-14). +현재 버전: **v1.0.1** (upstream pending merge 3건 reconcile, 2026-05-14). ## Contributing back upstream diff --git a/pyproject.toml b/pyproject.toml index 251a1866..653e484a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ccbot" -version = "1.0.0" +version = "1.0.1" description = "Telegram Bot for monitoring Claude Code sessions" readme = "README.md" requires-python = ">=3.12" diff --git a/src/ccbot/bot.py b/src/ccbot/bot.py index 213bea46..d81a5097 100644 --- a/src/ccbot/bot.py +++ b/src/ccbot/bot.py @@ -1240,17 +1240,6 @@ async def _create_and_bind_window( user.id, pending_thread_id, created_wid, window_name=created_wname ) - # Rename the topic to match the window name - resolved_chat = session_manager.resolve_chat_id(user.id, pending_thread_id) - try: - await context.bot.edit_forum_topic( - chat_id=resolved_chat, - message_thread_id=pending_thread_id, - name=created_wname, - ) - except Exception as e: - logger.debug(f"Failed to rename topic: {e}") - status = "Resumed" if resume_session_id else "Created" await safe_edit( query, @@ -1638,17 +1627,6 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) - user.id, thread_id, selected_wid, window_name=display ) - # Rename the topic to match the window name - resolved_chat = session_manager.resolve_chat_id(user.id, thread_id) - try: - await context.bot.edit_forum_topic( - chat_id=resolved_chat, - message_thread_id=thread_id, - name=display, - ) - except Exception as e: - logger.debug(f"Failed to rename topic: {e}") - await safe_edit( query, f"✅ Bound to window `{display}`", diff --git a/src/ccbot/handlers/interactive_ui.py b/src/ccbot/handlers/interactive_ui.py index 174e3a9e..7d975fe7 100644 --- a/src/ccbot/handlers/interactive_ui.py +++ b/src/ccbot/handlers/interactive_ui.py @@ -17,6 +17,7 @@ import logging from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.error import BadRequest from ..session import session_manager from ..terminal_parser import extract_interactive_content, is_interactive_ui @@ -202,13 +203,24 @@ async def handle_interactive_ui( ) _interactive_mode[ikey] = window_id return True - except Exception: - # Edit failed (message deleted, etc.) - clear stale msg_id and send new + except BadRequest as e: + if "Message is not modified" in str(e): + # Content unchanged — keep existing message as-is + _interactive_mode[ikey] = window_id + return True + # Other edit failure — fall through to send new message, + # but keep old message until replacement succeeds + logger.debug( + "Edit failed for interactive msg %s: %s, sending new", + existing_msg_id, + e, + ) + except Exception as e: logger.debug( - "Edit failed for interactive msg %s, sending new", existing_msg_id + "Edit failed for interactive msg %s: %s, sending new", + existing_msg_id, + e, ) - _interactive_msgs.pop(ikey, None) - # Fall through to send new message # Send new message (plain text — terminal content is not markdown) logger.info( @@ -228,6 +240,12 @@ async def handle_interactive_ui( if sent: _interactive_msgs[ikey] = sent.message_id _interactive_mode[ikey] = window_id + # New message sent successfully — now safe to delete the old one + if existing_msg_id: + try: + await bot.delete_message(chat_id=chat_id, message_id=existing_msg_id) + except Exception: + pass # Old message may already be gone return True return False diff --git a/src/ccbot/transcript_parser.py b/src/ccbot/transcript_parser.py index fa0bbf69..73dbed1e 100644 --- a/src/ccbot/transcript_parser.py +++ b/src/ccbot/transcript_parser.py @@ -344,7 +344,12 @@ def _format_expandable_quote(cls, text: str) -> str: return f"{cls.EXPANDABLE_QUOTE_START}{text}{cls.EXPANDABLE_QUOTE_END}" @classmethod - def _format_tool_result_text(cls, text: str, tool_name: str | None = None) -> str: + def _format_tool_result_text( + cls, + text: str, + tool_name: str | None = None, + tool_input_data: dict | None = None, + ) -> str: """Format tool result text with statistics summary. Shows relevant statistics for each tool type, with expandable quote for full content. @@ -363,9 +368,16 @@ def _format_tool_result_text(cls, text: str, tool_name: str | None = None) -> st return f" ⎿ Read {line_count} lines" elif tool_name == "Write": - # Write: show lines written - stats = f" ⎿ Wrote {line_count} lines" - return stats + # Write: line count comes from the input content, not the result + # (result is usually just "File created successfully at: ...") + written = tool_input_data.get("content", "") if tool_input_data else "" + if not written: + written_lines = 0 + else: + written_lines = written.count("\n") + ( + 0 if written.endswith("\n") else 1 + ) + return f" ⎿ Wrote {written_lines} lines" elif tool_name == "Bash": # Bash: show output line count @@ -528,7 +540,9 @@ def parse_entries( # Store tool info for later tool_result formatting # Edit tool needs input_data to generate diff in tool_result stage input_data = ( - inp if name in ("Edit", "NotebookEdit") else None + inp + if name in ("Edit", "NotebookEdit", "Write") + else None ) pending_tools[tool_id] = PendingToolInfo( summary=summary, @@ -691,7 +705,7 @@ def parse_entries( and cls.EXPANDABLE_QUOTE_START not in tool_summary ): entry_text += "\n" + cls._format_tool_result_text( - result_text, tool_name + result_text, tool_name, tool_input_data ) result.append( ParsedEntry( @@ -708,7 +722,7 @@ def parse_entries( ParsedEntry( role="assistant", text=cls._format_tool_result_text( - result_text, tool_name + result_text, tool_name, tool_input_data ) if result_text else (tool_summary or ""), diff --git a/tests/ccbot/test_transcript_parser.py b/tests/ccbot/test_transcript_parser.py index 16d4c730..d8e9dda9 100644 --- a/tests/ccbot/test_transcript_parser.py +++ b/tests/ccbot/test_transcript_parser.py @@ -256,21 +256,24 @@ def test_format_edit_diff(self, old: str, new: str, check): class TestFormatToolResultText: @pytest.mark.parametrize( - "text, tool_name, check", + "text, tool_name, tool_input_data, check", [ ( "line1\nline2\nline3", "Read", + None, lambda r: r == " ⎿ Read 3 lines", ), ( - "line1\nline2", + "File created successfully at: out.txt", "Write", + {"content": "line1\nline2"}, lambda r: r == " ⎿ Wrote 2 lines", ), ( "output line", "Bash", + None, lambda r: ( r.startswith(" ⎿ Output 1 lines") and EXPQUOTE_START in r @@ -280,21 +283,25 @@ class TestFormatToolResultText: ( "file1.py\nfile2.py\n", "Grep", + None, lambda r: "Found 2 matches" in r and EXPQUOTE_START in r, ), ( "a.py\nb.py\nc.py", "Glob", + None, lambda r: "Found 3 files" in r and EXPQUOTE_START in r, ), ( "agent says hello", "Task", + None, lambda r: "Agent output 1 lines" in r and EXPQUOTE_START in r, ), ( "page content here", "WebFetch", + None, lambda r: ( f"Fetched {len('page content here')} characters" in r and EXPQUOTE_START in r @@ -303,13 +310,22 @@ class TestFormatToolResultText: ( "", "Read", + None, lambda r: r == "", ), ], ids=["Read", "Write", "Bash", "Grep", "Glob", "Task", "WebFetch", "empty"], ) - def test_format_tool_result_text(self, text: str, tool_name: str, check): - result = TranscriptParser._format_tool_result_text(text, tool_name) + def test_format_tool_result_text( + self, + text: str, + tool_name: str, + tool_input_data: dict | None, + check, + ): + result = TranscriptParser._format_tool_result_text( + text, tool_name, tool_input_data + ) assert check(result), f"Failed check for {tool_name!r}: {result!r}"