From 2160090ec6554895d6aeabc176464eb4d40d564b Mon Sep 17 00:00:00 2001 From: chenhg5 Date: Fri, 6 Mar 2026 19:08:43 +0800 Subject: [PATCH 1/4] feat(streaming): add per-platform streaming preview control Add disabled_platforms config option to disable streaming preview for specific platforms (e.g. feishu) where it causes formatting issues. - Add DisabledPlatforms field to StreamPreviewConfig and StreamPreviewCfg - Check platform name against disabled list in canPreview() - Allows pure text messages to use correct message type instead of cards Co-Authored-By: Claude Opus 4.6 --- cmd/cc-connect/main.go | 13 ++++++++----- config/config.go | 9 +++++---- core/streaming.go | 28 +++++++++++++++++++--------- go.mod | 2 +- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/cmd/cc-connect/main.go b/cmd/cc-connect/main.go index 832d8a4..3c9c489 100644 --- a/cmd/cc-connect/main.go +++ b/cmd/cc-connect/main.go @@ -247,12 +247,15 @@ func main() { spcfg.MinDeltaChars = *cfg.StreamPreview.MinDeltaChars } if cfg.StreamPreview.MaxChars != nil { - spcfg.MaxChars = *cfg.StreamPreview.MaxChars - } - engine.SetStreamPreviewCfg(spcfg) - } + spcfg.MaxChars = *cfg.StreamPreview.MaxChars + } + if cfg.StreamPreview.DisabledPlatforms != nil { + spcfg.DisabledPlatforms = cfg.StreamPreview.DisabledPlatforms + } + engine.SetStreamPreviewCfg(spcfg) + } - // Wire rate limiting + // Wire rate limiting { maxMsg := 20 windowSecs := 60 diff --git a/config/config.go b/config/config.go index 979ad52..c8c7817 100644 --- a/config/config.go +++ b/config/config.go @@ -44,10 +44,11 @@ type DisplayConfig struct { // StreamPreviewConfig controls real-time streaming preview in IM. type StreamPreviewConfig struct { - Enabled *bool `toml:"enabled"` // default true - IntervalMs *int `toml:"interval_ms"` // min ms between updates; default 1500 - MinDeltaChars *int `toml:"min_delta_chars"` // min new chars before update; default 30 - MaxChars *int `toml:"max_chars"` // max preview length; default 2000 + Enabled *bool `toml:"enabled"` // default true + DisabledPlatforms []string `toml:"disabled_platforms,omitempty"` // platforms where preview is disabled (e.g. ["feishu"]) + IntervalMs *int `toml:"interval_ms"` // min ms between updates; default 1500 + MinDeltaChars *int `toml:"min_delta_chars"` // min new chars before update; default 30 + MaxChars *int `toml:"max_chars"` // max preview length; default 2000 } // RateLimitConfig controls per-session message rate limiting. diff --git a/core/streaming.go b/core/streaming.go index 6e121f0..75ef672 100644 --- a/core/streaming.go +++ b/core/streaming.go @@ -3,25 +3,28 @@ package core import ( "context" "log/slog" + "strings" "sync" "time" ) // StreamPreviewCfg controls the streaming preview behavior. type StreamPreviewCfg struct { - Enabled bool // global toggle - IntervalMs int // minimum ms between updates (default 1500) - MinDeltaChars int // minimum new chars before sending an update (default 30) - MaxChars int // max preview length (default 2000) + Enabled bool // global toggle + DisabledPlatforms []string // platforms where streaming preview is disabled (e.g. "feishu") + IntervalMs int // minimum ms between updates (default 1500) + MinDeltaChars int // minimum new chars before sending an update (default 30) + MaxChars int // max preview length (default 2000) } // DefaultStreamPreviewCfg returns sensible defaults. func DefaultStreamPreviewCfg() StreamPreviewCfg { return StreamPreviewCfg{ - Enabled: true, - IntervalMs: 1500, - MinDeltaChars: 30, - MaxChars: 2000, + Enabled: true, + DisabledPlatforms: nil, + IntervalMs: 1500, + MinDeltaChars: 30, + MaxChars: 2000, } } @@ -73,11 +76,18 @@ func newStreamPreview(cfg StreamPreviewCfg, p Platform, replyCtx any, ctx contex } } -// canPreview returns true if the platform supports message updating. +// canPreview returns true if the platform supports message updating and is not disabled. func (sp *streamPreview) canPreview() bool { if sp.degraded || !sp.cfg.Enabled { return false } + // Check if platform is in disabled list + platformName := sp.platform.Name() + for _, disabled := range sp.cfg.DisabledPlatforms { + if strings.EqualFold(disabled, platformName) { + return false + } + } _, ok := sp.platform.(MessageUpdater) return ok } diff --git a/go.mod b/go.mod index f91bb10..47b2e9f 100644 --- a/go.mod +++ b/go.mod @@ -10,13 +10,13 @@ require ( github.com/larksuite/oapi-sdk-go/v3 v3.5.3 github.com/line/line-bot-sdk-go/v8 v8.19.0 github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 + github.com/robfig/cron/v3 v3.0.1 github.com/slack-go/slack v0.15.0 ) require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect ) From 16b028b0985d9924c0b5fca9cbc3715ab971e9f2 Mon Sep 17 00:00:00 2001 From: chenhg5 Date: Fri, 6 Mar 2026 20:31:37 +0800 Subject: [PATCH 2/4] feat(ui): improve status display and fix Feishu message formatting - Add quiet mode status to /status command with i18n support - Fix Feishu help command line breaks (use buildPostJSON instead of md tag) - Improve tool message formatting (code block for long content) - Strip XML tags from session summary display Co-Authored-By: Claude Opus 4.6 --- agent/claudecode/claudecode.go | 9 ++++++ core/engine.go | 17 +++++++++- core/i18n.go | 58 ++++++++++++++++++++++++---------- platform/feishu/feishu.go | 24 +++----------- 4 files changed, 71 insertions(+), 37 deletions(-) diff --git a/agent/claudecode/claudecode.go b/agent/claudecode/claudecode.go index 1b932e9..958cee1 100644 --- a/agent/claudecode/claudecode.go +++ b/agent/claudecode/claudecode.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "sort" "strings" "sync" @@ -326,12 +327,20 @@ func scanSessionMeta(path string) (string, int) { } } } + summary = stripXMLTags(summary) + summary = strings.TrimSpace(summary) if utf8.RuneCountInString(summary) > 40 { summary = string([]rune(summary)[:40]) + "..." } return summary, count } +var xmlTagRe = regexp.MustCompile(`<[^>]+>`) + +func stripXMLTags(s string) string { + return xmlTagRe.ReplaceAllString(s, "") +} + // GetSessionHistory reads the Claude Code JSONL transcript and returns user/assistant messages. func (a *Agent) GetSessionHistory(_ context.Context, sessionID string, limit int) ([]core.HistoryEntry, error) { homeDir, err := os.UserHomeDir() diff --git a/core/engine.go b/core/engine.go index 4f4a814..70474f7 100644 --- a/core/engine.go +++ b/core/engine.go @@ -972,7 +972,15 @@ func (e *Engine) processInteractiveEvents(state *interactiveState, session *Sess if !quiet { sp.freeze() inputPreview := truncateIf(event.ToolInput, e.display.ToolMaxLen) - e.send(p, replyCtx, fmt.Sprintf(e.i18n.T(MsgTool), toolCount, event.ToolName, inputPreview)) + // Use code block if content is long (>5 lines or >200 chars), otherwise inline code + lineCount := strings.Count(inputPreview, "\n") + 1 + var formattedInput string + if lineCount > 5 || utf8.RuneCountInString(inputPreview) > 200 { + formattedInput = fmt.Sprintf("```\n%s\n```", inputPreview) + } else { + formattedInput = fmt.Sprintf("`%s`", inputPreview) + } + e.send(p, replyCtx, fmt.Sprintf(e.i18n.T(MsgTool), toolCount, event.ToolName, formattedInput)) } case EventText: @@ -1655,6 +1663,13 @@ func (e *Engine) cmdStatus(p Platform, msg *Message) { } } + // Quiet mode + quietStr := e.i18n.T(MsgQuietOffShort) + if state, ok := e.interactiveStates[msg.SessionKey]; ok && state.quiet { + quietStr = e.i18n.T(MsgQuietOnShort) + } + modeStr += e.i18n.Tf(MsgStatusQuiet, quietStr) + // Session info s := e.sessions.GetOrCreateActive(msg.SessionKey) sessionDisplayName := e.sessions.GetSessionName(s.AgentSessionID) diff --git a/core/i18n.go b/core/i18n.go index 91cfbd9..fa8bd85 100644 --- a/core/i18n.go +++ b/core/i18n.go @@ -215,6 +215,9 @@ const ( MsgStatusMode MsgKey = "status_mode" MsgStatusSession MsgKey = "status_session" MsgStatusCron MsgKey = "status_cron" + MsgStatusQuiet MsgKey = "status_quiet" + MsgQuietOnShort MsgKey = "quiet_on_short" + MsgQuietOffShort MsgKey = "quiet_off_short" MsgModelDefault MsgKey = "model_default" MsgModelListTitle MsgKey = "model_list_title" @@ -257,8 +260,8 @@ const ( MsgDoctorTitle MsgKey = "doctor_title" MsgDoctorSummary MsgKey = "doctor_summary" - MsgRestarting MsgKey = "restarting" - MsgRestartSuccess MsgKey = "restart_success" + MsgRestarting MsgKey = "restarting" + MsgRestartSuccess MsgKey = "restart_success" MsgUpgradeChecking MsgKey = "upgrade_checking" MsgUpgradeUpToDate MsgKey = "upgrade_up_to_date" @@ -267,12 +270,12 @@ const ( MsgUpgradeSuccess MsgKey = "upgrade_success" MsgUpgradeDevBuild MsgKey = "upgrade_dev_build" - MsgAliasEmpty MsgKey = "alias_empty" + MsgAliasEmpty MsgKey = "alias_empty" MsgAliasListHeader MsgKey = "alias_list_header" - MsgAliasAdded MsgKey = "alias_added" - MsgAliasDeleted MsgKey = "alias_deleted" - MsgAliasNotFound MsgKey = "alias_not_found" - MsgAliasUsage MsgKey = "alias_usage" + MsgAliasAdded MsgKey = "alias_added" + MsgAliasDeleted MsgKey = "alias_deleted" + MsgAliasNotFound MsgKey = "alias_not_found" + MsgAliasUsage MsgKey = "alias_usage" MsgNewSessionCreated MsgKey = "new_session_created" MsgNewSessionCreatedName MsgKey = "new_session_created_name" @@ -296,11 +299,11 @@ const ( MsgRelayNotFound MsgKey = "relay_not_found" MsgRelayNoTarget MsgKey = "relay_no_target" - MsgSearchUsage MsgKey = "search_usage" - MsgSearchError MsgKey = "search_error" - MsgSearchNoResult MsgKey = "search_no_result" - MsgSearchResult MsgKey = "search_result" - MsgSearchHint MsgKey = "search_hint" + MsgSearchUsage MsgKey = "search_usage" + MsgSearchError MsgKey = "search_error" + MsgSearchNoResult MsgKey = "search_no_result" + MsgSearchResult MsgKey = "search_result" + MsgSearchHint MsgKey = "search_hint" ) var messages = map[MsgKey]map[Language]string{ @@ -316,11 +319,11 @@ var messages = map[MsgKey]map[Language]string{ LangChinese: "💭 %s", }, MsgTool: { - LangEnglish: "🔧 Tool #%d: **%s**\n`%s`", - LangChinese: "🔧 工具 #%d: **%s**\n`%s`", - LangTraditionalChinese: "🔧 工具 #%d: **%s**\n`%s`", - LangJapanese: "🔧 ツール #%d: **%s**\n`%s`", - LangSpanish: "🔧 Herramienta #%d: **%s**\n`%s`", + LangEnglish: "🔧 **Tool #%d: %s**\n---\n%s", + LangChinese: "🔧 **工具 #%d: %s**\n---\n%s", + LangTraditionalChinese: "🔧 **工具 #%d: %s**\n---\n%s", + LangJapanese: "🔧 **ツール #%d: %s**\n---\n%s", + LangSpanish: "🔧 **Herramienta #%d: %s**\n---\n%s", }, MsgExecutionStopped: { LangEnglish: "⏹ Execution stopped.", @@ -1147,6 +1150,27 @@ var messages = map[MsgKey]map[Language]string{ LangJapanese: "スケジュールタスク: %d (有効: %d)\n", LangSpanish: "Tareas programadas: %d (habilitadas: %d)\n", }, + MsgStatusQuiet: { + LangEnglish: "Quiet mode: %s\n", + LangChinese: "安静模式: %s\n", + LangTraditionalChinese: "安靜模式: %s\n", + LangJapanese: "出力抑制モード: %s\n", + LangSpanish: "Modo silencioso: %s\n", + }, + MsgQuietOnShort: { + LangEnglish: "ON", + LangChinese: "开启", + LangTraditionalChinese: "開啟", + LangJapanese: "ON", + LangSpanish: "Activado", + }, + MsgQuietOffShort: { + LangEnglish: "OFF", + LangChinese: "关闭", + LangTraditionalChinese: "關閉", + LangJapanese: "OFF", + LangSpanish: "Desactivado", + }, MsgModelDefault: { LangEnglish: "Current model: (not set, using agent default)\n", LangChinese: "当前模型: (未设置,使用 Agent 默认值)\n", diff --git a/platform/feishu/feishu.go b/platform/feishu/feishu.go index 05c8fe3..94c41cf 100644 --- a/platform/feishu/feishu.go +++ b/platform/feishu/feishu.go @@ -449,13 +449,13 @@ func buildReplyContent(content string) (msgType string, body string) { b, _ := json.Marshal(map[string]string{"text": content}) return larkim.MsgTypeText, string(b) } - // Dual rendering strategy (aligned with Claude-to-IM / Openclaw): - // - Code blocks / tables → interactive card (schema 2.0 markdown), smaller font but renders code properly - // - Other markdown → post with md tag, normal chat font size + // Dual rendering strategy: + // - Code blocks / tables → interactive card (schema 2.0 markdown), renders code/tables properly + // - Other markdown → post (rich text), preserves blank-line spacing and supports inline formatting if hasComplexMarkdown(content) { return larkim.MsgTypeInteractive, buildCardJSON(preprocessFeishuMarkdown(content)) } - return larkim.MsgTypePost, buildPostMdJSON(content) + return larkim.MsgTypePost, buildPostJSON(content) } // hasComplexMarkdown detects code blocks or tables that require card rendering. @@ -473,21 +473,6 @@ func hasComplexMarkdown(s string) bool { return false } -// buildPostMdJSON builds a Feishu post message using the md tag, -// which renders markdown at normal chat font size. -func buildPostMdJSON(content string) string { - post := map[string]any{ - "zh_cn": map[string]any{ - "content": [][]map[string]any{ - { - {"tag": "md", "text": content}, - }, - }, - }, - } - b, _ := json.Marshal(post) - return string(b) -} // preprocessFeishuMarkdown ensures code fences have a newline before them, // which prevents rendering issues in Feishu card markdown. @@ -937,3 +922,4 @@ func (p *Platform) extractPostParts(messageID string, post *postLang) ([]string, } return textParts, images } + From 54ea246f600e4be0d6de7f55ae1e3b628c7e8313 Mon Sep 17 00:00:00 2001 From: chenhg5 Date: Fri, 6 Mar 2026 20:34:13 +0800 Subject: [PATCH 3/4] feat(i18n): add translation for session switch success message Co-Authored-By: Claude Opus 4.6 --- core/engine.go | 2 +- core/i18n.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/engine.go b/core/engine.go index 70474f7..384460b 100644 --- a/core/engine.go +++ b/core/engine.go @@ -1436,7 +1436,7 @@ func (e *Engine) cmdSwitch(p Platform, msg *Message, args []string) { displayName = matched.Summary } e.reply(p, msg.ReplyCtx, - fmt.Sprintf("✅ Switched to: %s (%s, %d msgs)", displayName, shortID, matched.MessageCount)) + e.i18n.Tf(MsgSwitchSuccess, displayName, shortID, matched.MessageCount)) } // matchSession resolves a user query to an agent session. Priority: diff --git a/core/i18n.go b/core/i18n.go index fa8bd85..47892d5 100644 --- a/core/i18n.go +++ b/core/i18n.go @@ -285,6 +285,8 @@ const ( MsgDeleteActiveDenied MsgKey = "delete_active_denied" MsgDeleteNotSupported MsgKey = "delete_not_supported" + MsgSwitchSuccess MsgKey = "switch_success" + MsgBannedWordBlocked MsgKey = "banned_word_blocked" MsgCommandDisabled MsgKey = "command_disabled" MsgRateLimited MsgKey = "rate_limited" @@ -1569,6 +1571,13 @@ var messages = map[MsgKey]map[Language]string{ LangJapanese: "🗑️ セッション削除:%s", LangSpanish: "🗑️ Sesión eliminada: %s", }, + MsgSwitchSuccess: { + LangEnglish: "✅ Switched to: %s (%s, %d msgs)", + LangChinese: "✅ 已切换到:%s(%s,%d 条消息)", + LangTraditionalChinese: "✅ 已切換到:%s(%s,%d 則訊息)", + LangJapanese: "✅ 切り替え:%s(%s、%d件)", + LangSpanish: "✅ Cambiado a: %s (%s, %d mensajes)", + }, MsgDeleteActiveDenied: { LangEnglish: "❌ Cannot delete the currently active session. Switch to another session first.", LangChinese: "❌ 不能删除当前活跃会话,请先切换到其他会话。", From 896d8db6ab88ae5767095a01f7a9d40b8390094f Mon Sep 17 00:00:00 2001 From: chenhg5 Date: Fri, 6 Mar 2026 20:40:16 +0800 Subject: [PATCH 4/4] feat(feishu): add three-tier rendering strategy for markdown messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Code blocks/tables → card (schema 2.0 markdown) - Many \n\n paragraphs (≥4, e.g. help, status) → post rich-text (preserves blank lines) - Other markdown → post md tag (best native rendering) Co-Authored-By: Claude Opus 4.6 --- platform/feishu/feishu.go | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/platform/feishu/feishu.go b/platform/feishu/feishu.go index 94c41cf..2204978 100644 --- a/platform/feishu/feishu.go +++ b/platform/feishu/feishu.go @@ -449,13 +449,17 @@ func buildReplyContent(content string) (msgType string, body string) { b, _ := json.Marshal(map[string]string{"text": content}) return larkim.MsgTypeText, string(b) } - // Dual rendering strategy: - // - Code blocks / tables → interactive card (schema 2.0 markdown), renders code/tables properly - // - Other markdown → post (rich text), preserves blank-line spacing and supports inline formatting + // Three-tier rendering strategy: + // 1. Code blocks / tables → card (schema 2.0 markdown) + // 2. Many \n\n paragraphs (help, status, etc.) → post rich-text (preserves blank lines) + // 3. Other markdown → post md tag (best native rendering) if hasComplexMarkdown(content) { return larkim.MsgTypeInteractive, buildCardJSON(preprocessFeishuMarkdown(content)) } - return larkim.MsgTypePost, buildPostJSON(content) + if strings.Count(content, "\n\n") >= 4 { + return larkim.MsgTypePost, buildPostJSON(content) + } + return larkim.MsgTypePost, buildPostMdJSON(content) } // hasComplexMarkdown detects code blocks or tables that require card rendering. @@ -473,6 +477,21 @@ func hasComplexMarkdown(s string) bool { return false } +// buildPostMdJSON builds a Feishu post message using the md tag, +// which renders markdown at normal chat font size. +func buildPostMdJSON(content string) string { + post := map[string]any{ + "zh_cn": map[string]any{ + "content": [][]map[string]any{ + { + {"tag": "md", "text": content}, + }, + }, + }, + } + b, _ := json.Marshal(post) + return string(b) +} // preprocessFeishuMarkdown ensures code fences have a newline before them, // which prevents rendering issues in Feishu card markdown.