- Narrow frontmatter descriptions and add an activation gate so ordinary
code-review requests do not launch difit (#457)
- Document the one-live-server-per-git-root invariant with --keep-alive,
--background, and difit comment add/get/resolve for reuse (#456)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
prism-react-renderer strips the trailing newline from each tokenized
line, and PrismSyntaxHighlighter rendered lines as inline spans, so
multi-line code blocks in comment bodies collapsed into a single line
(#460). Re-insert the newline between lines in the shared component
and drop the per-call-site workarounds in NotebookDiffViewer and
global.css that papered over the same defect.
Fixes#460
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
When `origin/HEAD` is unset, `getOriginDefaultBranch` falls back to probing
`refs/remotes/origin/{main,master}` with `git show-ref --verify --quiet`. That
probe never fails. simple-git decides success partly from stderr, and `--quiet`
tells git to stay silent, so a missing ref resolves instead of rejecting.
Every local-only repository therefore reports `originDefaultBranch:
"origin/main"` from `GET /api/revisions`, and the quick menu offers an
`origin/main...Uncommitted (merge-base)` preset that cannot resolve.
Dropping `--quiet` lets git write `fatal: ... - not a valid ref` to stderr,
which simple-git surfaces as a rejection, so the fallback behaves as intended.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: --background parent actually exits and detached server stays alive
Use IPC for the parent/child handshake instead of stdout pipes so the
parent can exit without SIGPIPE killing the detached child. Disconnect
IPC after the handshake and ignore stdio errors on the background child.
* Address PR comments
When a diff contains a path as both a standalone file (e.g. a deleted
symlink) and a directory prefix (e.g. new files under the same name),
buildFileTree created the node as a leaf on first encounter and never
promoted it to a directory. Children were silently hidden in the tree.
Fixes#452
Two follow-ups to the frontmatter preview (#442):
- Strip frontmatter lines from the diff preview blocks only when the
first chunk actually starts at the top of the file. Previously a body
`---` horizontal rule at the start of a mid-file chunk matched the
frontmatter delimiter and was silently removed from the preview.
- Render the frontmatter diff table only when at least one key actually
changed. Body-only edits no longer show a redundant Before/After table
with identical values.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: add frontmatter samples (#441)
Add before/after markdown pairs covering scalar, array, single-level
nested, and deep-nested YAML frontmatter changes for the frontmatter
preview feature.
* feat(client): add extractFrontmatter helper and YAML deps (#441)
Add js-yaml as a runtime dependency for the frontmatter preview
feature, plus a standalone extractFrontmatter helper that
regex-splits a leading YAML block and safely parses it via the
safe `load()` default.
* feat(client): add FrontmatterTable snapshot component (#441)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* feat(client): render frontmatter in MarkdownFullPreview (#441)
Wire extractFrontmatter and FrontmatterTable into MarkdownFullPreview
so the Full Preview tab renders a two-column key/value snapshot table
above the body for markdown files with YAML frontmatter. Files without
frontmatter continue to render unchanged, preserving existing behavior.
* refactor(client): fetch both base and target markdown blobs (#441)
Restructures MarkdownDiffViewer's preview fetch logic to retrieve both
base and target blobs in parallel via Promise.allSettled, laying the
groundwork for diff-preview frontmatter rendering while keeping
MarkdownFullPreview and MarkdownDiffPreview behavior unchanged.
* feat(client): add computeFrontmatterDiff pure helper (#441)
Adds computeFrontmatterDiff(oldData, newData) which returns per-key
FrontmatterDiffEntry records with status of added / removed /
modified / unchanged. Modified detection uses a canonical stringify
that recursively sorts object keys before JSON.stringify, so
frontmatter key reorderings do not produce false positives. Arrays
preserve their original order (order changes are treated as
modifications), and Date values are normalized via toISOString for
comparison so equal timestamps parsed by js-yaml stay unchanged.
Entry order is new-first (newData Object.keys order) with old-only
keys appended, matching the layout the FrontmatterTable diff mode
will render in the next task.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(client): add FrontmatterTable diff mode (#441)
Extend FrontmatterTable with a `mode: 'diff'` variant that consumes
FrontmatterDiffEntry[] from computeFrontmatterDiff and renders per-key
rows in a Key / Before / After 3-column layout. Cell-level coloring
uses the existing diff Tailwind classes: `bg-diff-deletion-bg` on the
Before cell for removed/modified, `bg-diff-addition-bg` on the After
cell for added/modified. added/removed leave the opposite cell empty
so the horizontal comparison stays aligned. Nested values use one
level of recursive indent with a JSON.stringify fallback for deeper
structures.
Prepares the component for MarkdownDiffPreview integration in the
next task.
* feat(client): integrate frontmatter diff into MarkdownDiffPreview (#441)
Wires the already-fetched base/target markdown blobs and computeFrontmatterDiff
into MarkdownDiffPreview so the Diff Preview tab shows a frontmatter diff table
(added/removed/modified rows) for modified/renamed files, all-added rows for
new files, all-removed rows for deleted files, and a labeled snapshot fallback
when only one side is available (partial fetch failure).
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* chore(deps): pin js-yaml to v4 and drop unused remark-frontmatter (#441)
js-yaml v5 duplicated the dependency tree since every transitive
consumer (eslint, secretlint, etc.) still resolves to v4. Pin to the
v4 series so pnpm dedupes to a single version; the v4 `load()` API
used by extractFrontmatter is unchanged, so the safety rationale from
#441 still applies.
remark-frontmatter was added to dependencies but never imported —
frontmatter parsing goes through js-yaml directly. Drop it along with
its knip ignoreDependencies entry now that it's gone from
package.json.
Co-authored-by: yoshiko-pg <yoshiko-pg@users.noreply.github.com>
* refactor(client): narrow knip config to specific unused export (#441)
ignoreExportsUsedInFile:true suppressed this class of warning
repo-wide, with no other precedent for that setting in this project.
Remove it and un-export the handful of file-local types/functions it
was masking (FrontmatterTable's renderValue/*Props types,
frontmatterDiff's FrontmatterDiffStatus) so knip can catch this
category of unused export everywhere else too.
Co-authored-by: yoshiko-pg <yoshiko-pg@users.noreply.github.com>
* fix(client): restore Full Preview tab for deleted markdown files (#441)
Fetching base and target blobs separately (for the frontmatter diff)
left hasFullPreview and the Full Preview pane hard-wired to
contents.target, which is intentionally never populated for a deleted
file. As a result, deleted markdown files silently lost the Full
Preview tab that main provides via a single status-aware source.
Derive the relevant content (base for deleted files, target
otherwise) once and use it everywhere hasFullPreview and the preview
pane are computed, plus the effect's refetch condition, restoring the
pre-refactor behavior. Add a regression test asserting the tab
appears and base content renders for a deleted file.
Co-authored-by: yoshiko-pg <yoshiko-pg@users.noreply.github.com>
* feat(client): show clear label when partial preview fetch fails (#441)
#441's acceptance criteria call for falling back to snapshot mode
"with a clear label" when a blob fetch fails. Previously that label
only appeared inside FrontmatterTable, so files without frontmatter
gave no indication that half of the diff was missing.
Track whether exactly one side (base or target) failed to fetch and
surface a plain-text banner in both Diff Preview and Full Preview,
independent of whether frontmatter is present.
Co-authored-by: yoshiko-pg <yoshiko-pg@users.noreply.github.com>
* fix(client): strip frontmatter lines from markdown diff preview
Address review feedback on PR #442: in the diff-preview tab the
frontmatter table was rendered at the top while the raw frontmatter
lines from the diff chunks were still rendered as markdown below it,
turning the `---` delimiters into stray <hr> elements.
When both sides needed for the diff are available, compute the
frontmatter line span for each side and exclude those leading lines
from the preview blocks. Matching is content-verified so a diff that
starts below the frontmatter (leading lines are body content) is left
untouched. Snapshot fallback mode keeps its current behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014svkmsEcukbfT4pAFZJMvL
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: yoshiko-pg <yoshiko-pg@users.noreply.github.com>
* feat: add Groovy language support (.groovy, .gvy, .gy, .gsh, .gradle)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs: mention Groovy in supported languages list
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor: reorder groovy/svelte entries and drop svelte loader comment
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: restore svelte loader comment removed by mistake
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs: move Groovy next to Java in backend languages list
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
A single request with an invalid ref (e.g. /api/diff?target=deleted-branch)
permanently broke the server: currentSelection was overwritten before
parseDiff succeeded, so every subsequent parameter-less request reused the
bad ref and returned 500 until restart. The handler also had no try/catch,
so failures surfaced as Express's default HTML stack trace.
Move the currentSelection update after the diff is successfully resolved
and return a JSON 500 error on parse failure, matching the error handling
pattern of the other API endpoints.
Add a regression test covering the JSON 500 response and the recovery of
subsequent requests, replacing the previously skipped placeholder test.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add a chevron toggle to the comment thread header that collapses the
whole discussion into a compact one-liner showing the file location,
the first line of the root comment, and the message count, similar to
GitHub. Clicking the summary line (or the chevron) expands it again.
Implements suggestion 4 in #432.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
localStorage is origin-scoped (port included), so appearance settings,
diff view mode, and sidebar state reset whenever difit starts on a new
port. Persist them server-side instead (#426):
- New user-config module reads/writes ~/.difit/config.json (override dir
via DIFIT_CONFIG_DIR) with atomic tmp+rename writes, a versioned
schema, and a 64KB size guard.
- New GET/PUT /api/user-settings endpoints; PUT shallow-merges the patch
per top-level key (last-write-wins across concurrent servers).
- The client hydrates from the server config on boot and falls back to
localStorage when no API server exists (static demo site). Changes are
debounced into a single PUT; localStorage stays as the sync cache.
- If the server has no stored value yet but localStorage does, the local
value seeds the config once, migrating existing users.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(client): add a search box to the revision selector
Add a sticky filter input at the top of the branch/commit dropdown so
revisions can be narrowed down by typing, similar to GitHub's branch
picker. Matching is case-insensitive across special option labels,
commit hashes and messages, and branch names. Pressing Enter selects
the first enabled match, and an empty state is shown when nothing
matches.
Also cap the dropdown height with the floating-ui size middleware so
the list (and the new search box) always stays inside the viewport
instead of overflowing off-screen.
Implements suggestion 2 in #432.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(client): add a search box to the revision quick menu
Move the primary search experience into the DiffQuickMenu dropdown: a
sticky filter input now sits above the Quick Diffs section. Typing
filters across quick diff presets, recent commits (select for
commit^...commit), and branches (select for
branch...Uncommitted (merge-base)). Enter picks the first match and an
empty state is shown when nothing matches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Replace the header Reply toggle button with a GitHub-style reply trigger
that is always shown below the last message in a thread. Clicking or
focusing the trigger expands it into the full reply form, and it
collapses back after submitting or cancelling.
Closes the first suggestion in #432.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add a line-selection anchor in unified and side-by-side diff chunks so a plain click records the start line and a Shift-click on the same side opens a range comment.
Keep the existing comment-button drag behavior intact, and skip empty-form cleanup for Shift-clicks that open a range comment form.
Cover the new Shift-click range path in unified and side-by-side diff views, including the fallback to a single-line comment when no anchor has been set.
Related to #425.
* feat(client): render comment bodies as markdown
Comment bodies were rendered as plain text, so markdown like **bold**,
inline code, and fenced code blocks showed up as literal characters.
Render text parts of comment bodies with react-markdown + GFM instead:
- Fenced code blocks get Prism syntax highlighting, and ```diff blocks
are rendered with diff addition/deletion line backgrounds
- Single newlines are kept as line breaks (remark-breaks), matching how
comment text used to wrap
- Raw HTML is not rendered and unsafe link/image URLs are dropped
- Existing ```suggestion block parsing and rendering is unchanged
Closes second request in #426.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(client): address review feedback on comment markdown rendering
- Preserve consecutive spaces/tabs inside paragraphs and list items with
whitespace-pre-wrap, keeping alignment-formatted comments readable
- Deduplicate isSafeUrl / extractText / isElementWithCodeProps in
NotebookDiffViewer by importing the shared markdownUtils helpers
- Hoist remark plugin array and urlTransform to module-level constants so
ReactMarkdown props stay referentially stable across renders
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(client): replace resolve/delete browser confirm() with inline confirmation UI
Clicking resolve (or delete on a reply) now switches the action buttons
to a compact inline confirm/cancel pair inside the card instead of
opening a blocking browser confirm() dialog. Escape or clicking outside
cancels. The confirmRootAction prop keeps its semantics: when false the
root action still runs immediately with no confirmation.
Closes item 4 of #426.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(client): consume Escape in inline confirmation and extract useClickOutside hook
Register the Escape handler in the capture phase and call
preventDefault/stopPropagation so cancelling a confirmation inside
CommentsListModal no longer also closes the modal via its escape hotkey.
Extract the shared document-mousedown outside-click detection into a
useClickOutside hook and reuse it in CommentsDropdown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(cli): add comment resolve verb to remove threads via CLI
Adds `difit comment resolve <threadIds...>` (alias: `remove`) backed by a
new `DELETE /api/comments/:threadId` endpoint, so agent review loops can
resolve or remove comment threads without re-posting the full thread set
to the internal comments API (#426).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(cli): collect resolve errors per thread and dedupe connection error handling
Review feedback on #428:
- Server errors during resolve no longer abort the loop; each thread ID
now reports into an errors array in the summary JSON so agents can
detect partial failures, with exit code 1 on any notFound or error.
- DELETE requests run in parallel via Promise.all.
- Extract the shared fetch connection-error handling from add/get/resolve
into handleCommandError.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Rebuild the VS Code extension from scratch around a bundled difit server:
- Bundle the server straight from the monorepo source with esbuild and run
it via child_process.fork on the extension host's own Node runtime, so
launching no longer depends on the user's shell or PATH (removes the
login-shell fallback and install-prompt flow entirely)
- Report the server URL over the fork IPC channel instead of scraping stdout
- Ship prebuilt @parcel/watcher binaries for all VS Code desktop platforms
inside the VSIX; load the watcher lazily in FileWatcherService so an
unsupported platform degrades to no live reload instead of failing startup
- Serve the prebuilt client assets from dist/client inside the extension
- Keep difit.executablePath as an opt-in escape hatch for running an
external difit CLI
- Use node:readline/promises so the CLI utils resolve under esbuild
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat: add ability to mark an entire folder as reviewed from the file tree (#419)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: reveal folder review checkbox on hover in place of the folder icon
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: nudge folder review checkbox 2px right to align with file checkboxes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(client): improve keyboard file navigation (#378)
- Highlight the focused file's header so the current file is visible
even when its diff is collapsed
- Remember the last cursor position so [ / ] resume from it after a
mouse click clears the cursor, instead of restarting from the top
- Add Shift+V to mark the current file as viewed and jump to the next
unviewed file
- Move keyboard focus to the next unviewed file after clicking the
Viewed button
- Make v toggle the hovered file when there is no keyboard cursor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style(client): use green for focused file header indicator
Yellow clashed with the theme; use a brighter green than the accent
top border so the focused header still stands out.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style(client): add gap between Shift+V keys and wrapped description in help modal
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(client): don't show keyboard cursor when clicking Viewed button
Clicking Viewed now only updates the remembered navigation position, so
keyboard navigation resumes from that file but mouse-only interaction
never surfaces keyboard UI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Move the range-selection commit logic from the comment button's own
mouseup handler to the document-level mouseup listener, so releasing
the mouse outside the button still opens the comment form. Applies to
both unified and side-by-side views.
Closes#410
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Marking a file as viewed always scrolled its header to the top of the
diff container, pushing files above it out of the viewport (#402). That
scroll was introduced in #164 to keep the viewport anchored when
collapsing a long file the user had scrolled deep into — without it, the
viewport would land in unrelated content below.
Only scroll when the file header is actually above the container top at
the moment of marking. When the header is already visible, collapse in
place: files above stay stationary and files below fill the space.
Closes#402
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
WebKit ignores position:relative on <tr>, so the after:inset-0 overlay
applied to the row resolved against the app root and covered the whole
viewport when selecting lines in unified mode. Move the overlay styles
to global.css and anchor them to each td, following the same approach
as the existing keyboard-cursor styles.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
intent(highlight): .svelte files rendered as plain text, hard to read in diffs
decision(highlight): use third-party prism-svelte plugin (no svelte grammar in prismjs core)
learned(highlight): single loadPrismLanguage entry covers both per-line and whole-file SFC paths
Claude-Session: https://claude.ai/code/session_01CEjYe8GH1BYPzf52LN8ty1