Skip to content

fix(feishu): valid icon tokens + width_mode for Card 2.0 rich mode (follow-up to #657)#795

Open
Cigarrr wants to merge 6 commits intochenhg5:mainfrom
Cigarrr:pr-1-rich-card-icons-width-mode
Open

fix(feishu): valid icon tokens + width_mode for Card 2.0 rich mode (follow-up to #657)#795
Cigarrr wants to merge 6 commits intochenhg5:mainfrom
Cigarrr:pr-1-rich-card-icons-width-mode

Conversation

@Cigarrr
Copy link
Copy Markdown
Contributor

@Cigarrr Cigarrr commented Apr 27, 2026

Summary

Two small fixes on top of @AaronZ345's excellent Card 2.0 rich mode work in #657:

  1. Replace 4 invalid standard_icon tokens in toolIconMap that aren't in Lark's official enumeration
  2. Use width_mode: "default" instead of wide_screen_mode: true in 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-base branch (i.e. all 5 commits from #657 are visible in the diff below mine), because the code being patched is introduced by #657toolIconMap and the rich-card fallback config don't exist on main yet.

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 toolIconMap are not in the official list and render as the generic fallback icon when used in Lark cards:

Old (invalid) New (valid) Used for
terminal-two_outlined code_outlined Bash / shell tool
file-open_outlined file-link-text_outlined Read tool
notes_outlined richtext_outlined Edit / Write tool
folder-open_outlined creat-folder_outlined Glob / directory tool

Verified by sending parallel test cards via lark-cli with 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 buildCardJSONWithStatus was using:

"wide_screen_mode": true

which is the Card 1.0 / message-card field. For Card 2.0 the equivalent field is:

"width_mode": "default"

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 on pr-657-base carry through; none related to this change).

张彧 and others added 6 commits April 25, 2026 12:49
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>
Copy link
Copy Markdown
Owner

@chenhg5 chenhg5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Icon token fixes: 4 invalid standard_icon tokens replaced with valid Lark enumeration values:

    • terminal-two_outlinedcode_outlined
    • file-open_outlinedfile-link-text_outlined
    • notes_outlinedrichtext_outlined
    • folder-open_outlinedcreat-folder_outlined (yes, "creat" is the official spelling)
  2. width_mode: "default": Correct Card 2.0 schema field instead of deprecated wide_screen_mode: true.

  3. [display] mode = "rich" opt-in: Card 2.0 rich mode is opt-in, defaulting to "legacy" for all platforms. Good progressive rollout strategy.

  4. EffectiveDisplay returns 5 values: Now includes mode string.

  5. Test updates: TestEffectiveDisplayQuiet updated to unpack 5 return values.

  6. TestMgmt_ProjectPatch_DisabledCommands: Adds sort.Strings(got) to fix flaky test expectation (same fix as #782).


💭 Observations:

  • streamPreview.finish() now attempts degraded recovery via UpdateMessage before deleting
  • TestStreamPreview_FreezeDeletesOnFinish expectation updated to reflect successful recovery

Will approve once CI is green.

Copy link
Copy Markdown
Owner

@chenhg5 chenhg5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@AaronZ345
Copy link
Copy Markdown
Contributor

AaronZ345 commented Apr 30, 2026

@chenhg5Re: rich-card NO_REPLY leak

The root cause is in #657 (where the mode = "rich" path was first introduced) — #795 just inherits the bug via the shared base. I've pushed a fix commit to #657's head:

fix(core): rich-mode card cleanup on NO_REPLY + drop done emoji in rich mode
fecc642 — cherry-picked from @Cigarrr's e80a0df4 (originally in #796)

The fix extends the isSilent branch in processInteractiveEvents to also delete cardMessageID via PreviewCleaner when hasRichCard is true. sp.discard() alone doesn't reach it because the rich card is created via starter.SendPreviewStart outside the sp pipeline.

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.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants