Skip to content
Merged
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
10 changes: 5 additions & 5 deletions .claude/rules/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@

Additional modules:
screenshot.py ─ Terminal text → PNG rendering (ANSI color, font fallback)
transcribe.py ─ Voice-to-text transcription via whisper.cpp / Apple / OpenAI
transcribe.py ─ Voice-to-text transcription via whisper.cpp / Apple Speech
i18n.py ─ Per-user UI strings (en / ru / zh)
naming.py ─ Haiku-generated session names + readable summaries
usage.py ─ Token usage aggregator + per-session token alerts
Expand Down Expand Up @@ -140,10 +140,10 @@ Handler modules (handlers/):
quota_alerts.py ─ Background /usage modal poll (default 10 min) →
5h/weekly band crossings 50/75/90 %
inbox.py ─ photo/document inbox under <workdir>/.ccbot-inbox/
interactive_ui.py ─ AskUserQuestion / ExitPlanMode / Permission UI +
adopt_interactive_msg / render_interactive_keyboard
(used by switcher tap to claim the carrier as the
interactive UI for a bg session whose prompt was stashed)
interactive_ui.py ─ AskUserQuestion / ExitPlanMode / Permission UI
(handle_interactive_ui + _build_interactive_keyboard).
A switcher tap surfaces a bg session's stashed prompt
via notifications.enter_kb_mode on the claimed carrier.
directory_browser.py─ Directory + session picker UI builders
switcher.py ─ Inline session-switcher keyboard
menu.py ─ Footer / More / Settings keyboard composition;
Expand Down
2 changes: 1 addition & 1 deletion .claude/rules/dm-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ When the user taps a session button in the main switcher:

1. `transfer_card_to_carrier` pauses the FROM session's card and claims the carrier message_id for the TO session.
2. `set_active_session(user, target)` flips the routing pointer.
3. If the TO session has a stashed `bg_status.pending_interactive_ui` *and* the live pane still shows the prompt, the carrier is repainted with the prompt + the regular CB_ASK_* keyboard (`adopt_interactive_msg`). Otherwise the carrier is painted with `send_history` — the full paginated transcript view, with the standard footer ridden along as `extra_rows` so management controls stay reachable.
3. If the TO session has a stashed `bg_status.pending_interactive_ui` *and* the live pane still shows the prompt, the carrier is claimed as the live card and flipped into kb-mode (`enter_kb_mode`) so the CB_ASK_* keyboard drives the prompt. Otherwise the carrier is painted as the session's live card (`paint_card_on_carrier` — header + paginated body + bg-panel + footer) and receives subsequent claude events in place.
4. `bg_status.mark_seen` + `prune_seen` drop the just-viewed badge from the panel.

Pagination (`CB_HISTORY_PREV/NEXT`) preserves the original `extra_rows` by stamping `context.user_data['_history_origin']` (`switcher` or `menu_list`) when the history view is first painted; the pagination handler rebuilds the matching footer from this hint. There is no explicit "History" button in the footer — pagination buttons themselves are the navigation affordance, and the user lands on the paginated view via switcher tap, Menu → List, or `/screenshot Back` (both `m` and `l` origins now paint history).
Expand Down
1 change: 0 additions & 1 deletion .claude/rules/secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ where **not** to put) credentials when working on or with ccbot.
| ccbot Telegram bot token | `~/.ccbot/.env` → `TELEGRAM_BOT_TOKEN=…` (or `./.env` in cwd) |
| ccbot allowlist (Telegram user ids) | `~/.ccbot/.env` → `ALLOWED_USERS=…` |
| ccbot outbound TG proxy (optional) | `~/.ccbot/.env` → `TG_PROXY_URL=…` |
| OpenAI fallback voice key (optional) | `~/.ccbot/.env` → `OPENAI_API_KEY=…` |
| Claude Code login token | `claude auth status` — managed by the CLI, not a file in the repo |
| whisper.cpp model | `~/.ccbot/models/ggml-medium.bin` (path overridable via `WHISPER_MODEL_PATH`) |
| ccbot persisted state | `~/.ccbot/state.json` — non-secret, but contains user ids / paths |
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ ways that are intentional and not negotiable:
writes `session_map.json`; the monitor polls it. No reliance on
process-tree introspection or claude SDK.
- **Voice transcription is local-first.** `whisper.cpp` (default) or
Apple Speech via PyObjC on macOS. The OpenAI fallback exists but is
off by default — no API key required to run.
Apple Speech via PyObjC on macOS — no API key required to run.

