git rerere learns conflict resolutions from past upstream merges and
replays them on the next merge. When past resolutions used the buggy
mergeWithNewestVersions ordering, rerere auto-resolves package.json
files with stale content before transformConflictedPackageJson ever
gets a chance to run — so the fixed merge logic never reaches the
file.
Add reconcileAllPackageJson, which runs after every merge (clean,
auto-resolved, or partially conflicted) and rewrites every package.json
that the merge touched from the kilo branch's pre-merge HEAD and the
opencode compat branch using the same merge logic. Files that are
still conflicted are skipped so manual resolution isn't silently
overwritten.
This makes our merge logic the source of truth for package.json
content, regardless of what rerere or git's textual merge produced.
Seeding the result with theirs' keys and appending ours-only keys at the
end caused kilo-only deps (e.g. rotating-file-stream in packages/core)
to relocate from the middle of the deps block to the end during the
pre-merge transform. Git's textual 3-way merge then saw ours keeping
the line in place and theirs adding the same key elsewhere, producing
a duplicate JSON key in the merged file.
Iterate ours first so kilo-only deps stay in their original position,
then append any theirs-only keys at the end.
The transform that materialises the 'kilo compat for vX.Y.Z' commit during
an upstream merge takes upstream's package.json wholesale and re-applies a
hand-picked list of Kilo scripts. That list was incomplete: it only covered
`extension`, `changeset`, `changeset:version`, `test`, and `test:ci`, so
every merge silently dropped Kilo's `postinstall` tail (`&& bun run
script/setup-git.ts`) and the `dev-setup` root shortcut, and it never
pruned upstream-only scripts (`dev:desktop` / `dev:web` / `dev:console`)
or upstream-only catalog entries (`@sentry/solid`,
`@sentry/vite-plugin`) whose target packages Kilo doesn't ship.
- replace the per-script if-blocks with data-driven PRESERVE_SCRIPTS
- add DELETE_UPSTREAM_SCRIPTS for scripts that reference packages Kilo
doesn't ship
- add DELETE_UPSTREAM_CATALOG for catalog entries with zero Kilo consumers
- apply the same policy in both transformPackageJson (conflict path) and
transformAllPackageJson (pre-merge sweep path) — previously the
pre-merge sweep was missing `changeset` / `changeset:version`
preservation
- new tests covering preservation, deletion, opencode test scripts, and
the catalog pruning
The automated kilo compat transform keeps upstream's root scripts/catalog
wholesale and only re-applies a handful of Kilo-specific scripts, which
silently dropped:
- postinstall's `&& bun run script/setup-git.ts` tail (needed to set
merge.conflictStyle=zdiff3 locally, which upstream merges rely on)
- the `dev-setup` script shorthand
- kept dead `dev:desktop` / `dev:web` / `dev:console` scripts whose
target packages aren't tracked in Kilo
- kept `@sentry/solid` / `@sentry/vite-plugin` catalog entries that
have zero consumers in our tree
When the consumer (readLines) hits the line/byte cap and destroys the
PassThrough, the underlying createReadStream had no link back and would
keep reading chunks to EOF in the background, defeating the early-exit
optimisation for large files.
Optimistically stream the file as UTF-8 -- the common case -- using a
fatal-mode TextDecoder so the read tool can stop pulling bytes from
disk once the line / 50KB byte cap is hit. Only fall back to a
full-buffer iconv decode when the bytes turn out not to be valid UTF-8.
The streaming + retry logic lives in a new kilo helper
(packages/opencode/src/kilocode/text-stream.ts) so the read tool's
`lines` function stays close to upstream OpenCode shape.
The existing apply_patch encoding test only checked final file bytes,
which were correct because Patch.deriveNewContentsFromChunks does its
own encoding-aware read. The diff and additions/deletions counts
returned to the user (and shown in the permission prompt) were
nonetheless garbled because the surrounding tool reused a hard-coded
UTF-8 decoder. Pin the diff metadata to catch that regression.
Bom.readFile uses a hard-coded UTF-8 TextDecoder, so the oldContent
captured for the diff, additions/deletions counts, and permission-prompt
metadata was mojibake for any non-UTF-8 file. The actual bytes written
were correct because deriveNewContentsFromChunks already does
encoding-aware reads, but the user-visible diff did not reflect the real
file contents. Switch to EncodedIO.read to match.
* feat(diff-viewer): add base branch picker to workspace diff source
Introduce a UI control that lets users override the comparison base
branch in the diff viewer. The picker lists local and remote branches
sorted by commit date, with a "Default" option that falls back to the
auto-resolved tracking/default branch.
Key changes:
- Add `listBranches` to GitOps for sorted branch enumeration
- Extend DiffSourceCatalog with base branch override state and disposal
- Add `reactivate` method to SourceController for in-place source rebuild
- Create shared BranchSelect component (moved from agent-manager)
- Add BaseBranchPicker component for the diff viewer header
- Wire new webview messages (requestBranches, setBaseBranch, branches)
- Add i18n keys for all supported locales
- Register DiffSourceCatalog as disposable in extension activation
* refactor(diff-sources): extract staged and unstaged git diff sources into standalone modules
Decompose the diff source system by introducing dedicated modules for
staged (index vs HEAD) and unstaged (working tree vs index) views,
alongside shared git-status parsing utilities.
- Create git-status.ts with reusable parseNameStatus, parseNumstat,
showBlob, readDisk helpers and the summarize builder
- Implement staged.ts source using `git diff --cached` against HEAD
- Implement unstaged.ts source combining tracked diffs with untracked
file enumeration via `git ls-files --others`
- Register both sources in DiffSourceCatalog when a workspace root exists
- Extend DiffSourceType union with "staged" and "unstaged" variants
- Rename workspace label from "Local Changes" to "Branch" and add
i18n entries for the new source picker options
* feat(vscode): display current branch in diff viewer base branch picker
Show the currently checked-out branch (HEAD) alongside the base branch
selector with an arrow indicator (current → base), providing clearer
context for which branches are being compared in the diff viewer.
* refactor(diff): replace magic empty string with named INDEX_REF constant and fix disposal
Extract `INDEX_REF` constant in git-status module to clarify intent when
referencing the staging area instead of a commit. Update staged and
unstaged sources to use it. Additionally:
- Clear `baseBranchOverride` on dispose to prevent stale state
- Apply `generatedLike` detection to staged diff source
- Update tests to reflect new disposal semantics
* fix(vscode): move baseBranchOverride state from catalog to provider
Relocate the base branch override from DiffSourceCatalog into
DiffViewerProvider where it belongs as panel-level state. Pass it
through PanelContext so the catalog remains stateless and testable.
- Add `baseBranchOverride` field to PanelContext type
- Thread override via ctx in DiffViewerProvider.openPanel and setBaseBranch
- Remove setBaseBranchOverride/getBaseBranchOverride from catalog
- Accept override as parameter in listWorkspaceBranches
- Simplify catalog dispose and update tests accordingly
* feat(i18n): add staged/unstaged diff source labels and rename workspace to branch
Introduce translated strings for the new "staged" and "unstaged" diff
viewer source options across all 18 locale files. Rename the existing
workspace source label from "Local changes" to "Branch" in each
language to better reflect its scope.
* test(vscode): update diff source catalog tests to include staged and unstaged entries
Align test expectations with the newly added staged/unstaged diff
sources. The listAvailable assertions now verify that both "staged"
and "unstaged" appear alongside "workspace" in the returned source
list.
* fix(vscode): add path traversal protection and size guards to diff sources
Introduce `resolveInside` to reject absolute paths and `..` traversal
that could escape the workspace directory. Replace raw `path.join`
calls in `readDisk`, `fileSize`, and unstaged file lookups with the
safe resolver.
Add `blobSize` and `fileSize` helpers to check content length before
reading, skipping detail fetches for files exceeding MAX_DETAIL_BYTES
in both staged and unstaged sources. Re-export MAX_DETAIL_BYTES from
git-status for shared access.
* fix(diff): resolve override branch refs via remote fallback
When `baseBranchOverride` is a short remote-tracking name (e.g.
`feature` from `refs/remotes/origin/feature`), `git merge-base` fails
because no local branch exists. Add `resolveOverrideRef` that attempts
`rev-parse --verify` on the short name first, then falls back to
`origin/<name>` before giving up entirely and resuming auto-detection.
* fix(vscode): use lstat for symlink-safe working-tree reads
Replace `fs.stat` with `fs.lstat` in `readDisk`, `fileSize`, and
unstaged file enumeration to avoid following symlinks. For symlink
entries, `readDisk` now returns the link target string (matching git's
blob storage) instead of reading the pointed-to file's contents.
This prevents untracked symlinks from leaking arbitrary file contents
(e.g. `~/.aws/credentials`) into the diff viewer, since `resolveInside`
only guards against lexical path traversal, not symlink dereferencing.
* refactor(diff): propagate mtime-based stamps for untracked file cache invalidation
Untracked files always report additions/deletions as 0 since numstat
cannot compute them without an index blob. This made the webview cache
unable to detect edits to untracked files, leaving stale content visible
between polling cycles.
Introduce an optional `stamp` field on `FileEntry` that encodes
size+mtime for untracked entries, and thread it through `summarize()`
and `fetchFile()` so cache keys update whenever the file is modified on
disk. Tracked entries continue using the numstat-derived stamp as before.
* fix(vscode): log for-each-ref failures in listBranches instead of silently swallowing
Replace the empty `.catch(() => "")` with a handler that logs the
error message before returning the fallback empty string, improving
debuggability when branch enumeration fails.