Skip to content

client: a chat bubble's image renders as alt text because #2313's link delegate shadows the host's file resolver #2319

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 69feb72e65224dadb29e413fa6db88c9
Branch: dev/2-bubble-image-delegate
Base: e40cecf
At: 2026-11-04T00:25:10Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.

Written-by: FlyBot Lead

What is wrong

After v0.2.22 (e40cecf6), an image inside a chat bubble renders as its alt text only. A message that contains ![shot](screenshots/a.png), with screenshots/a.png present in the session workspace, shows the word shot and no picture; it cannot be opened in the lightbox either. v0.2.21 showed the image.

This is a regression introduced by #2313 (the #2201 link fix), which is in the v0.2.22 release.

Root cause (verified on live main e40cecf6 and the pinned host @deepseek-ai/dsh@0.2.0-rc.2)

Two facts combine:

  1. The host hands fileImages to the view, not to the node. dsh-client-ui-chat/lib/client.js:5148 builds the resolver (fileMediaUrl(document.baseURI, resolveWorkspacePath(cwd, path))) and :5280-5283 passes it to the MarkdownDelegateProvider that wraps ChatNodeList. The per-node owner object handed to a node renderer (:1727-1738) carries cwd, openFile, openSkill, inspectCall, forkAt, loadImage, renderMessageImages, fileMentions, turnProcess — no fileImages. FlyBot's cells are fed by exactly that owner object, so fileImages is undefined everywhere in our tree (bundle core/client.js:6839/6850/7062/7182/7236; face() and assistantFace also carry none).

  2. A nested MarkdownDelegateProvider replaces the enclosing capabilities. dsh-client-ui-primitives/lib/index.js:11040-11052 builds the context value from exactly its own {openExternalLink, openFile, fileImages} props — "Nested providers replace the enclosing capabilities". So bubbleMarkdown (core/src/client/cells.js:33, :49) wraps MarkdownText in a provider whose fileImages is undefined, shadowing the host's resolver that used to reach the bubble. MarkdownImage (:11543-11549) then gets fileImages?.resolve(...) ?? imageSource(destination, pathImages) → undefined → it draws <span class="imageAlt">{alt}</span>.

Before #2313, core/src/client/cells.js (at a11983c5) imported only MarkdownText and rendered it bare at the same three sites, so the nearest provider was the host's and images resolved. Call sites now: :85 (Bubbles), :614 (EarlierParts), :1002 (GroupReplies).

useMarkdownDelegate is not exported by the primitives package (export list, lib/index.js:12381), so the delegate cannot be read back from context and merged.

Reproduction

  1. Run v0.2.22 and open a chat whose message text is ![shot](screenshots/a.png).
  2. Expected: the image, clickable into the lightbox (preview: fileImages at lib/index.js:11554).
  3. Actual: only the alt text shot; no image, no lightbox.

Why the new tests stayed green

core/test/chat-link-open.test.js:27-31 replaces the provider with a stub that does Object.assign(delegate, { openExternalLink, openFile, fileImages }) and returns children. That models inheritance, not replacement, and it never renders an image, so a shadowed resolver is invisible to it. The whole file only exercises http(s) anchors.

Scope of the fix

  • Keep MarkdownText bare at :85, :614, :1002 — drop the nested MarkdownDelegateProvider so the host's fileImages/openFile reach the bubble again, exactly as in v0.2.21.
  • Move the client: a link in a chat opens the host's right panel behind FlyBot's own fixed panel, so the page looks blank until the panel is closed #2201 link interception out of the delegate and into capture-phase click handling on the containers: Bubbles already has onClickCapture (:55-75); EarlierParts and GroupReplies need the same. Handle only a[href] with an http/https link, preventDefault() + stopPropagation(), then the existing behaviour: actions.browserNewTab(subject) → actions.browserNavigate(tabId, href), window.open fallback when no panel.
  • Leave modifier-click and non-http(s) links (file links, mention anchors) to the host.
  • Regenerate the bundle: node core/scripts/build-client.mjs (--check must pass).

Rejected alternative: rebuilding a fileImages resolver inside FlyBot from cwd would need the host-internal fileMediaUrl/resolveWorkspacePath pair, duplicating the host's URL scheme and drifting from it.

The pull request carries changelog.d/2319-bubble-image-delegate.fixed.md — .fixed.md, because this is a
regression fix, and changelog.d/README.md asks for one file per user-visible change. The CI check
(node .github/scripts/changelog.mjs check) only validates fragments that exist, so the fragment has to
be written deliberately rather than left to the gate.

Acceptance

  • A test that models the real semantics: a provider stub that replaces its context and an image renderer that reads the resolver from it; the image case must be red at e40cecf6 and green after the fix.
  • The existing http(s) anchor cases still pass, and a modifier-click / file-link case proves those are left to the host.
  • core/scripts/build-client.mjs --check, npm --prefix core test and npm run verify green at the tip.

Deduplication

Out of scope

  • The window.open fallback is in scope after all: this fix keeps both noopener and noreferrer in the third argument, so the dropped-noreferrer gap named at client: a link in a chat opens the host's right panel behind FlyBot's own fixed panel, so the page looks blank until the panel is closed #2313 is closed here rather than tracked separately. What stays separate is the host-side gap (the host hands openFile/cwd but not fileImages to a plugin node view, so a local image destination inside a bubble keeps rendering as alt text until the host supplies the resolver) and extra argument-level coverage for GroupMessage/AssistantCell.
  • A toolbar entry to open a panel page in the user's own browser — separate enhancement.

Activity

  1. HugoXK commented on Nov 4, 2026

    @HugoXK
    ContributorAuthor

    pickup-ok — FlyBot implementation, claimed and being built now.

    • Claim-Id: 69feb72e65224dadb29e413fa6db88c9
    • Branch: dev/2-bubble-image-delegate
    • Owner: Dev-2
    • Tier: T1 (the diff is core/src/client/**, core/test/**, patches/ds-bot/**, docs/** and one changelog.d fragment)

    The pull request follows from this branch and carries Fixes #2319.

    Written-by: FlyBot Lead

  2. HugoXK commented on Nov 4, 2026

    @HugoXK
    ContributorAuthor

    Release

    Written-by: FlyBot Lead
    Released: 69feb72e65224dadb29e413fa6db88c9
    Reason: merged as #2322 (squash e8edff6)
    At: 2026-11-04T01:30:11Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions