feat(display): [mode=rich] opt-in Card 2.0 single-card turn + reply elapsed time footer#657
Open
AaronZ345 wants to merge 5 commits intochenhg5:mainfrom
Open
feat(display): [mode=rich] opt-in Card 2.0 single-card turn + reply elapsed time footer#657AaronZ345 wants to merge 5 commits intochenhg5:mainfrom
AaronZ345 wants to merge 5 commits intochenhg5:mainfrom
Conversation
Owner
|
CI 失败 (lint): core/streaming.go:419 - U1000: func 请删除未使用的函数。 |
6eb945f to
21efe31
Compare
chenhg5
approved these changes
Apr 17, 2026
Owner
chenhg5
left a comment
There was a problem hiding this comment.
LGTM. Card 2.0 rich mode实现完整:
RichCardSupporter接口设计良好,平台侧实现可选,对不支持的平台向后兼容CardStatus(thinking/working/done/error) + elapsed time footer 提升 UXToolStep结构化工具调用,streaming 时分段展示- legacy mode 保持原有行为,默认关闭
- CI 全绿(lint 已修复)
这是一个较大的 feature,建议合并前确认 human 审核通过。
4517b16 to
8b57889
Compare
4 tasks
97047ba to
5ca5ed3
Compare
This was referenced Apr 27, 2026
5ca5ed3 to
18a6057
Compare
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 28, 2026
…card fallback Per Lark official icon library (https://open.feishu.cn/document/feishu-cards/enumerations-for-icons), 4 standard_icon tokens used in chenhg5#657's toolIconMap are not in the official enumeration: - terminal-two_outlined (Bash) -> code_outlined - file-open_outlined (Read) -> file-link-text_outlined - notes_outlined (Write) -> richtext_outlined - folder-open_outlined (Glob) -> creat-folder_outlined ("creat" is the official spelling) Without this fix Lark client renders default placeholder icons or empty icon slots. Also migrate fallback card config 'wide_screen_mode: true' (schema 1.0) to 'width_mode: "default"' (schema 2.0) for self-consistency since the card declares schema: "2.0". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 28, 2026
… (Phase A.5) B018 Phase A.5 — implements true Lark client-side typewriter rendering for the markdown body of chenhg5#657 Card 2.0 rich cards. Replaces chenhg5#657's full-card replace on every EventText with per-element streaming via cardkit-v1. Three-step send flow (replaces single-step Im.Message.Create): 1. POST /open-apis/cardkit/v1/cards { type: card_json, data: <cardJSON> } -> card_id (numeric string, 14-day TTL) 2. POST /open-apis/im/v1/messages with content {"type":"card","data":{"card_id":"..."}} -> message_id 3. PUT /open-apis/cardkit/v1/cards/{card_id}/elements/main_text/content { content: <fullText>, sequence: <monotonic int> } on each EventText All HTTP calls use the lark Go SDK generic client.Post/Put with larkcore.AccessTokenTypeTenant — no raw HTTP, no manual token cache, no hardcoded domain (SDK auto-handles lark international vs feishu domestic). Concurrency: feishuPreviewHandle.mu protects the sequence counter through HTTP completion. Per-card serialization keeps sequence monotonic regardless of concurrent EventText goroutines. Lark's 50 QPS per-element rate limit is well above this throttle. Throttle: cardkit-v1 path uses 200ms / 20 chars (5 updates/sec, smooth typewriter); fallback to full-card Patch keeps original 1500ms / 30 chars since Im.Message.Patch is heavier. Fallback chain (turn never gets stuck): - Create Card Entity fails -> SendPreviewStart falls through to inline card JSON (= original chenhg5#657 path); handle cardID stays empty - StreamRichCardText returns ErrNotSupported (no cardID) or any error -> engine routes EventText to full-card Patch via UpdateMessage; cardID kept for next try - Reply API path (thread) -> skips cardkit-v1 entirely (Reply doesn't document card_id reference support) Schema validated end-to-end via lark-cli before commit: POST /cardkit/v1/cards -> { code: 0, data.card_id: "..." } POST /im/v1/messages with type=card -> { code: 0, data.message_id: "..." } PUT .../elements/main_text/content -> { code: 0 } Files: core/streaming.go + RichCardTextStreamer optional interface core/engine.go EventText: prefer streamer; fallback to full Patch on ErrNotSupported / error; new 200ms throttle when streamer present, else 1500ms platform/feishu/feishu.go + richCardMainTextElementID = "main_text" buildRichCard: markdown element gets element_id feishuPreviewHandle: + cardID, sequence (mu-guarded) SendPreviewStart: two-step flow with fallback + createCardEntity helper + StreamRichCardText (impls RichCardTextStreamer) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 28, 2026
…abling typewriter on every @-mention)
Phase A.5 v1 conservatively skipped cardkit-v1 in Im.Message.Reply path, fearing
the Reply API might not accept the {type:card,data:{card_id}} content schema
(Lark docs only document it for Im.Message.Create). But shouldUseThreadOrReplyAPI()
returns true on every @-mention turn (rc.messageID != "" + reply-to-trigger
default), which is the dominant CCC usage. So cardkit-v1 streaming was effectively
DEAD on every actual user turn — the engine kept falling through to full-card
Patch (= original chenhg5#657 behavior, no typewriter).
Verified by direct Lark API call:
POST /open-apis/im/v1/messages/{message_id}/reply
body: {msg_type: interactive,
content: '{"type":"card","data":{"card_id":"..."}}'}
-> { code: 0, data.message_id: "..." }
Reply API DOES accept the same card_id reference content schema. Fix:
unconditionally try createCardEntity for rich card sends; let cardkit-v1
streaming flow through both Reply and Create paths.
Also bumped the create-card-entity-failure log from Debug to Info so we
can spot fallbacks at the default INFO log level.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 28, 2026
…ard via unified statusFooter (方案 B) The Phase A take-chenhg5#657-for-engine.go reset dropped chenhg5#774's footer rendering on the rich card path. Phase B reintroduces it cleanly through the planned uniform 'statusFooter string' interface so elapsed time, claudecode model/ctx status, and workdir all flow through the same composition pipeline -- no duplicate elapsed-time logic on platform side, no chenhg5#657-vs-chenhg5#774 conflict. Interface refactor: RichCardSupporter.BuildRichCard(..., elapsed time.Duration) ^ removed (..., statusFooter string) ^ added Multi-line, '\n' separated. Empty string = footer hidden. Engine adds composeRichStatusFooter() helper (3 lines, each toggleable): line 1: ⏱ <i18n elapsed> (subject to e.replyFooterEnabled) line 2: model · effort · ctx (subject to e.showContextIndicator) line 3: <workdir> (subject to e.showWorkdirIndicator) Engine adds formatElapsed(d, streaming, lang) with ZH / EN paths: ZH streaming: "⏱ 运行中 12.3 秒..." ZH done: "⏱ 用时 1 分 23 秒" EN streaming: "⏱ Running for 12.3s..." EN done: "⏱ Elapsed 1m 23s" ja/es fall back to EN format -- promote to MsgKey i18n later if needed. Feishu renderer: - drops the local elapsed/footerMap branch (engine owns formatting now) - splits incoming statusFooter on '\n', renders each line as its own markdown element with text_size:notation + text_color:grey - inserts <hr> between body markdown and footer for visual separation - dead formatElapsedCN helper removed All 8 engine BuildRichCard call sites updated to the new signature; feishu Platform.BuildRichCard wrapper signature updated; buildRichCard() inner renderer signature updated. Per-project [display].mode override deferred to a follow-up commit; the existing global [display].mode keeps working unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 28, 2026
…card fallback Per Lark official icon library (https://open.feishu.cn/document/feishu-cards/enumerations-for-icons), 4 standard_icon tokens used in chenhg5#657's toolIconMap are not in the official enumeration: - terminal-two_outlined (Bash) -> code_outlined - file-open_outlined (Read) -> file-link-text_outlined - notes_outlined (Write) -> richtext_outlined - folder-open_outlined (Glob) -> creat-folder_outlined ("creat" is the official spelling) Without this fix Lark client renders default placeholder icons or empty icon slots. Also migrate fallback card config 'wide_screen_mode: true' (schema 1.0) to 'width_mode: "default"' (schema 2.0) for self-consistency since the card declares schema: "2.0". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 28, 2026
… (Phase A.5) B018 Phase A.5 — implements true Lark client-side typewriter rendering for the markdown body of chenhg5#657 Card 2.0 rich cards. Replaces chenhg5#657's full-card replace on every EventText with per-element streaming via cardkit-v1. Three-step send flow (replaces single-step Im.Message.Create): 1. POST /open-apis/cardkit/v1/cards { type: card_json, data: <cardJSON> } -> card_id (numeric string, 14-day TTL) 2. POST /open-apis/im/v1/messages with content {"type":"card","data":{"card_id":"..."}} -> message_id 3. PUT /open-apis/cardkit/v1/cards/{card_id}/elements/main_text/content { content: <fullText>, sequence: <monotonic int> } on each EventText All HTTP calls use the lark Go SDK generic client.Post/Put with larkcore.AccessTokenTypeTenant — no raw HTTP, no manual token cache, no hardcoded domain (SDK auto-handles lark international vs feishu domestic). Concurrency: feishuPreviewHandle.mu protects the sequence counter through HTTP completion. Per-card serialization keeps sequence monotonic regardless of concurrent EventText goroutines. Lark's 50 QPS per-element rate limit is well above this throttle. Throttle: cardkit-v1 path uses 200ms / 20 chars (5 updates/sec, smooth typewriter); fallback to full-card Patch keeps original 1500ms / 30 chars since Im.Message.Patch is heavier. Fallback chain (turn never gets stuck): - Create Card Entity fails -> SendPreviewStart falls through to inline card JSON (= original chenhg5#657 path); handle cardID stays empty - StreamRichCardText returns ErrNotSupported (no cardID) or any error -> engine routes EventText to full-card Patch via UpdateMessage; cardID kept for next try - Reply API path (thread) -> skips cardkit-v1 entirely (Reply doesn't document card_id reference support) Schema validated end-to-end via lark-cli before commit: POST /cardkit/v1/cards -> { code: 0, data.card_id: "..." } POST /im/v1/messages with type=card -> { code: 0, data.message_id: "..." } PUT .../elements/main_text/content -> { code: 0 } Files: core/streaming.go + RichCardTextStreamer optional interface core/engine.go EventText: prefer streamer; fallback to full Patch on ErrNotSupported / error; new 200ms throttle when streamer present, else 1500ms platform/feishu/feishu.go + richCardMainTextElementID = "main_text" buildRichCard: markdown element gets element_id feishuPreviewHandle: + cardID, sequence (mu-guarded) SendPreviewStart: two-step flow with fallback + createCardEntity helper + StreamRichCardText (impls RichCardTextStreamer) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 28, 2026
…abling typewriter on every @-mention)
Phase A.5 v1 conservatively skipped cardkit-v1 in Im.Message.Reply path, fearing
the Reply API might not accept the {type:card,data:{card_id}} content schema
(Lark docs only document it for Im.Message.Create). But shouldUseThreadOrReplyAPI()
returns true on every @-mention turn (rc.messageID != "" + reply-to-trigger
default), which is the dominant CCC usage. So cardkit-v1 streaming was effectively
DEAD on every actual user turn — the engine kept falling through to full-card
Patch (= original chenhg5#657 behavior, no typewriter).
Verified by direct Lark API call:
POST /open-apis/im/v1/messages/{message_id}/reply
body: {msg_type: interactive,
content: '{"type":"card","data":{"card_id":"..."}}'}
-> { code: 0, data.message_id: "..." }
Reply API DOES accept the same card_id reference content schema. Fix:
unconditionally try createCardEntity for rich card sends; let cardkit-v1
streaming flow through both Reply and Create paths.
Also bumped the create-card-entity-failure log from Debug to Info so we
can spot fallbacks at the default INFO log level.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 28, 2026
…ard via unified statusFooter (方案 B) The Phase A take-chenhg5#657-for-engine.go reset dropped chenhg5#774's footer rendering on the rich card path. Phase B reintroduces it cleanly through the planned uniform 'statusFooter string' interface so elapsed time, claudecode model/ctx status, and workdir all flow through the same composition pipeline -- no duplicate elapsed-time logic on platform side, no chenhg5#657-vs-chenhg5#774 conflict. Interface refactor: RichCardSupporter.BuildRichCard(..., elapsed time.Duration) ^ removed (..., statusFooter string) ^ added Multi-line, '\n' separated. Empty string = footer hidden. Engine adds composeRichStatusFooter() helper (3 lines, each toggleable): line 1: ⏱ <i18n elapsed> (subject to e.replyFooterEnabled) line 2: model · effort · ctx (subject to e.showContextIndicator) line 3: <workdir> (subject to e.showWorkdirIndicator) Engine adds formatElapsed(d, streaming, lang) with ZH / EN paths: ZH streaming: "⏱ 运行中 12.3 秒..." ZH done: "⏱ 用时 1 分 23 秒" EN streaming: "⏱ Running for 12.3s..." EN done: "⏱ Elapsed 1m 23s" ja/es fall back to EN format -- promote to MsgKey i18n later if needed. Feishu renderer: - drops the local elapsed/footerMap branch (engine owns formatting now) - splits incoming statusFooter on '\n', renders each line as its own markdown element with text_size:notation + text_color:grey - inserts <hr> between body markdown and footer for visual separation - dead formatElapsedCN helper removed All 8 engine BuildRichCard call sites updated to the new signature; feishu Platform.BuildRichCard wrapper signature updated; buildRichCard() inner renderer signature updated. Per-project [display].mode override deferred to a follow-up commit; the existing global [display].mode keeps working unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Based on upstream PR chenhg5#309 (Card 2.0 rich cards with collapsible panel and streaming, which already contains PR chenhg5#306's header status colors). Resolves 15 conflict hunks by taking main as base and appending Card 2.0 renderer. Changes: - core/streaming.go: RichCardSupporter.BuildRichCard gains elapsed time.Duration parameter - core/engine.go: hasRichCard path renders a single streaming card for the whole turn; non-RichCardSupporter platforms keep main's compact/legacy progress behavior unchanged - platform/feishu/feishu.go: +~350 lines - collapsible tool-step panel with icons - thinking verb header, status-colored (blue/green/red) - streaming_mode card updates (1500ms / 30-char throttle via engine) - splitMarkdownByTables for long tables - SetPreviewStatus for header color patches - feishuPreviewHandle: +mu/status/lastContent - SendPreviewStart / UpdateMessage recognize pre-built card JSON (isCardJSON) to skip double-wrapping - Timing footer: "⏱ 运行中 X 秒..." while streaming, "⏱ 用时 1 分 23 秒" on completion, appended as a separate div below main's reply footer (model · effort · workdir) Scope: only platforms implementing RichCardSupporter (currently feishu) get the new single-card experience. Other platforms unaffected.
The Card 2.0 rich card path used to bypass display.ThinkingMessages / display.ToolMessages entirely, so /quiet had no effect on feishu: the card still showed the Thinking... header and accumulated tool steps in the collapsible panel. Now: - EventThinking: when display.ThinkingMessages is false, skip card creation. The card is created later by EventText (streaming markdown) or by EventResult (final completed card). - EventToolUse: when display.ToolMessages is false, skip toolSteps append and card create/update. Final card from EventResult has empty toolSteps, so buildRichCard renders markdown-only (no panel).
…d 2.0 Gate the Card 2.0 hasRichCard path behind a config switch so each fork user can pick between upstream behavior and the rich card experience without recompiling. - DisplayConfig.Mode (*string toml "mode") — "legacy" (default) or "rich" - core.DisplayCfg.Mode string propagated via SetDisplayConfig - engine.go: force hasRichCard=false when mode != "rich", so existing Card 2.0 branches are only reached when explicitly enabled - EffectiveDisplay returns the resolved mode; invalid values fall back to "legacy" - config.toml (user) set mode = "rich" to keep current behavior active
When an agent makes many tool calls in one turn (e.g. 50+ MCP queries), buildRichCard packed every step into the collapsible_panel.elements, causing two problems: 1. Lark client renders huge collapsible panels poorly — the JSON view showed mangled fields where later body elements appeared spliced into the last panel step's text.content. 2. The card payload approached/exceeded Feishu's ~30KB interactive card limit, at which point the API rejected the card or the client rendered it as a JSON dump. Two minimal mitigations: - Cap panel rows at 30. Excess steps collapse into a single "… and N more steps" row. Tool execution itself is unaffected; only the panel preview is condensed. - After json.Marshal, if the card exceeds 28000 bytes, fall back to buildCardJSONWithStatus (markdown body only, no panel). Preserves the agent's reply text and status header even on degenerate turns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ch mode Two related rich-mode bugs surfaced after Phase A.5 deployment. 1. NO_REPLY + rich mode -> card stuck in 'Working' forever The rich path tracks the in-flight card via cardMessageID, an engine-local handle independent of sp.previewMsgID (the streamPreview's own state). The isSilent branch in EventResult only called sp.discard(), which deletes sp.previewMsgID -- but in rich mode that handle is usually nil because the card was created via starter.SendPreviewStart directly, not via the sp pipeline. Result: the rich card was never deleted or transitioned to Done, leaving a 'Working' (or 'Pondering') indicator visible to the user even though the agent had decided to suppress its reply. Fix: in the isSilent branch, also delete cardMessageID via PreviewCleaner. NO_REPLY now truly leaves no visible trace. 2. Done emoji is noise in rich mode The done reaction (green checkmark) was meant to signal 'agent finished' on platforms where the streaming preview update was silent (Lark in-place UpdateMessage doesn't ping the user, but the visible result is the same text). In rich mode the card's header template flips from blue 'Working' to green 'Done' on EventResult, which is a far more visible 'done' signal than a separate emoji on the trigger message. The emoji becomes redundant visual clutter on every turn. Fix: skip AddDoneReaction when hasRichCard is true. Legacy mode behavior unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
303c8c2 to
fecc642
Compare
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 30, 2026
…card fallback Per Lark official icon library (https://open.feishu.cn/document/feishu-cards/enumerations-for-icons), 4 standard_icon tokens used in chenhg5#657's toolIconMap are not in the official enumeration: - terminal-two_outlined (Bash) -> code_outlined - file-open_outlined (Read) -> file-link-text_outlined - notes_outlined (Write) -> richtext_outlined - folder-open_outlined (Glob) -> creat-folder_outlined ("creat" is the official spelling) Without this fix Lark client renders default placeholder icons or empty icon slots. Also migrate fallback card config 'wide_screen_mode: true' (schema 1.0) to 'width_mode: "default"' (schema 2.0) for self-consistency since the card declares schema: "2.0". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 30, 2026
… (Phase A.5) B018 Phase A.5 — implements true Lark client-side typewriter rendering for the markdown body of chenhg5#657 Card 2.0 rich cards. Replaces chenhg5#657's full-card replace on every EventText with per-element streaming via cardkit-v1. Three-step send flow (replaces single-step Im.Message.Create): 1. POST /open-apis/cardkit/v1/cards { type: card_json, data: <cardJSON> } -> card_id (numeric string, 14-day TTL) 2. POST /open-apis/im/v1/messages with content {"type":"card","data":{"card_id":"..."}} -> message_id 3. PUT /open-apis/cardkit/v1/cards/{card_id}/elements/main_text/content { content: <fullText>, sequence: <monotonic int> } on each EventText All HTTP calls use the lark Go SDK generic client.Post/Put with larkcore.AccessTokenTypeTenant — no raw HTTP, no manual token cache, no hardcoded domain (SDK auto-handles lark international vs feishu domestic). Concurrency: feishuPreviewHandle.mu protects the sequence counter through HTTP completion. Per-card serialization keeps sequence monotonic regardless of concurrent EventText goroutines. Lark's 50 QPS per-element rate limit is well above this throttle. Throttle: cardkit-v1 path uses 200ms / 20 chars (5 updates/sec, smooth typewriter); fallback to full-card Patch keeps original 1500ms / 30 chars since Im.Message.Patch is heavier. Fallback chain (turn never gets stuck): - Create Card Entity fails -> SendPreviewStart falls through to inline card JSON (= original chenhg5#657 path); handle cardID stays empty - StreamRichCardText returns ErrNotSupported (no cardID) or any error -> engine routes EventText to full-card Patch via UpdateMessage; cardID kept for next try - Reply API path (thread) -> skips cardkit-v1 entirely (Reply doesn't document card_id reference support) Schema validated end-to-end via lark-cli before commit: POST /cardkit/v1/cards -> { code: 0, data.card_id: "..." } POST /im/v1/messages with type=card -> { code: 0, data.message_id: "..." } PUT .../elements/main_text/content -> { code: 0 } Files: core/streaming.go + RichCardTextStreamer optional interface core/engine.go EventText: prefer streamer; fallback to full Patch on ErrNotSupported / error; new 200ms throttle when streamer present, else 1500ms platform/feishu/feishu.go + richCardMainTextElementID = "main_text" buildRichCard: markdown element gets element_id feishuPreviewHandle: + cardID, sequence (mu-guarded) SendPreviewStart: two-step flow with fallback + createCardEntity helper + StreamRichCardText (impls RichCardTextStreamer) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 30, 2026
…abling typewriter on every @-mention)
Phase A.5 v1 conservatively skipped cardkit-v1 in Im.Message.Reply path, fearing
the Reply API might not accept the {type:card,data:{card_id}} content schema
(Lark docs only document it for Im.Message.Create). But shouldUseThreadOrReplyAPI()
returns true on every @-mention turn (rc.messageID != "" + reply-to-trigger
default), which is the dominant CCC usage. So cardkit-v1 streaming was effectively
DEAD on every actual user turn — the engine kept falling through to full-card
Patch (= original chenhg5#657 behavior, no typewriter).
Verified by direct Lark API call:
POST /open-apis/im/v1/messages/{message_id}/reply
body: {msg_type: interactive,
content: '{"type":"card","data":{"card_id":"..."}}'}
-> { code: 0, data.message_id: "..." }
Reply API DOES accept the same card_id reference content schema. Fix:
unconditionally try createCardEntity for rich card sends; let cardkit-v1
streaming flow through both Reply and Create paths.
Also bumped the create-card-entity-failure log from Debug to Info so we
can spot fallbacks at the default INFO log level.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 30, 2026
…ard via unified statusFooter (方案 B) The Phase A take-chenhg5#657-for-engine.go reset dropped chenhg5#774's footer rendering on the rich card path. Phase B reintroduces it cleanly through the planned uniform 'statusFooter string' interface so elapsed time, claudecode model/ctx status, and workdir all flow through the same composition pipeline -- no duplicate elapsed-time logic on platform side, no chenhg5#657-vs-chenhg5#774 conflict. Interface refactor: RichCardSupporter.BuildRichCard(..., elapsed time.Duration) ^ removed (..., statusFooter string) ^ added Multi-line, '\n' separated. Empty string = footer hidden. Engine adds composeRichStatusFooter() helper (3 lines, each toggleable): line 1: ⏱ <i18n elapsed> (subject to e.replyFooterEnabled) line 2: model · effort · ctx (subject to e.showContextIndicator) line 3: <workdir> (subject to e.showWorkdirIndicator) Engine adds formatElapsed(d, streaming, lang) with ZH / EN paths: ZH streaming: "⏱ 运行中 12.3 秒..." ZH done: "⏱ 用时 1 分 23 秒" EN streaming: "⏱ Running for 12.3s..." EN done: "⏱ Elapsed 1m 23s" ja/es fall back to EN format -- promote to MsgKey i18n later if needed. Feishu renderer: - drops the local elapsed/footerMap branch (engine owns formatting now) - splits incoming statusFooter on '\n', renders each line as its own markdown element with text_size:notation + text_color:grey - inserts <hr> between body markdown and footer for visual separation - dead formatElapsedCN helper removed All 8 engine BuildRichCard call sites updated to the new signature; feishu Platform.BuildRichCard wrapper signature updated; buildRichCard() inner renderer signature updated. Per-project [display].mode override deferred to a follow-up commit; the existing global [display].mode keeps working unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AaronZ345
pushed a commit
to AaronZ345/cc-connect
that referenced
this pull request
Apr 30, 2026
…d recovery The cherry-picked chenhg5#774 commit (reply footer refactor) re-introduced the old TestStreamPreview_FreezeDeletesOnFinish expectations from main when adding the statusFooter parameter to finish(), but the cherry-picked chenhg5#795 commit (Lark icon + width_mode follow-up to chenhg5#657) had already updated finish() with degraded-recovery behavior — it now attempts UpdateMessage on the degraded preview and returns true on success without deleting. Restore the post-chenhg5#795 expectation: finish should return true when recovery via UpdateMessage succeeds (mockCleanerPlatform embeds mockUpdaterPlatform so the recovery path completes), and drop the "expected 1 delete call" assertion that no longer applies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Today an agent turn produces many separate messages — thinking updates, tool invocations, tool results, final response — cluttering the chat and triggering multiple push notifications per turn. This PR packages #309 (Card 2.0 rich cards) together with three extensions so that one turn = one card:
[display] modeopt-in gate — new config field. Default"legacy"preserves current upstream behavior,"rich"enables the Card 2.0 single-card flow. Users can A/B the two just by editing config and restarting.⏱ 运行中 12.3 秒...while streaming and⏱ 用时 1 分 23 秒when complete, rendered as a separate div below the existing model/effort/workdir reply footer./quietin Card 2.0 path — whendisplay.thinking_messages/display.tool_messagesare false, the rich card skips the "Thinking..." header and the tool-steps panel respectively, mirroring upstream's quiet semantics.User benefit
mode = "rich"): one card, one push. Tool steps live in a collapsible panel (auto-folded when done), markdown body streams in place, elapsed time visible throughout.mode = "legacy"leaves every existing user untouched. Feature is 100% opt-in via a config flag.Relation to #309 / #655
modehere uses"legacy"/"rich"values which do not clash with feat(display): add display_mode enum to replace boolean quiet #655's"full"/"compact"/"quiet"— both fields can coexist, or be unified later (e.g.mode = "rich-quiet"as an orthogonal axis).Testing
go build ./...passesgo test ./...passesmodebetweenrichandlegacyvia config + restart verified both paths work end-to-end.Footprint
config/config.go,cmd/cc-connect/main.go,core/streaming.go,core/engine.go,platform/feishu/feishu.go+ tests.🤖 Generated with Claude Code