fix(feishu): valid icon tokens + width_mode for Card 2.0 rich mode (follow-up to #657)#795
fix(feishu): valid icon tokens + width_mode for Card 2.0 rich mode (follow-up to #657)#795Cigarrr wants to merge 6 commits intochenhg5:mainfrom
Conversation
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>
GetDisabledCommands() returns map keys, whose iteration order is randomized by Go. The assertion expected the input order [new, delete] but got [delete, new] ~50% of runs. Sort before comparing.
…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>
chenhg5
left a comment
There was a problem hiding this comment.
Review Summary (Pending CI)
Large follow-up to #657 (Card 2.0 rich mode). The code looks solid, but CI hasn't started yet. Will approve once CI passes.
✅ Good:
-
Icon token fixes: 4 invalid
standard_icontokens replaced with valid Lark enumeration values:terminal-two_outlined→code_outlinedfile-open_outlined→file-link-text_outlinednotes_outlined→richtext_outlinedfolder-open_outlined→creat-folder_outlined(yes, "creat" is the official spelling)
-
width_mode: "default": Correct Card 2.0 schema field instead of deprecatedwide_screen_mode: true. -
[display] mode = "rich"opt-in: Card 2.0 rich mode is opt-in, defaulting to "legacy" for all platforms. Good progressive rollout strategy. -
EffectiveDisplayreturns 5 values: Now includesmodestring. -
Test updates:
TestEffectiveDisplayQuietupdated to unpack 5 return values. -
TestMgmt_ProjectPatch_DisabledCommands: Addssort.Strings(got)to fix flaky test expectation (same fix as #782).
💭 Observations:
streamPreview.finish()now attempts degraded recovery via UpdateMessage before deletingTestStreamPreview_FreezeDeletesOnFinishexpectation updated to reflect successful recovery
Will approve once CI is green.
chenhg5
left a comment
There was a problem hiding this comment.
Review Summary
REQUEST_CHANGES for now. I found one correctness issue in the rich-card path, and this PR still does not have GitHub checks on the current head.
🔴 Correctness: rich card path can leak NO_REPLY/silent responses
In core/engine.go, the mode = "rich" path creates/updates Feishu cards directly from EventText via cardMessageID, bypassing the legacy silentHold guard. When EventResult later detects a silent response, it only calls sp.discard(), but the rich card is not owned by sp, so an already-created card can remain visible with NO_REPLY or partial silent content.
Please either delay rich-card preview creation until the response is known not to be a NO_REPLY prefix, or explicitly delete/clean up the rich card when isSilent is true. Please add a focused test for rich mode silent suppression.
🟡 Process/mergeability
This PR still includes the #657 base commits while #657 is open, so it is not yet the clean follow-up diff described in the PR. Please merge/rebase after #657 or otherwise clarify that #795 is intended to supersede #657.
CI: no checks are reported for the current head, so QA cannot confirm build/test pass yet.
|
@chenhg5 — Re: rich-card NO_REPLY leak The root cause is in #657 (where the
The fix extends the Once #657 picks this up, #795 on top of that base no longer leaks NO_REPLY content. Happy to add a focused silent-suppression test on #657 if you'd prefer it live alongside the fix. |
…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>
Summary
Two small fixes on top of @AaronZ345's excellent Card 2.0 rich mode work in #657:
standard_icontokens intoolIconMapthat aren't in Lark's official enumerationwidth_mode: "default"instead ofwide_screen_mode: truein the rich-card fallback config (the schema 2.0 field name)Why this is a follow-up to #657
This PR is based on the
pr-657-basebranch (i.e. all 5 commits from #657 are visible in the diff below mine), because the code being patched is introduced by #657 —toolIconMapand the rich-card fallback config don't exist onmainyet.Massive thanks to @AaronZ345 for the rich card foundation. After dogfooding #657 in production for a week, the only friction we hit was these two cosmetic issues. Filing them as a separate PR rather than asking @AaronZ345 to amend, so #657 can proceed on its own timeline.
Recommended merge order: merge #657 first → rebase this PR onto
main(will become a clean 5-line diff) → merge.Detail 1: invalid icon tokens
Per Lark official icon library enumeration, 4 of the tokens currently in
toolIconMapare not in the official list and render as the generic fallback icon when used in Lark cards:terminal-two_outlinedcode_outlinedfile-open_outlinedfile-link-text_outlinednotes_outlinedrichtext_outlinedfolder-open_outlinedcreat-folder_outlinedVerified by sending parallel test cards via
lark-cliwith the original vs the replacement tokens — the originals showed the generic fallback, the replacements rendered correctly.Detail 2: width_mode
The fallback path's card config in
buildCardJSONWithStatuswas using:which is the Card 1.0 / message-card field. For Card 2.0 the equivalent field is:
This is a no-op for the Card 2.0 schema since the field is just unrecognized, but using the right name keeps the config self-consistent across the file.
Tests
go test ./...passes (the same 4 pre-existing failures onpr-657-basecarry through; none related to this change).