Skip to content

feat(web-app-version-changelog): add version changelog sidebar panel#459

Merged
LukasHirt merged 14 commits into
mainfrom
ext/2026-06-18-version-changelog
Jun 18, 2026
Merged

feat(web-app-version-changelog): add version changelog sidebar panel#459
LukasHirt merged 14 commits into
mainfrom
ext/2026-06-18-version-changelog

Conversation

@LukasHirt

Copy link
Copy Markdown
Collaborator

Summary

Adds a new web-app-version-changelog extension that registers a Changelog sidebar panel
in the file details sidebar. When a single file is selected, the panel lists its
version history and lets users generate a plain-English description of what changed in
each version — powered by a BYO LLM endpoint.

  • Each version row shows a Generate button that fetches the two version blobs
    on-demand, diffs them, and sends the diff to a configured OpenAI-compatible endpoint
  • Generated summaries are cached for the session; a Retry button appears on failure
  • The panel is hidden for folders and shows an informational message for binary files
  • Responses are requested in the user's preferred language (BCP 47 tag passed in the
    prompt)
  • The user's auth token is forwarded to the LLM endpoint

Screenshots

image image

Configuration

Add the following to the app's entry in apps.yaml:

- id: version-changelog
  path: ...
  config:
    llm:
      endpoint: https://your-llm-host/v1
      model: your-model-name

Without configuration the panel still appears, but all Generate buttons are disabled
with a notice to configure the AI endpoint in admin settings.

Limitations

  • Text files only; binary files show a placeholder message
  • Files longer than 2,000 lines cannot be compared (an error is shown per version row)
  • The diff sent to the LLM is capped at 8,000 characters

Test plan

  • Select a versioned text file — Changelog tab appears in the sidebar
  • Click Generate on a version row — summary appears after a moment
  • Click Generate again on the same row — no second request is made (cached)
  • Simulate an LLM error — error message and Retry button appear
  • Click Retry — clears the error and re-generates
  • Select a binary file — binary placeholder is shown, no Generate buttons
  • Remove llm config — Generate buttons are disabled with the admin notice
  • Select a folder — Changelog tab does not appear

@LukasHirt LukasHirt requested a review from dj4oC June 18, 2026 15:01
@LukasHirt LukasHirt self-assigned this Jun 18, 2026
@kw-security

kw-security commented Jun 18, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@LukasHirt LukasHirt force-pushed the ext/2026-06-18-version-changelog branch from 8762ceb to 67f3883 Compare June 18, 2026 15:06
@LukasHirt LukasHirt enabled auto-merge (squash) June 18, 2026 15:14
@LukasHirt LukasHirt force-pushed the ext/2026-06-18-version-changelog branch from 67f3883 to aea0833 Compare June 18, 2026 17:50

@dj4oC dj4oC 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.

Clean, well-scoped, read-only extension (no file writes, so none of #454's data-loss risk), and the rendering is XSS-safe. The one issue I'd treat as blocking is the same token-handling pattern flagged on #454, sharper here because there's no proxy in this PR. Reviewed across six dimensions (security, rendering, diff correctness, flow, tests, infra) with each finding independently refuted; a few candidate findings were dropped as false positives (e.g. the row-0/HEAD pairing is correct by design, and bare vitest is a nit not a CI hang since it auto-detects CI).

🔴 Blocking

1. The oCIS session token is sent to the configured LLM endpoint with no validation. useChangelog.buildHeaders() attaches Authorization: Bearer <oCIS accessToken> (lines 35-42) and callLlm POSTs it to ${config.endpoint}/chat/completions (lines 74, 86). index.ts:15-17 takes endpoint verbatim from app config with only a truthiness check, with no new URL, protocol, same-origin, or allowlist guard anywhere. This PR adds no proxy (docker-compose is +1 line, the dist mount), and the README's own example shows a BYO https://.../v1 endpoint, so following the docs sends a live, replayable oCIS bearer token (plus the file's diff content) to a third party that can reuse it against the oCIS API for the token's lifetime. The dev/support configs happen to point same-origin at the existing ai-llm-proxy, but nothing in the code enforces that. Fix: constrain the endpoint to same-origin/relative (or an allowlist), or route through a token-terminating proxy and do not forward the token cross-origin.

🟠 Should-fix

  • 8000-char diff cap truncates mid-line silently (useChangelog.ts:122): a large-but-under-2000-line diff is cut at exactly 8000 chars with no marker, so the LLM summarizes a partial diff as if it were complete. Only the >2000-line case surfaces an error.
  • diff.ts is completely untested — there is no diff.spec.ts, and useChangelog.spec.ts mocks computeDiff/diffToText away. The LCS algorithm and the 2000-line/8000-char caps (the only real logic in the extension) are unverified. The algorithm itself looks correct on inspection, but nothing guards regressions.
  • Tests miss the risky paths: no unit or e2e assertion that the auth token/headers are actually sent (the e2e Generate round-trip fulfils a canned 200 without inspecting route.request().headers()); the timeout/network error branches and the version-pairing/cache-key logic are untested. Same loose-mock pattern as #454.
  • support/actions/ocis.apps.yaml keys the config web-app-ai-version-changelog but APP_ID is version-changelog (index.ts:9), so it never loads in that stack (the dev apps.yaml is correct). #454 recurrence.
  • README documents the wrong config shape (options.llm, global) vs the per-app version-changelog.config.llm block the code actually reads.
  • Blob fetches have no error handling (ChangelogPanel.vue:149-177): a failed/empty fetch returns '' and is diffed silently, so a transient error shows as "No text changes" or a whole-file-added diff rather than an error.
  • i18n not wired: defineWebApplication omits the translations field, so all $gettext strings stay English-only for non-English users (the panel is otherwise fully marked up for translation).
  • Performance: both full version blobs are pulled into memory (responseType: 'text') before the size guards run; the 2000-line/8000-char limits only apply after download.
  • Licensing: package.json and the README claim Apache-2.0 with a LICENSE badge, but no LICENSE file is added.

🟡 Nits

test:unit uses bare vitest (works in CI via auto-detection, but vitest run is the explicit/safe form). No finish_reason check, so a max_tokens: 256-truncated summary is cached as complete. Cache key falls back to the array index when a version lacks an etag (collision risk), and row-0's entry can go stale after a HEAD edit. The oldest-version row has no baseline (it diffs against the second-oldest) but is presented as a normal diff. The BCP-47 language tag is interpolated raw into the prompt. No client-side rate limiting beyond the per-key cache. Binary detection treats unknown and some text-ish MIME types (js/yaml) as binary. "Generating…" is not announced to assistive tech (no aria-live/role="status"). Dates use toLocaleString() with the runtime locale rather than the oCIS UI language. ts-node is an unused devDependency.

🟢 Done well

  • XSS-safe throughout: the LLM summary, the diff, and all error/metadata strings render via {{ }} text interpolation; no v-html/innerHTML/markdown/sanitizer anywhere, and the (attacker-influenceable) diff is never put in the DOM.
  • Correct concurrency: double-click is de-duped by a synchronous guard before the first await; switching files mid-request cannot corrupt another file's entry (keyed by fileId+etag, captured at click time); Retry clears the error cleanly.
  • LCS diff is algorithmically correct, and the line guard bounds memory before allocation.
  • No hardcoded secrets (token read live, never persisted); max_tokens hardcoded and model from trusted config; isVisible correctly hides folders and multi-select; vue-tsc present; package added to the CI matrix.

Bottom line: the only thing I'd block on is not forwarding the oCIS token to an unvalidated/cross-origin endpoint. Everything else is routine.

Added a new sidebar panel extension that generates human-readable changelog
entries for each file version. When a user selects a file with version history,
the "Changelog" panel appears alongside the standard file history. Each version
row has a **Generate** button that diffs the two version blobs on-demand and
sends the diff to a configured BYO-LLM endpoint to produce a concise description
of what changed.

Signed-off-by: Lukas Hirt <info@hirt.cz>
@LukasHirt LukasHirt force-pushed the ext/2026-06-18-version-changelog branch from aea0833 to e1aa4d4 Compare June 18, 2026 18:31
LukasHirt and others added 12 commits June 18, 2026 21:13
…prevent token exfiltration

Validates the configured endpoint against window.location.origin at startup.
A cross-origin URL disables changelog generation and logs an error rather
than forwarding the oCIS session token to a third-party server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… the input is partial

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ithm and caps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…error, and cache key paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…with APP_ID

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…per-app applicationConfig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e instead of silently returning empty string

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…are localised for non-English users

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ing LCS to avoid O(m*n) allocation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…k finish_reason, announce generating state to AT

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…yaml key to web-app-version-changelog

The GHA workflow mounts the dist dir as /apps/$matrix.app where matrix.app
is the package directory name (web-app-version-changelog). The ocis.apps.yaml
key must match that path, not APP_ID. Previous change was incorrect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@LukasHirt

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review. Here's what was addressed:

🔴 Blocking — fixed

  • Cross-origin endpoint / token exfiltration: index.ts now parses the configured endpoint with new URL() and rejects
    it (disables changelog generation + logs an error) if the origin doesn't match window.location.origin.

🟠 Should-fix — fixed

  • Silent 8000-char truncation: the sliced diff now ends with \n...[diff truncated] so the LLM knows the input is
    partial.
  • diff.ts untested: added tests/unit/diff.spec.ts with 13 tests covering the LCS algorithm, the 2000-line guard, empty
    inputs, hunk splitting, and diffToText formatting.
  • Tests missing risky paths: added tests asserting the Authorization header is sent (and omitted when no token), the
    timeout DOMException branch, the TypeError (network failure) branch, and independent cache keys.
  • support/actions/ocis.apps.yaml key: the key is web-app-version-changelog intentionally — the GHA workflow mounts the
    dist dir as /apps/$matrix.app where $matrix.app is the package directory name, so the config key must match the mount
    path, not APP_ID. No change made here.
  • README config shape: corrected to the per-app version-changelog: config: llm: block; added a note about the
    same-origin requirement.
  • Blob fetch silent '' returns: guard-clause paths in fetchVersionContent / fetchHeadContent now throw descriptive
    errors instead of returning empty string, so failures surface in the existing error-state UI.
  • i18n not wired: translations.json is now imported and passed to defineWebApplication.
  • Performance: added a MAX_CONTENT_CHARS = 500_000 check after download but before the LCS call to avoid O(m×n)
    allocation on very large blobs.
  • LICENSE file: added Apache-2.0 LICENSE to the package.

🟡 Nits — fixed

  • test:unit now uses vitest run.
  • ts-node removed from devDependencies.
  • Added finish_reason === 'length' check — a max_tokens-truncated response is not cached as a complete summary.
  • "Generating…" span now has role="status" aria-live="polite".

@LukasHirt LukasHirt requested a review from dj4oC June 18, 2026 19:26
Signed-off-by: Lukas Hirt <info@hirt.cz>

@dj4oC dj4oC 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.

LGTM

@LukasHirt LukasHirt merged commit 98cf4bf into main Jun 18, 2026
18 of 21 checks passed
@LukasHirt LukasHirt deleted the ext/2026-06-18-version-changelog branch June 18, 2026 19:41
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.

3 participants