Image rendering in diffs - #15412
Conversation
|
This pull request changes Lite's UI. Before/after screenshots make it To attach them, ask your agent for the Remove the |
There was a problem hiding this comment.
Pull request overview
Adds a Lite UI workaround to render image previews in diffs by introducing new backend endpoints to fetch file contents from either the workspace or a blob, then wiring those into the diff viewer via synthetic items/annotations.
Changes:
- Expose
getBlobFileandgetWorkspaceFilethroughbut-api(NAPI) and the generatedbut-sdk(JS + typings + param-name metadata). - Add an
ImageDiffReact component in Lite UI that renders “before/after” images from workspace/blob sources. - Extend Lite diff-view item construction to inject image-rendering annotations / synthetic file items for raster binary diffs (and SVG patches).
Reviewed changes
Copilot reviewed 6 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/but-sdk/src/generated/linear/index.js | Exports new SDK endpoints from the native binding. |
| packages/but-sdk/src/generated/linear/index.d.ts | Adds typings for new endpoints and the shared FileInfo type. |
| packages/but-sdk/src/generated/linear/cacheTags.js | Declares cache-tag classification for new endpoints. |
| packages/but-sdk/src/generated/linear/cacheTags.d.ts | Typings for new cache-tag entries. |
| packages/but-sdk/src/generated/linear/apiParamNames.js | Adds param-name metadata for IPC payload shaping. |
| packages/but-sdk/src/generated/linear/apiParamNames.d.ts | Typings for new param-name entries. |
| packages/but-sdk/src/generated/graph/index.js | Exports new SDK endpoints from the native binding. |
| packages/but-sdk/src/generated/graph/index.d.ts | Adds typings for new endpoints and the shared FileInfo type. |
| packages/but-sdk/src/generated/graph/cacheTags.js | Declares cache-tag classification for new endpoints. |
| packages/but-sdk/src/generated/graph/cacheTags.d.ts | Typings for new cache-tag entries. |
| packages/but-sdk/src/generated/graph/apiParamNames.js | Adds param-name metadata for IPC payload shaping. |
| packages/but-sdk/src/generated/graph/apiParamNames.d.ts | Typings for new param-name entries. |
| crates/but-api/src/legacy/repo.rs | Implements get_workspace_file and new get_blob_file endpoints (plus JSON FileInfo schema type). |
| apps/lite/ui/src/routes/project/$id/workspace/ImageDiff.tsx | New UI component that queries file contents and renders before/after image panels. |
| apps/lite/ui/src/routes/project/$id/workspace/ImageDiff.module.css | Styling for the new image diff panels. |
| apps/lite/ui/src/routes/project/$id/workspace/diff-view.ts | Injects synthetic items/annotations to trigger image rendering in the diff viewer. |
| apps/lite/ui/src/routes/project/$id/workspace/Details.tsx | Renders ImageDiff for image annotations and adjusts item handling for file items. |
| apps/lite/ui/src/file.ts | Adds helpers to detect raster images / SVG by extension. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
f96414f to
88236e3
Compare
88236e3 to
367c5f3
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 18 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
apps/lite/ui/src/routes/project/$id/workspace/Details.tsx:1428
- Typo/grammar in the injected CSS comment: “empty diff that causes to render” is missing an “it” (reads awkwardly). Consider changing to “empty diff that causes it to render” to keep comments clear.
We leverage annotations on synthetic empty diffs as a workaround. Here we hide the
empty diff that causes to render. */
pre[data-file] {
367c5f3 to
11fefa0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 19 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
crates/but-api/src/legacy/repo.rs:96
get_workspace_fileis now exported via#[but_api(napi, ...)], which makes arbitrary worktree reads available to the renderer/Node caller.Context::read_file_from_workspaceenforces “inside worktree” but does not consult.gitignore/tracked status, so gitignored/untracked secrets (e.g..env, tool metadata under.jj/) remain readable. Consider restricting this API to paths that are tracked or part of the diff/status surface, or explicitly rejecting gitignored paths by applying gitignore matching beforefs::read.
#[but_api(napi, json::FileInfo, provides = [])]
#[instrument(err(Debug))]
pub fn get_workspace_file(ctx: &Context, relative_path: String) -> Result<FileInfo> {
ctx.read_file_from_workspace(relative_path.as_ref())
}
|
oooh nice! |
11fefa0 to
6353e7c
Compare
|
I intend to merge these PRs on the following basis:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 19 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
crates/but-api/src/legacy/repo.rs:96
get_workspace_fileis now exported over N-API to the renderer, but it directly callsContext::read_file_from_workspace()with an arbitrary path. That repo helper validates traversal/symlinks but does not restrict access to tracked/non-ignored files, so the renderer can read any file inside the worktree (e.g..env,.git/config,.jj/**). Consider enforcing an allowlist (tracked in index/HEAD) or at minimum applying gitignore/exclude rules and blocking VCS metadata directories before exposing this API.
#[but_api(napi, json::FileInfo, provides = [])]
#[instrument(err(Debug))]
pub fn get_workspace_file(ctx: &Context, relative_path: String) -> Result<FileInfo> {
ctx.read_file_from_workspace(relative_path.as_ref())
}
crates/but-api/src/legacy/repo.rs:23
- The docstring says file content is base64 encoded when
mime_typeis present, but this JSON type isrename_all = "camelCase", so the field exposed to SDK consumers ismimeType. Updating the docs to refer tomimeTypewould prevent incorrect usage in generated TypeScript docs.
/// File contents and metadata suitable for display in a frontend.
#[derive(Debug, Serialize)]
#[cfg_attr(feature = "export-schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct FileInfo {
/// File content, base64 encoded when `mime_type` is present.
pub content: Option<String>,
/// The basename derived from the relative path.
pub file_name: String,
/// The decoded content size in bytes.
pub size: Option<usize>,
/// The inferred MIME type for binary displayable content.
pub mime_type: Option<String>,
apps/lite/ui/src/routes/project/$id/workspace/Details.tsx:1428
- Fix grammar in the injected CSS comment: “empty diff that causes to render” → “empty diff that causes it to render”.
/* Pierre doesn't support image diffs yet:
https://github.com/pierrecomputer/pierre/issues/258
We leverage annotations on synthetic empty diffs as a workaround. Here we hide the
empty diff that causes to render. */
6353e7c to
2fe6eea
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (1)
crates/but-api/src/legacy/repo.rs:96
get_workspace_fileexposesread_file_from_workspacedirectly to NAPI/SDK without any filtering beyond “path stays inside worktree”.read_file_from_workspacewill happily read files like.env,.git/config,.jj/**, etc. if they exist inside the worktree, which makes it possible for any renderer-side code path (including XSS) to exfiltrate local secrets.
Consider enforcing an allowlist (e.g. only paths that are tracked or at least not ignored) and/or explicitly denying sensitive internal directories (e.g. .git, .jj) at this API boundary, instead of relying solely on traversal/symlink checks.
#[but_api(napi, json::FileInfo, provides = [])]
#[instrument(err(Debug))]
pub fn get_workspace_file(ctx: &Context, relative_path: String) -> Result<FileInfo> {
ctx.read_file_from_workspace(relative_path.as_ref())
}
2fe6eea to
1f50018
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (1)
crates/but-api/src/legacy/repo.rs:96
get_workspace_fileis now exposed via the NAPI surface and reads arbitrary paths from the worktree viaread_file_from_workspace, which does not check Git visibility/ignore status. That makes gitignored or untracked secrets (e.g..env,.jj/**) readable by renderer JS. Consider enforcing an allowlist (tracked in index/HEAD, or part of the current diff) and/or denying gitignored paths and internal dirs like.git/.jj, and add a regression test for the policy.
#[but_api(napi, json::FileInfo, provides = [])]
#[instrument(err(Debug))]
pub fn get_workspace_file(ctx: &Context, relative_path: String) -> Result<FileInfo> {
ctx.read_file_from_workspace(relative_path.as_ref())
}
Resolves a security vulnerability in which this function could be used to read files in the workspace that are gitignored, such as .env. This was particularly concerning should one of our web renderers become compromised as they have unfiltered access to this function. The added tests specifying rejection are red without the fix. The fix has also been validated with a real repro in Lite, as specified in gitbutlerapp#15412: > Error invoking remote method 'getWorkspaceFile': Error: Refusing to read Git-ignored path '.jj/repo/config-id' I've also validated that it can't read :/.codex/config.toml, where I have .codex ignored in ~/.config/git/ignore. Performance summary from my agent: Previously, reading an existing workspace file was filesystem-only: approximately O(B), where B is the file size. After adding Git visibility validation: - Exact tracked files take O(log N + B) using the shared index, where N is the number of index entries. - Untracked files take O(N + B) because gitoxide constructs exclusion state to evaluate ignore rules. - The additional O(N) case-insensitive index accelerator is only built when a path is excluded, misses the exact index lookup, and may be a differently-cased tracked file. The implementation therefore keeps the common tracked-file path cheap while confining repository-wide work to paths that require ignore classification. The first index load may itself be O(N), but subsequent reads reuse the parsed index.
Closes GB-1824.
We need to discuss the security implications of
getWorkspaceFile, because secrets may be present (e.g..env) but uncommitted and the API makes these available to the renderer. I suspect Tauri already has this problem.This SDK function is used in this PR to get the blob of uncommitted images. Lite will also need this function for uncommitted additions in partial diff hydration (i.e. load whole file on demand).
We could potentially solve this by hiding files not visible to Git from this function. That limits the surface of attack to uncommitted secrets that haven't been gitignored yet.
Exploit example: