From a4451a949c6091b6c8f9dab143ecae422ed538f5 Mon Sep 17 00:00:00 2001 From: zhou zhichao <69954637+zhou-zhichao@users.noreply.github.com> Date: Wed, 22 Apr 2026 03:31:07 +0200 Subject: [PATCH 1/5] Fix interactive UI creating duplicate messages on button press (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When editing an existing interactive message fails with "Message is not modified" (content unchanged after button press), the old code caught all exceptions and fell through to send a new message, causing duplicates. Changes: - Handle BadRequest "Message is not modified" specifically: keep existing message and return early instead of creating a duplicate - On other edit failures, send the replacement message first, then delete the old one only after the new one succeeds — prevents stranding users without controls if the replacement send also fails Fixes six-ddc/ccbot#66 --- src/ccbot/handlers/interactive_ui.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) 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 From 4d43da2fa5fb5dec71124b366c6dd0b20809ffec Mon Sep 17 00:00:00 2001 From: Joshua Frank Date: Tue, 21 Apr 2026 20:31:04 -0500 Subject: [PATCH 2/5] fix: stop renaming user-created Telegram topics on bind (#73) Co-authored-by: Joshua Frank --- src/ccbot/bot.py | 22 ---------------------- 1 file changed, 22 deletions(-) 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}`", From 08d9c934614dc67f7b1631985b822fc384b37c3e Mon Sep 17 00:00:00 2001 From: six-ddc Date: Wed, 22 Apr 2026 09:22:14 +0800 Subject: [PATCH 3/5] fix: show correct line count for Write tool results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Write's tool_result text is just a confirmation message (e.g. "File created successfully at: ..."), not the written content — so line count was always 1. Now computed from the original tool_use input.content, with correct handling of trailing newlines. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/ccbot/transcript_parser.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) 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 ""), From 5bc3a2c441b8146e18faee0094c0f75f5215bc7b Mon Sep 17 00:00:00 2001 From: Tej Date: Thu, 14 May 2026 11:43:25 +0900 Subject: [PATCH 4/5] =?UTF-8?q?test(transcript=5Fparser):=20Write=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=EB=A5=BC=20=EC=83=88=20=EC=8B=9C?= =?UTF-8?q?=EA=B7=B8=EB=8B=88=EC=B2=98=EC=97=90=20=EB=A7=9E=EC=B6=B0=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit upstream f5ddd7f가 _format_tool_result_text에 tool_input_data 인자를 추가하고 Write의 line count를 input.content에서 계산하도록 변경. fork에만 있는 테스트 스위트(0e3c31c)가 옛 동작 기준이라 깨졌음. - parametrize에 tool_input_data 컬럼 추가 - Write 케이스: text는 실제 result 문자열로, tool_input_data={"content": "..."}로 line count 검증 - 나머지 도구는 tool_input_data=None Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/ccbot/test_transcript_parser.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) 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}" From 07538c5e31dbea7cb46354e49b3d76f609a8091e Mon Sep 17 00:00:00 2001 From: Tej Date: Thu, 14 May 2026 11:44:18 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore(release):=20v1.0.1=20=E2=80=94=20upst?= =?UTF-8?q?ream=20pending=20merge=203=EA=B1=B4=20reconcile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CHANGELOG.md에 [1.0.1] 섹션 추가, pyproject.toml 1.0.0 → 1.0.1, README의 현재 버전 표기 갱신. 이번 릴리스의 실제 코드 변경은 직전 4 commit: - a4451a9 Fix interactive UI duplicate messages (upstream #67) - 4d43da2 fix: stop renaming user-created Telegram topics (upstream #73) - 08d9c93 fix: show correct Write line count (upstream f5ddd7f) - 5bc3a2c test: TestFormatToolResultText 시그니처 갱신 (f5ddd7f 영향) CHANGELOG의 v1.0.0 섹션에 있던 "Pending upstream merges" 표에는 ✅ v1.0.1 reconcile 완료 메모 추가. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++-- README.md | 2 +- pyproject.toml | 2 +- 3 files changed, 31 insertions(+), 4 deletions(-) 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"