fix: render images referenced from the local filesystem#165
Conversation
|
I've gotta run to my lunch meeting w/ Ron so didn't have time to rebase the commits. Feel free to look now, or wait until I'm back to have an agent rebase |
a82cdc7 to
b73e73b
Compare
0b36af4 to
8cd622f
Compare
|
|
||
| const components = { | ||
| ...markdownComponents, | ||
| img: ({ src, alt, ...rest }: ComponentPropsWithoutRef<'img'>) => { |
There was a problem hiding this comment.
Snags images defined in our assets metadata and serves them from the purpose-built CLI endpoint.
| setTimeout(() => resolveResult(parsed.data), 0); | ||
| return new Response(null, { status: 204, headers: { connection: 'close' } }); | ||
| } | ||
| if (req.method === 'GET' && url.pathname.startsWith('/assets/')) { |
There was a problem hiding this comment.
This method is getting gnarly AF - see #170 for a refactor
Represent annotation assets as JSON-safe base64 payload records and add shared test factory coverage.
Serve only payload-provided assets by opaque ID and decode their base64 content for the local browser UI.
Thread payload assets into markdown rendering and rewrite matching local image references to the server asset route.
8cd622f to
cf16ccf
Compare
Extract only local allowlisted images, support file URLs and path-relative refs, encode bytes as base64, and enforce per-asset/total size limits before starting the annotation server.
cf16ccf to
2460134
Compare
jcarver989
left a comment
There was a problem hiding this comment.
Left some thoughts on cleaning up the big function you mentioned, but nothing blocking.
| import { visit } from 'unist-util-visit'; | ||
| import type { CliContext } from '#src/context.ts'; | ||
|
|
||
| export interface ExtractAssetsInput { |
There was a problem hiding this comment.
Ah yea I see what you were talking about know in our earlier discussion. Some observations/thoughts here:
1.
I'm not sure if you'd find this nicer, but the current code is doing a lot of check x and log y + return null for things that are essentially errors (or perhaps more accurately rejections), so you might consider creating a type like:
export type AssetExtractionIssue = IssueA | IssueB | IssueC...That would allow several of these functions to return a Result and you could log only at the top level.
2.
You might consider a AssetFilter type that looks something like: `type AssetFilter = (candidate) => Result<void, AssetExtractionIssue>
That'd allow you to compose different filter rules together more easily,
Replaces null/false rejection signals across extractAssets with an AssetExtractionIssue discriminated union returned via Result. Logging moves out of the individual checks into a single logIssue switch at the collection/load boundary, and the per-step checks are typed as AssetFilter<T> so future filters compose the same way. Addresses Josh's PR feedback on #165.
|
|
||
| const MIB = 1024 * 1024; | ||
| export const MAX_ASSET_BYTES = 10 * MIB; | ||
| export const MAX_TOTAL_ASSET_BYTES = 25 * MIB; |
There was a problem hiding this comment.
I don't expect this large of images, but I figured I'd have some safety mechanism since we base64 encode the data.
| import { visit } from 'unist-util-visit'; | ||
| import type { CliContext } from '#src/context.ts'; | ||
|
|
||
| export interface ExtractAssetsInput { |
🤖 I have created a release *beep* *boop* --- ## [0.8.0](v0.7.2...v0.8.0) (2026-05-20) ### Features * add planbridge-last skill to open the last agent message ([#189](#189)) ([dbfc615](dbfc615)) ### Bug Fixes * prevent data loss with onbeforeunload warning ([#188](#188)) ([ea1f5d5](ea1f5d5)) * render images referenced from the local filesystem ([#165](#165)) ([bdf98ff](bdf98ff)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: contextbridge-pr-automation[bot] <259134118+contextbridge-pr-automation[bot]@users.noreply.github.com>
Summary
Plan markdown containing local image references rendered as broken images because the browser tried to resolve filesystem paths against the localhost annotation server. This PR extracts local, allowlisted image files at annotation startup, embeds their bytes as JSON-safe base64 assets on
AnnotationPayload, and serves those embedded assets through/assets/:idfor the current browser UI. The payload is now portable enough for future shareable plan links while the markdown content remains byte-identical end-to-end. Closes #148.Review focus
Please scrutinize the asset contract and security boundary: image bytes now live in the payload as
dataBase64plusmimeType, and the server only serves assets by opaque ID from the in-memory payload map. There are no request-time filesystem reads and no network/data-URL fetching during extraction.The extraction limits are also deliberate: local images are capped at 10 MiB each and 25 MiB total, SVG remains excluded, and unsupported/empty/oversized images are skipped with logs instead of failing the review session. That keeps accidental huge/private payloads bounded, but it also means a skipped local image can still render as the original unresolved markdown path.
Commits
Stack is ordered by layer for commit-by-commit review: shared payload contract → server serving → browser rendering → CLI extraction/inlining.
2038ab1— feat(shared): add portable annotation assetsd80053a— feat(server): serve embedded annotation assets6069173— feat(annotation): render payload image assetscf16ccf— fix(cli): inline local markdown images as assets