Merge github/main into feature/add_web_admin_v2.
- Go backend files auto-merged cleanly
- All web/ conflicts resolved by keeping feature branch version
(feature branch has the latest, most complete web admin)
- Remove Logs.tsx brought in from main (no backend endpoint)
- Fix go.sum checksum for BurntSushi/toml
Made-with: Cursor
- Add /web install|upgrade|status|uninstall slash command (privileged)
- Auto-install cc-connect-web via npm into ~/.cc-connect/web/
- Auto-configure [bridge] and [management] in config.toml with random tokens
- Management server serves SPA static files from dist/ with fallback to index.html
- Add config.EnableWebAdmin() for atomic bridge+management setup
- Add core/web_manager.go with npm registry integration
- Prepare web/package.json for npm publish as cc-connect-web (files: dist/)
- Add i18n translations for all /web messages (en, zh, zh-TW, ja, es)
Made-with: Cursor
When a session mismatch is detected (e.g., after /new or /switch), the
old agent session was closed asynchronously via closeAgentSessionAsync().
The new session started immediately without waiting for the old one to
fully terminate, causing both agents to potentially output simultaneously
and produce duplicate responses.
Fix: Change async close to synchronous close in session recycling path.
This ensures the old agent process is fully terminated before starting
a new one. The closeAgentSessionWithTimeout() has a 10s timeout to
prevent indefinite blocking.
Root cause analysis:
- getOrCreateInteractiveStateWith() detected session mismatch
- markStopped() was called, but event loop might be mid-processing
- closeAgentSessionAsync() ran in goroutine, new session started
- Old agent continued outputting until Close() completed
- User received two similar responses from two different agents
Fixes#327
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(codex): force kill session process group on stop
* fix(codex): kill all active session processes on stop
* fix(core): stop stale event loops on stop
* feat(telegram): migrate to go-telegram/bot and add forum topic support
Migrate the Telegram platform from the unmaintained go-telegram-bot-api/v5
(last release 2021) to go-telegram/bot v1.20.0, which is actively maintained
and supports the full Telegram Bot API including forum topics.
Key changes:
- **Library migration**: Replace go-telegram-bot-api/v5 with go-telegram/bot.
Redesign the telegramBot interface around the new library's typed API methods
(SendMessage, SendPhoto, EditMessageText, etc.) instead of the old generic
Send/Request pattern. The *bot.Bot type satisfies the interface directly.
- **Forum topic support**: Add threadID to replyContext and session key.
Messages in forum groups (IsForum=true) now include the MessageThreadID
in the session key (telegram:{chatID}:{threadID}:{userID}), giving each
topic its own independent agent session. All send methods propagate
MessageThreadID so replies land in the correct topic. Non-forum groups
and private chats are unaffected.
- **Per-topic workspace binding**: Add ChannelKey field to core.Message so
platforms can provide an explicit channel identifier for workspace binding.
Telegram sets this to "chatID:threadID" for forum topics, enabling
multi-workspace mode to bind different topics to different project
directories. Add effectiveChannelID/effectiveWorkspaceChannelKey helpers
and update all workspace resolution call sites in the engine.
- **Skill command registration fix**: GetAllCommands now respects
disabled_commands for skills, matching the existing behavior for
built-in commands. This prevents disabled skills from being registered
as Telegram bot menu commands.
- **Connection management**: Simplify the reconnect architecture.
bot.New() handles initial validation, bot.Start(ctx) manages long
polling internally. Remove the stale bot detection (generation
comparison in handlers) since the new library's blocking Start()
ensures handlers are never called from a superseded bot instance.
Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
* fix: resolve lint errors exposed by engine.go and telegram_test.go changes
- Remove unused assignment to `s` in cmdNew (SA4006)
- Remove unused stubBotFactory and failingBotFactory helpers in telegram tests
Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
---------
Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
- Remove bridge navigation and page (unused)
- Dashboard projects: grid layout (1/2/3 cols) instead of single column list
- System config: display raw TOML from config file instead of JSON summary
- Remove logs page (no backend endpoint)
- Cron page: grid layout, click-to-edit, friendly schedule dropdown,
session key dropdown, mutually exclusive prompt/exec task type toggle
- Move language/theme/logout controls from sidebar to right header
- Unify accent color: use CSS variables with RGB format for proper
Tailwind opacity support; light mode uses darker green (#16a34a),
dark mode keeps neon green (#42ff9c)
- Fix Badge info variant from blue to accent green
- Align sidebar/header heights (both h-14)
- Add footer with copyright, version number, and GitHub link
- Fix project settings PATCH: persist quiet/language/admin_from/
disabled_commands to config.toml via new SaveProjectSettings()
- Return admin_from and disabled_commands in project GET response
- Add GetDisabledCommands() to Engine for settings readback
- Backend: add PATCH support to cron job endpoint for editing
- Backend: /config endpoint returns raw TOML file content
Made-with: Cursor
* feat(wecom): handle inbound file messages over HTTP callback
Parse MsgType=file (FileName + MediaId), download via media/get API,
and forward as core.FileAttachment for agents. Add MIME inference helper
and unit tests. Document inbound capabilities in config.example.toml.
WebSocket AI bot mode unchanged; file receive there depends on WeChat policy.
* chore(wecom): add Info/Warn logs for callback diagnostics
Log every POST (size, query flags), parsed msg_type/msg_id/from_user,
duplicate/old/allow_from drops, unsupported types at Warn, and Debug
preview of decrypted XML (truncated). Helps trace missing file pushes.
* feat(wecom-ws): receive image, file, mixed and quote media over AI bot WebSocket
Parse msgtype image/file/mixed and quote (text, voice, image, file, mixed)
per WeCom smart-bot protocol. Download encrypted media from signed URLs
and decrypt with AES-256-CBC (IV = first 16 bytes of base64 key), matching
@wecom/aibot-node-sdk. Voice accepts both content and legacy text fields.
Adds wsCollectInboundParts, deliverWSMediaInbound, unit tests for collect,
Content-Disposition parsing, and round-trip decryption.
* fix(wecom-ws): handle file in mixed payloads and normalize aeskey decoding
- Collect file items inside mixed.msg_item and merge top-level file/image
when msgtype is file/image but a non-empty mixed block is also present.
- decodeWeComAESKey: trim whitespace, URL-safe base64, padding, optional hex.
- Add regression tests for mixed+file and aeskey variants.
* fix: satisfy golangci-lint (gosimple, staticcheck)
- wecom: use := for walkMixed/walkQuote closures (S1021).
- engine: drop unused assignment from sessions.NewSession in cmdNew (SA4006).
---------
Co-authored-by: Claude <noreply@anthropic.com>
Add a new integration test suite that verifies real agent (Claude Code, Codex,
Cursor, Gemini, OpenCode) interactions with a mock platform. Tests are gated
by //go:build integration tag so they're excluded from normal CI.
core/engine.go:
- Add public ReceiveMessage(p, msg) method to allow test callers to
inject messages into the engine
tests/integration/agent_integration_test.go:
- mockPlatform implementing core.Platform interface for test verification
- agentPool for reusing agent instances across tests
- Session tests: new session, /list, /switch, /stop, /new context clear, /history
- Agent tests: claude code, codex, cursor, gemini (skip if quota), opencode (skip if no gitlab auth)
- Command tests: /shell (skip if admin_from not set), /provider list
- i18n: /lang zh language switch
- Concurrency: session isolation, long text chunking, empty message handling
docs/plans/2026-03-24-integration-tests.md:
- Test plan listing all implemented and planned integration test cases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: require explicit session key when sending attachments with multiple sessions
When cc-connect send --image or --file is called without an explicit
--session flag and there are multiple active sessions, the code would
arbitrarily pick the first session from a map (non-deterministic order).
This could cause attachments to be sent to the wrong chat.
Now returns an error when multiple sessions exist and no session key is
specified for attachment sends. Also adds debug logging to Telegram SendImage
to aid future diagnosis.
Fixes#242
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: clear AgentSessionID on old session in /new command
When /new is invoked, the old session's AgentSessionID was not being
cleared. This could cause the old session to be resumed on the next
message if GetOrCreateActive returned it instead of the newly created
session.
The fix clears the old session's AgentSessionID and clears its history
before creating a new session, similar to how cmdSwitch handles session
transitions.
Fixes#238
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Dev Claude <dev@cc-connect.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(codex): avoid closing events channel before readLoop exits
Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.
Fixes https://github.com/chenhg5/cc-connect/issues/281
* fix(engine): recycle live agent when Session clears AgentSessionID (/new)
getOrCreateInteractiveStateWith reused any alive process when wantID was empty
because the condition treated empty wantID as always compatible. After /new the
Session has no resume ID but the old Claude process still reported the previous
session — messages kept going to stale context.
Recycle when the live process has a concrete ID but the Session expects none or
a different one.
Fixes#238
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(codex): avoid closing events channel before readLoop exits
Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.
Fixes https://github.com/chenhg5/cc-connect/issues/281
* fix(session): never persist ContinueSession as agent_session_id (#255)
- Reject storing the __continue__ sentinel in SetAgentSessionID; normalize in SetAgentInfo.
- Let CompareAndSetAgentSessionID overwrite a mistaken persisted sentinel with a real ID.
- Strip sentinel when loading session JSON and when saving snapshots.
- Simplify engine post-start writeback to a single CompareAndSet path.
Fixes#255
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
When cc-connect send --image or --file is called without an explicit
--session flag and there are multiple active sessions, the code would
arbitrarily pick the first session from a map (non-deterministic order).
This could cause attachments to be sent to the wrong chat.
Now returns an error when multiple sessions exist and no session key is
specified for attachment sends. Also adds debug logging to Telegram SendImage
to aid future diagnosis.
Fixes#242
Co-authored-by: Dev Claude <dev@cc-connect.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Avoids race condition when cron and regular messages share the same
interactiveKey — the shared state.replyCtx was being overwritten by
concurrent goroutines, causing cron responses to be delivered to the
wrong session.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(agent): add ACP stdio JSON-RPC adapter (MVP)
Implement agent/acp as an ACP client over newline-delimited JSON-RPC: initialize,
session/new or session/load, session/prompt, session/update mapping to core.Event,
and session/request_permission wired to RespondPermission.
Includes plugin_agent_acp.go (!no_acp), Makefile ALL_AGENTS entry, config example,
unit tests for transport and mapping, and AgentDoctorInfo for doctor.
Made-with: Cursor
* fix(acp): Cursor CLI compatibility — authenticate + cursor/* RPC ack
- Optional auth_method (e.g. cursor_login) triggers JSON-RPC authenticate after initialize
- initialize includes minimal clientCapabilities (fs/terminal) per Cursor ACP docs
- Acknowledge cursor/* server requests with empty result to avoid blocking tool flows
- Add integration test against ~/.local/bin/agent when present
- Document auth_method and args in config.example.toml
* fix(telegram): use Request for sendChatAction typing indicator
Telegram returns result=true for sendChatAction; BotAPI.Send unmarshals
result as Message and fails. Request only checks ok and avoids that decode.
* fix(core): surface EventToolResult in IM; extend ACP tool/thinking mapping
- Engine: handle EventToolResult in interactive loop (was silently dropped);
compress and relay paths append tool output too. Add MsgToolResult i18n.
- ACP: stream tool_call_update for in_progress/pending; optional reasoning_*
sessionUpdate kinds -> EventThinking; fallback hook for unknown kinds.
* fix(core): run agent Send concurrently with interactive event loop
Agents that block inside Send (e.g. ACP session/prompt) can emit
EventPermissionRequest while blocked; the engine must consume Events()
in parallel or permission prompts never reach the platform.
- processInteractiveMessageWith: async Send + pass sendDone into processInteractiveEvents
- processInteractiveEvents: select on sendDone for errors and sync at turn boundaries
- drainPendingMessages: same async Send pattern as the main path
- Add TestProcessInteractiveEvents_PermissionWhileSendBlocked regression test
* docs(acp): document OpenClaw and Copilot CLI; add display_name option
- config.example.toml: Cursor, OpenClaw (`openclaw acp`), Copilot (`copilot --acp --stdio`) examples with upstream doc links
- acp agent: optional display_name for doctor (AgentDoctorInfo)
- agent_test.go: cover default and custom display_name
* fix(core): remove ineffectual pendingSend assignments (ineffassign)
pendingSend was set to nil immediately before return or before
reassignment to nextSend; golangci-lint flags these as ineffectual.
* test(core): fix race in PermissionWhileSendBlocked under -race
The test goroutine polled stubInlineButtonPlatform.buttonRows while
processInteractiveEvents wrote them from another goroutine. Use a
permSignalInlinePlatform wrapper that signals on perm:allow via a channel
instead of racy field reads.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(discord): add mode and reasoning button controls
Use Discord buttons for /mode and /reasoning, and apply Claude Code mode changes to the current session when supported.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(discord): clean up mode labels and log button fallback errors
Remove the extra mode marker symbol, show the current mode with text instead, and include the underlying error when permission button fallbacks trigger.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
- core: interactive /dir card (nav/act) on CardSender platforms; dirApply refactor
- core+i18n: card strings for dir history UI
- config: projects.show_context_indicator (hide [ctx: ~N%] suffix)
- feishu/lark: reply_to_trigger=false posts via Create instead of Reply
- codex: skip flush when ctx done; sync.Once on session Close
- dingtalk: stream runner reconnect loop; cancel on Stop
- go.mod: mark rsc.io/qr as direct require
* feat: add session resilience with --continue, resume recovery, and context indicator
First connection after engine startup uses --continue to pick up the most
recent CLI session, bridging direct CLI usage and cc-connect. If resume
fails (e.g. prompt too long), automatically falls back to a fresh session.
Appends [ctx: ~XX%] to agent responses using SDK token counts, with
fallback to agent self-reported context percentage when SDK values are
unreliable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: --continue logic bug — respect fresh session intent after first connection
Remove the startSessionID == "" condition that forced --continue even
when users explicitly requested fresh sessions (e.g. via /new). Now only
the first connection after engine startup uses --continue; subsequent
connections respect the stored session ID or "" for fresh.
Add TestFreshSessionRespectedAfterFirstConnection to verify.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(npm): correct pre-release version comparison
The isNewerOrEqual function was comparing pre-release strings
lexicographically (e.g., "beta.2" > "beta.10" due to string comparison),
causing incorrect downgrade detection. Now parses pre-release into
id and numeric parts for proper numeric comparison.
Fixes: beta version upgrade being treated as outdated and downgrading to stable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore(npm): bump version to 1.2.2-beta.4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make hasConnectedOnce per-workspace so idle-reaped workspaces resume with --continue
hasConnectedOnce was engine-global, so after the first connection set it
to true, any workspace that got idle-reaped and later recreated would
skip --continue and spawn a fresh session — losing all prior context.
Move the flag into workspaceState so each workspace independently tracks
its first connection. When a workspace is reaped and GetOrCreate makes a
new one, the flag starts false again, enabling --continue on reconnection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: fork session on resume/continue to prevent context interleaving
When cc-connect picks up a session via --continue or --resume, add
--fork-session so the platform conversation gets its own independent
context branch. This prevents history interleaving when the user is
also working in the same session from the CLI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restore persisted /dir overrides on startup, apply them to session and heartbeat wiring, and add /dir reset plus docs so work directory changes survive restart without editing config.toml.
Made-with: Cursor
Co-authored-by: KRATSZ <KRATSZ@users.noreply.github.com>
CompareAndSetAgentSessionID only fills an empty ID; after restart the
stored sentinel __continue__ would block writing the actual session id
from the running agent. Handle that case explicitly.
- Add DirHistory module to track recently used directories per project
- /dir now shows history list with indices for quick navigation
- Support /dir <number> to jump to history entry
- Support /dir - to return to previous directory
- History stored in data_dir/dir_history.json
- Initial work_dir from config is automatically added to history
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Only flip hasConnectedOnce when starting an agent for non-synthetic
messages (exclude UserID cron and heartbeat) so scheduled jobs cannot
consume the one-time ContinueSession bridge.
- Add regression tests and align Codex exec resume arg comments with
the actual --json placement.
- Estimate tokens including the current turn assistant text before AddHistory
so auto-compress triggers match intent and tests stay green.
- Add SessionManager.NewSideSession for jobs that need an isolated Session
without changing the user active session; use it for cron new_per_run.
Fixes issue #233 and #220.
## Changes
### 1. Fix /mode command not taking effect (Issue #233)
When switching mode via `/mode yolo`, the new mode was saved to the agent
but the existing session continued using the old mode. This caused YOLO
mode to not work until a new session was manually started.
Fix: Reset the session when mode changes, similar to how /model works.
File: core/engine.go
### 2. Fix --json flag position in codex exec resume (Issue #220)
The --json flag was placed before the thread_id positional argument in
`codex exec resume` command, causing "unexpected argument '--json'" error
on the second message in a session.
Fix: Move --json to the correct position after thread_id and before prompt.
Before: codex exec resume --json --skip-git-repo-check <tid> <prompt>
After: codex exec resume --skip-git-repo-check <tid> --json --cd <dir> <prompt>
Files: agent/codex/session.go, agent/codex/session_test.go
Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When cc-connect starts up and picks up the most recent CLI session via
--continue, it now adds --fork-session to create an independent context
branch. This prevents history interleaving when the user is concurrently
working in the same workspace from both the CLI and a messaging platform.
Without this, resuming into an active CLI session corrupts both context
windows — the model sees interleaved tool calls and responses from two
different conversations, leading to hallucinations about files and state.
The fork only applies to --continue (first connection after startup,
where we grab "whatever is latest" which may be an active CLI session).
Plain --resume of cc-connect's own stored session ID is unaffected,
since that session belongs to cc-connect and has no interleaving risk.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In handleMessage, the check to skip handleWorkspaceInitFlow for
/workspace and /ws commands was comparing against "workspace" and "ws "
without the leading "/" prefix. Since content retains its "/" at this
point, the check always evaluated to false, causing /workspace commands
to be consumed by the init flow when it was active.
This meant that during an active workspace init flow, sending
"/workspace bind <name>" would trigger the "not a git URL" error
instead of being routed to the workspace command handler.
The changes add error handling for previously ignored operations and introduce logging for critical errors and warnings. This includes:
- Adding error checks for file operations, JSON unmarshaling, system calls
- Logging warnings/errors for failed operations using slog
- Properly handling return values from functions that were previously ignored
- Adding context to error messages for better debugging
- Cleaning up unused code and variables
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>
Implement card rendering for /whoami command when platform supports cards, with proper formatting of user identity information. Added extractPlatformName helper and new i18n messages. Includes test case for card rendering.
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>
- Added new /whoami command (with /myid alias) to show user identity info
- Updated /status command to include User ID in output
- Modified allow_from guidance to recommend /whoami instead of manual lookup
- Added i18n strings and tests for new functionality
- Updated help menus and config example comments to reference /whoami
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>
SetPlatformPrompt is only called when the platform implements
FormattingInstructionProvider. If a Slack message sets the prompt to
mrkdwn instructions and then a Telegram message arrives (which does
not implement the interface), the stale Slack prompt persists and
leaks into the Telegram session.
Always call SetPlatformPrompt — with the platform's instructions if
available, or with an empty string to clear any stale prompt.
- Replaced 15 occurrences of fmt.Sprintf("❌ %v", err) with e.i18n.Tf(MsgError, err)
- Replaced 3 occurrences of fmt.Sprintf("❌ %s", ...) with e.i18n.Tf(MsgError, ...)
- Added MsgFailedToDeleteSession with multi-language support
- Remaining uncounted: cron job errors (lines 750/752) have special formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Added MsgFailedToStartAgentSession with EN/ZH/ZH-TW/JA/ES translations
- Updated engine.go to use i18n key directly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>