Skip to content

Teach a table-safe image size syntax, and only to agents that can use it - #323

Merged
frenchie4111 merged 1 commit into
mainfrom
fix/chat-image-size-in-tables
Oct 10, 2026
Merged

frenchie4111 merged 1 commit into
mainfrom
fix/chat-image-size-in-tables

Conversation

@frenchie4111

Copy link
Copy Markdown
Collaborator

Follow-up to #320. Two problems with how the screenshot-embed behaviour was prompted.

The documented size syntax broke inside tables

A pipe in a GFM table row is a column delimiter, so ![cap|300](path) gets split into ![cap and 300](path) before any inline parsing happens — no image node is produced at all, and the cells render as broken literal text:

Before After
Sage sheet, keyboard up ![ 300](/tmp/kbshots/before-sheet.png)

The pipe form was the one form we taught, so this fired precisely when an agent did what it was told.

Every prompt surface now teaches the #w= fragment instead — ![cap](path#w=500), #w=50%, #w=400x300. It's part of the URL, so it survives table cells, lists and blockquotes untouched. The pipe and =title forms still work for agents that guess them; the pipe is now mentioned only as the thing to avoid.

Fragments also needed percent-decoding: markdown encodes the URL on the way through, so an agent's #w=50% arrives as #w=50%25.

The tool description over-promised

The MCP bridge is shared between Chat and Terminal tabs, but inline rendering only exists in Chat (json-mode). A terminal-tab agent was told "Ness renders the screenshot inline in the chat" and would dutifully emit markdown into an xterm, where it's literal text.

CallerScope gains rendersMarkdown (true for json-claude tabs), /scope carries it, and the bridge strips the embed sentence from the tool description and the hint from the per-result line when it's false — mirroring how stripForkAffordance already works for create_worktree. The path itself is still reported either way, since that's useful regardless. Defaults permissive when /scope can't answer, matching the existing browser gates.

Verification

  • src/renderer/chat-image-markdown.test.ts (new) pins the actual bug: renders each syntax through ReactMarkdown + remark-gfm inside a table cell and asserts the pipe form yields no image while the fragment, =title, and escaped-pipe forms all resolve to the right size. Follows the existing remark-insight.test.ts pattern — no new deps.
  • /scope test asserts rendersMarkdown reaches the caller, and that a missing terminal id still 400s rather than guessing.
  • Two new cases for percent-decoded fragments.
  • npm run typecheck, npx electron-vite build, node -c resources/mcp-bridge.js, and all 1756 tests pass.

One note for whoever merges: the system-prompt change only reaches users with no persisted harnessSystemPrompt. Anyone who has edited their prompt in Settings keeps their copy, pipe syntax and all.

🤖 Generated with Claude Code

Two problems with how the screenshot-embed behaviour was prompted.

The documented size syntax broke inside tables. A pipe in a GFM table
row is a column delimiter, so `![cap|300](path)` is split into `![cap`
and `300](path)` before any inline parsing happens — no image node is
produced at all and the cells render as broken literal text. The pipe
form was the one form we taught, so this fired exactly when an agent
did what it was told.

Every prompt surface now teaches the `#w=` fragment instead:
`![cap](path#w=500)`, `#w=50%`, `#w=400x300`. It's part of the URL, so
it survives table cells, lists and blockquotes untouched. The pipe and
`=title` forms still work for agents that guess them; the pipe is now
documented only as the thing to avoid. Fragments also needed
percent-decoding — markdown encodes the URL on the way through, so an
agent's `#w=50%` arrives as `#w=50%25`.

The tool description also over-promised. The MCP bridge is shared
between Chat and Terminal tabs, but inline rendering only exists in
Chat; a terminal-tab agent was told "Ness renders the screenshot inline
in the chat" and would emit markdown into an xterm, where it's literal
text. CallerScope gains rendersMarkdown (true for 'json-claude' tabs),
/scope carries it, and the bridge strips the embed sentence from the
tool description and the hint from the result line when it's false —
mirroring how stripForkAffordance already works. The path itself is
still reported either way. Defaults permissive when /scope can't say.

src/renderer/chat-image-markdown.test.ts pins the actual failure: it
renders each syntax through ReactMarkdown + remark-gfm inside a table
cell and asserts the pipe form yields no image while the fragment does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@frenchie4111
frenchie4111 merged commit 4e564c7 into main Oct 10, 2026
1 check passed
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.

1 participant