The full design rationale lives in `doc/dm-multisession-spec.md`. The
implementation map is in `doc/dm-multisession-plan.md`.
Expand Down
2 changes: 1 addition & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ ccbot 让你可以:
- **基于 hook 的会话跟踪。** Claude Code 的 `SessionStart` hook 写入
`session_map.json`;监控器轮询它。不依赖进程树检查或 claude SDK。
- **语音 — 本地优先。** `whisper.cpp`(默认)或 macOS 上通过 PyObjC
的 Apple Speech。OpenAI fallback 存在但默认关闭 — 运行不需要 API key。
的 Apple Speech — 运行不需要 API key。

完整的设计动机在 `doc/dm-multisession-spec.md`。实现地图在
`doc/dm-multisession-plan.md`。
Expand Down
3 changes: 1 addition & 2 deletions README_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ Claude Code живёт в терминале. Отошёл от стола —
пишет `session_map.json`; монитор бота его опрашивает. Никаких
process-tree introspection или claude SDK.
- **Голос — local-first.** `whisper.cpp` (по умолчанию) или Apple
Speech через PyObjC на macOS. OpenAI-fallback есть, но выключен —
API-ключ для запуска не нужен.
Speech через PyObjC на macOS — API-ключ для запуска не нужен.

Полная архитектурная мотивация — в `doc/dm-multisession-spec.md`.
Карта реализации — в `doc/dm-multisession-plan.md`.
Expand Down
2 changes: 0 additions & 2 deletions doc/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ curl -s --max-time 8 -x "$TG_PROXY_URL" \
- `VOICE_BACKEND=whisper` → requires `WHISPER_BIN` (default
`whisper-cli`) and `WHISPER_MODEL_PATH` (default
`$CCBOT_DIR/models/ggml-medium.bin`, ~1.5GB).
- `VOICE_BACKEND=openai` → falls back to the legacy gpt-4o-transcribe
HTTP path; needs `OPENAI_API_KEY`.
- `VOICE_BACKEND=off` → reject voice messages.

## State and disk usage
Expand Down
35 changes: 6 additions & 29 deletions src/ccbot/bot/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
from ..metrics import metrics_flush_loop
from ..session import session_manager
from ..session_monitor import NewMessage, SessionMonitor
from ..transcribe import close_client as close_transcribe_client
from ._common import CC_COMMANDS
from .callbacks import callback_handler
from .commands.info import (
Expand Down Expand Up @@ -90,7 +89,6 @@
_status_poll_task: asyncio.Task[None] | None = None
_card_timer_task: asyncio.Task[None] | None = None
_quota_alerts_task: asyncio.Task[None] | None = None
_context_poll_task: asyncio.Task[None] | None = None
_metrics_flush_task: asyncio.Task[None] | None = None


Expand All @@ -101,7 +99,6 @@ async def post_init(application: "Application[Any, Any, Any, Any, Any, Any]") ->
_status_poll_task, \
_card_timer_task, \
_quota_alerts_task, \
_context_poll_task, \
_metrics_flush_task, \
_conflict_app

Expand Down Expand Up @@ -173,15 +170,11 @@ async def message_callback(msg: NewMessage) -> None:
_quota_alerts_task = asyncio.create_task(quota_alerts_loop(application.bot))
logger.info("Quota alerts task started")

# Context-poll loop disabled — sending /context to live panes
# writes the modal's markdown output INTO the session's JSONL as
# a user-turn, which then renders on the live card as a fake
# ``[Request interrupted by user] ## Context Usage…`` block AND
# eats real tokens from claude's own context window every cycle.
# Context % is now computed from JSONL math (input + cache reads
# vs. 1M default for Claude 4.x models). See ``usage.context_pct_for_session``.
# _context_poll_task = asyncio.create_task(context_poll_loop(application.bot))
# logger.info("Context poll task started")
# Per-session context % is computed from JSONL math
# (usage.context_pct_for_session) — NOT by polling /context into panes.
# Polling wrote the modal's markdown into each session's JSONL as a fake
# user-turn (polluting the live card + burning tokens), so that path was
# removed. See doc/dm-multisession-spec.md §4.6.

_metrics_flush_task = asyncio.create_task(metrics_flush_loop())
logger.info("Metrics flush task started")
Expand Down Expand Up @@ -249,12 +242,7 @@ async def post_shutdown(
application: "Application[Any, Any, Any, Any, Any, Any]",
) -> None:
"""Stop background tasks, flush queues, close HTTP clients."""
global \
_status_poll_task, \
_card_timer_task, \
_quota_alerts_task, \
_context_poll_task, \
_metrics_flush_task
global _status_poll_task, _card_timer_task, _quota_alerts_task, _metrics_flush_task

if _status_poll_task:
_status_poll_task.cancel()
Expand Down Expand Up @@ -283,15 +271,6 @@ async def post_shutdown(
_quota_alerts_task = None
logger.info("Quota alerts stopped")

if _context_poll_task:
_context_poll_task.cancel()
try:
await _context_poll_task
except asyncio.CancelledError:
pass
_context_poll_task = None
logger.info("Context poll stopped")

if _metrics_flush_task:
_metrics_flush_task.cancel()
try:
Expand All @@ -315,8 +294,6 @@ async def post_shutdown(
await session_monitor.stop()
logger.info("Session monitor stopped")

await close_transcribe_client()


def create_bot() -> "Application[Any, Any, Any, Any, Any, Any]":
"""Build the Application, wire all handlers, return it ready to run_polling."""
Expand Down
8 changes: 0 additions & 8 deletions src/ccbot/bot/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,14 +607,6 @@ async def voice_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
if config.voice_backend == "off":
await safe_reply(update.message, "⚠ Voice is disabled (VOICE_BACKEND=off).")
return
if config.voice_backend == "openai" and not config.openai_api_key:
await safe_reply(
update.message,
"⚠ VOICE_BACKEND=openai but OPENAI_API_KEY is unset.\n"
"Set the key or switch VOICE_BACKEND to whisper/auto.",
)
return

wid = active_window(user.id)
if wid is None:
await safe_reply(
Expand Down
6 changes: 0 additions & 6 deletions src/ccbot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,6 @@ def __init__(self) -> None:
os.getenv("CCBOT_SHOW_HIDDEN_DIRS", "").lower() == "true"
)

# OpenAI API for voice message transcription (optional)
self.openai_api_key: str = os.getenv("OPENAI_API_KEY", "")
self.openai_base_url: str = os.getenv(
"OPENAI_BASE_URL", "https://api.openai.com/v1"
)

# --- DM multi-session mode ---
# Sessions
self.max_sessions: int = int(os.getenv("MAX_SESSIONS", "10"))
Expand Down
1 change: 0 additions & 1 deletion src/ccbot/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@
- directory_browser: Directory selection UI
- interactive_ui: Interactive UI (AskUserQuestion, Permission Prompt, etc.)
- status_polling: Terminal status line polling
- response_builder: Build paginated response messages
"""
5 changes: 0 additions & 5 deletions src/ccbot/handlers/bg_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,6 @@ def _touch(entry: BgStatus) -> None:
entry.last_change = time.time()


def has_panel_content(user_id: int) -> bool:
"""True if the user has any bg-status entry worth rendering."""
return bool(_bg.get(user_id))


def update_status(
user_id: int,
session_id: str,
Expand Down
4 changes: 0 additions & 4 deletions src/ccbot/handlers/card_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,6 @@ class Event:
is_error: bool = False
image_data: list[tuple[str, bytes]] | None = None # tool_result images

@property
def is_tool(self) -> bool:
return self.type in ("tool_use", "tool_result")


@dataclass
class CardState:
Expand Down
Loading
Loading