feat(lanes | component): redesign the changes & compare experience#10378
feat(lanes | component): redesign the changes & compare experience#10378luvkapur wants to merge 122 commits into
Conversation
Defines scope, architecture, hook bindings, and acceptance criteria (no-drawer, no-monaco, tab rewiring, manual smoke) for porting the new lane-compare design into ____bit while keeping workspace data sources. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16-task plan with explicit scope/name/rootDir per new bit component so resulting npm package ids match imports from the new design. Phases A-F cover primitives, non-monaco diff foundation, inline tab components, compare shell, lane-compare port (drawer stripped), and final tab-list wiring + acceptance gates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ports CompareToolbar from new-changes workspace; adds @teambit/design.inputs.selectors.multi-select@2.0.17 to workspace policy (was missing). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ports CompareSidebar (with SidebarGroup and SidebarComponentItem sub-components) from the new-changes workspace into the component-compare component and exports all public types from the index. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add teambit.dependencies/ui/deps-diff-table — a pure table component for rendering dependency diffs between two component versions, with status indicators and compare URL links. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds teambit.code/ui/inline-code-compare, which renders per-file inline diffs using DiffFileRenderer from inline-diff-viewer and wires into ComponentCompare context. Fixed import path (component.ui.component-compare.component-compare) and removed unused variable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ports teambit.review/ui/inline-tests-compare from lane-compare branch; remaps bare component-compare import to component-compare.component-compare package id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds teambit.review/ui/inline-config-compare — renders aspect config/data diffs between two component versions using DiffFileRenderer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eHeader Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Required by InlineComponentCompare (Task 12). bit install side-effects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds teambit.semantics/ui/api-diff-view — a full-pane React component that queries and renders API surface changes between two component versions using GQL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e data Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hapes - Drop `isMerged` references from lane-compare-page (LaneModel only has `deleted`) - Adapt `useLaneComponents` call to single-arg signature returning an object - Use `componentDescriptors` (not `components`) for the compositions map since `.get()` is a ComponentDescriptor method Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hema InlineContextProvider and EagerFileRegistrar used useGetComponents / createComponentModel from the cloud GraphQL schema (getComponents / CompDescriptor), which the workspace/scope dev-server schema does not expose - every InlineComponentCompare fired a 400, breaking the code diff and stalling the lane-diff query via a re-render storm. Replace with the schema-compatible useComponent hook (spec section 7.1 fallback). Drop the now-unused cloud packages from workspace.jsonc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract workspace-overview's data-agnostic pieces into a shared @teambit/explorer.ui.components-overview component; refactor both workspace-overview and lane-overview into thin adapters. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… plan Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rview Creates @teambit/explorer.ui.components-overview with data-agnostic card, filter-panel, aggregation hooks, and a new ComponentsOverview orchestrator that accepts getHref / storageNamespace injection points. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…onent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the header slot and filter command bar in a single sticky div, measures its height via ResizeObserver into --components-overview-sticky-height, and uses that CSS var as the top offset for section headers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit 7b5b858 |
|
Code review by qodo was updated up to the latest commit 95ba129 |
The lane export failed because ui/inline-code-compare was a NEW component with default-scope teambit.code, colliding with the existing teambit.code/ui/inline-code-compare on main (unrelated histories → unmergeable). Re-track it as the existing component (teambit.code/ui/inline-code-compare@0.0.1) so our implementation lands as a snap on top of main's history and the lane exports cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit 9e79e8c |
|
Code review by qodo was updated up to the latest commit 656ee41 |
|
Code review by qodo was updated up to the latest commit 6997023 |
|
Code review by qodo was updated up to the latest commit 5b52d6b |
buildSplitRows returns { rows, hunkHeaders } but only rows is used here.
Drops the dead _hunkHeaders binding (react-env eslint no-unused-vars warning).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Code review by qodo was updated up to the latest commit b4e7b09 |
…d-react Re-tracking onto the existing teambit.code/ui/inline-diff-viewer@0.0.3 made the component inherit that unrelated component's cloud-react env, whose strict airbnb EslintLint build task rejects this diff code (for-loops, ++, continue) with 42 errors. Align it with its sibling teambit.code/ui/diff-viewer (react-env@1.3.5), which builds clean. bit lint under react-env: 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| { | ||
| "compilerOptions": { | ||
| "jsx": "react", | ||
| "experimentalDecorators": true, | ||
| "emitDecoratorMetadata": true, | ||
| "strict": true, | ||
| "moduleResolution": "node", | ||
| "module": "commonjs", | ||
| "target": "es2015", | ||
| "skipLibCheck": true, | ||
| "noImplicitAny": false, | ||
| "preserveConstEnums": true, | ||
| "strictPropertyInitialization": false, | ||
| "esModuleInterop": true, | ||
| "resolveJsonModule": true, | ||
| "sourceMap": true, | ||
| "outDir": "./dist" | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
1. Unformatted mock tsconfig.json 📘 Rule violation ⚙ Maintainability
The newly added scopes/semantics/schema/mock/default-export/tsconfig.json is not Prettier-formatted (indentation does not match the repo’s Prettier settings), which is likely to fail npm run prettier:check / the lint gate.
Agent Prompt
## Issue description
The new `tsconfig.json` fixture is not formatted according to the repository’s Prettier configuration, which can fail `npm run prettier:check`.
## Issue Context
Compliance requires PR changes to pass the repo lint + typecheck gate and adhere to Prettier formatting.
## Fix Focus Areas
- scopes/semantics/schema/mock/default-export/tsconfig.json[1-19]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| private async getOrCompute<T>( | ||
| inflight: Map<string, Promise<T>>, | ||
| cacheKey: string, | ||
| compute: () => Promise<T>, | ||
| cacheable: (value: T) => boolean = () => true, | ||
| skipPersistentCache = false | ||
| ): Promise<T> { | ||
| const pending = inflight.get(cacheKey); | ||
| if (pending) return pending; | ||
| if (!skipPersistentCache) { | ||
| const cached = await this.cache.get<T>(cacheKey); | ||
| if (cached !== undefined) return cached; | ||
| // a concurrent caller may have started computing while we awaited the cache read. | ||
| const started = inflight.get(cacheKey); | ||
| if (started) return started; | ||
| } | ||
| const promise = compute() | ||
| .then((result) => { | ||
| if (!skipPersistentCache && cacheable(result)) void this.cache.set(cacheKey, result); | ||
| return result; | ||
| }) | ||
| .finally(() => inflight.delete(cacheKey)); | ||
| inflight.set(cacheKey, promise); | ||
| return promise; |
There was a problem hiding this comment.
2. Unbounded compare cache 🐞 Bug ☼ Reliability
ComponentCompareMain now persists compare/api-diff results via CacheMain.set() without a TTL, so every unique (baseId, compareId) pair can remain on disk indefinitely under CACHE_ROOT. Because these results can include large diffOutput plus baseContent/compareContent, the cache can grow without bound over time and add serialization overhead on compare-heavy workloads.
Agent Prompt
### Issue description
`ComponentCompareMain.getOrCompute()` writes compare/API-diff results into the persistent `@teambit/cache` store without any TTL. `CacheMain.set()` treats `ttl` as optional and stores entries with `expire = null` when omitted, which makes them effectively permanent. Since compare results may include large per-file `diffOutput` and even full `baseContent`/`compareContent`, this can lead to unbounded cache growth under `CACHE_ROOT`.
### Issue Context
The new bulk compare implementation increases the number of cached entries dramatically (many pairs per lane/workspace view). Without a TTL or an eviction/size policy, the cache directory can grow over time and eventually impact disk usage and server stability.
### Fix Focus Areas
- scopes/component/component-compare/component-compare.main.runtime.ts[89-112]
- scopes/harmony/cache/cache.main.runtime.ts[34-40]
### Suggested fix
1. Introduce a bounded caching strategy for compare results and API diffs, e.g.:
- Add a configurable TTL (e.g. `compareCacheTtlMs`, `apiDiffCacheTtlMs`) and pass it to `cache.set(cacheKey, result, ttl)`.
- Optionally avoid persisting the heaviest fields (e.g. omit `baseContent`/`compareContent` from cached payloads, or only cache when payload size is under a threshold).
2. (If TTL isn’t acceptable) implement an explicit eviction policy in `CacheMain` (e.g. periodic cleanup / max size), but TTL is the minimal safe change here.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit bee92f6 |
…eview to public teambit.code These three new components defaulted to the private teambit.review scope, so exporting them to the public changes-redesign lane failed with "can't export private components to public lane". Re-home them alongside the other diff/compare components (diff-viewer, inline-diff-viewer, inline-code-compare, code-compare) in the public teambit.code scope. Verified: no same-named components exist on teambit.code, so no collision. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| const MAX_INTRA_LINE_CHARS = 2000; | ||
| const MAX_INTRA_LINE_CELLS = 1_000_000; | ||
|
|
||
| /** | ||
| * Token-level (word) diff of two lines via an LCS walk, returning the half-open character ranges that | ||
| * changed on each side. Adjacent changed ranges are merged so the highlight reads as one span. Lines | ||
| * past a size cap fall back to no intra-line ranges to keep the computation bounded. | ||
| */ | ||
| export function intraLineDiff( | ||
| from: string, | ||
| to: string | ||
| ): { delRanges: Array<[number, number]>; addRanges: Array<[number, number]> } { | ||
| if (from.length > MAX_INTRA_LINE_CHARS || to.length > MAX_INTRA_LINE_CHARS) { | ||
| return { delRanges: [], addRanges: [] }; | ||
| } | ||
| const a = tokenizeLine(from); | ||
| const b = tokenizeLine(to); | ||
| const m = a.length; | ||
| const n = b.length; | ||
| if (m * n > MAX_INTRA_LINE_CELLS) { | ||
| return { delRanges: [], addRanges: [] }; | ||
| } | ||
| const dp: number[][] = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => 0)); | ||
| for (let i = m - 1; i >= 0; i--) { | ||
| for (let j = n - 1; j >= 0; j--) { | ||
| dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]); | ||
| } | ||
| } |
There was a problem hiding this comment.
1. Large intra-line dp alloc 🐞 Bug ➹ Performance
intraLineDiff() allocates a full (m+1)×(n+1) DP matrix (nested JS arrays) for token-level LCS when m*n <= 1_000_000, which can still cause significant jank/memory pressure on long/minified lines because this runs for each del/add line pair in a change block.
Agent Prompt
### Issue description
`components/ui/diff-viewer/diff-model.ts` implements intra-line highlighting using an LCS DP table allocated as a full 2D `(m+1)×(n+1)` matrix of JS numbers/arrays. Even with `MAX_INTRA_LINE_CELLS = 1_000_000`, this can still allocate many arrays and a large number of boxed numbers per changed-line pair, creating avoidable UI jank and memory pressure.
### Issue Context
This code executes on the UI thread and may run multiple times per diff block (paired del/add lines). The current cap avoids the worst pathological cases, but the allowed maximum is still large enough to be expensive in JS.
### Fix Focus Areas
- components/ui/diff-viewer/diff-model.ts[101-173]
### Suggested fix approach
- Replace the full 2D DP allocation with a space-optimized DP (two-row rolling arrays), ideally using typed arrays (`Uint16Array`/`Uint32Array`) to reduce overhead.
- Alternatively (or additionally), reduce `MAX_INTRA_LINE_CELLS` to a safer bound for browser memory/CPU, and/or add an additional cap on token count (not only char count).
- Keep the current fallback behavior (returning no intra-line ranges) when the caps are exceeded.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit 78f523b |
| it('settles immediately and reports no data when skipped', () => { | ||
| const wrapper = ({ children }: { children: React.ReactNode }) => ( | ||
| <MockedProvider mocks={[]}>{children}</MockedProvider> | ||
| ); |
There was a problem hiding this comment.
1. use-bulk-paged-query test uses .spec.tsx 📘 Rule violation ⚙ Maintainability
A new unit test was added as a .spec.tsx file instead of the required .spec.ts naming convention. This diverges from the repository’s unit test organization standard and can make test discovery/consistency harder.
Agent Prompt
## Issue description
A new unit test was introduced as `use-bulk-paged-query.spec.tsx`, but the compliance checklist requires unit tests to be written as `.spec.ts` files alongside the source.
## Issue Context
The current test uses JSX for the `wrapper`, which is why it was created as `.tsx`. If the repo standard is strictly `.spec.ts`, refactor to avoid JSX (e.g., `React.createElement(...)`) and rename the file to `.spec.ts`.
## Fix Focus Areas
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.spec.tsx[1-29]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // object keys and sorts array elements by their serialized form, yielding a stable set signature. | ||
| const canonical = (value: unknown): string => { | ||
| const norm = (v: any): any => { | ||
| if (Array.isArray(v)) return v.map(norm).sort((x, y) => (JSON.stringify(x) < JSON.stringify(y) ? -1 : 1)); |
There was a problem hiding this comment.
2. Invalid sort comparator 🐞 Bug ≡ Correctness
deriveChangeTypes() canonicalizes unordered data using an Array.sort() comparator that never returns 0, violating the comparator contract for equal elements and making ordering undefined when two normalized items stringify identically. This can lead to unstable canonical strings and therefore flaky change-type classification in those collision cases.
Agent Prompt
### Issue description
`canonical()` uses `sort((x,y)=> JSON.stringify(x) < JSON.stringify(y) ? -1 : 1)` which never returns `0` when the keys are equal. This violates the `Array.sort()` comparator requirements and makes the order undefined when two normalized elements stringify to the same value.
### Issue Context
This comparator is used to build a canonical string representation for unordered structures in `deriveChangeTypes()`. If canonicalization is unstable in collision cases, `differsUnordered()` can intermittently report differences.
### Fix Focus Areas
- scopes/lanes/lanes/lanes.main.runtime.ts[1710-1724]
### Suggested fix
Change the comparator to return `0` when equal (and ideally avoid repeated `JSON.stringify` calls):
- Compute `sx`/`sy` once per comparison.
- Return `0` when `sx === sy`, otherwise `sx < sy ? -1 : 1`.
Optionally, for performance and determinism, map items to `{key, value}` pairs, sort by `key`, then pluck `value`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const plainLaneDiff: PlainLaneDiff | undefined = data?.lanes.diffStatus && { | ||
| sourceLane: LaneId.from(data.lanes.diffStatus.source.name, data.lanes.diffStatus.source.scope).toString(), | ||
| targetLane: LaneId.from(data.lanes.diffStatus.target.name, data.lanes.diffStatus.target.scope).toString(), | ||
| diff: data.lanes.diffStatus.componentsStatus.map((c) => ({ | ||
| ...c, | ||
| changes: c.changes || [], | ||
| componentId: ComponentID.fromObject(c.componentId).toString(), | ||
| })), |
There was a problem hiding this comment.
3. Unknown changes shown none 🐞 Bug ≡ Correctness
When the server cannot derive change types it returns changes = undefined (explicitly not “no changes”), but useLaneDiffStatus() coerces missing changes to [] and LaneComponentDiff.changeType then treats [] as ChangeType.NONE. This loses the “unknown/unclassified” state and can incorrectly surface components as having no changes after transient derivation failures.
Agent Prompt
### Issue description
The lane-diff pipeline currently collapses `changes: undefined/null` (meaning “couldn’t classify / not computed”) into an empty array in the UI hook. Downstream, the lane-diff model treats an empty array as `NONE`, which misrepresents uncertainty as a confirmed no-op.
### Issue Context
On the server, `deriveChangeTypes()` can return `undefined` when it can’t load the required `Version` objects (transient). That undefined is meaningful and should not be mapped to “no changes”.
### Fix Focus Areas
- components/ui/compare/lane-compare-hooks/use-lane-diff-status/use-lane-diff-status.ts[84-91]
- scopes/lanes/lanes/lanes.main.runtime.ts[1697-1701]
- scopes/lanes/entities/lane-diff/lane-component-diff.ts[46-53]
### Suggested fix
Implement a tri-state representation end-to-end:
1) In `useLaneDiffStatus`, do **not** coerce missing `c.changes` to `[]`. Preserve `null/undefined` (e.g. `changes: c.changes ?? null`).
2) Update the lane-diff entities (`PlainLaneComponentDiff` / `LaneComponentDiff`) to allow `changes` to be nullable/optional and introduce an explicit “unclassified/unknown” handling path.
3) Ensure UI filtering/grouping does not treat “unknown” as `NONE` and does not hide those components silently (e.g. show a dedicated state/badge or keep them visible under a separate group).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit 5ea49d3 |
Reworks how changes are viewed across the app: the single-component compare, the lane compare, and the workspace/lane overviews, onto one shared, bulk-fetched implementation, and replaces the Monaco-based code diff with a lean shiki-based viewer. It also brings the semantic API diff into the compare surface as a first-class view, and fixes lane-vs-main comparison. The goal was best-in-class feature and performance parity for reviewing changes, with Bit's own tokens, theme, and identity.
Unified compare surface
InlineComponentCompareis now the single implementation shared by the single-component compare page and lane-compare: one set of view-mode tabs (Code / Preview / Docs / Dependencies / Tests / Config / API), one diff pane, one toolbar.data-view-modeattribute plus CSS, so a mode click never remounts or re-fetches (fixes the "switching is slow" and console-error regressions).DiffModeProvider/useDiffModegives a single global split/unified toggle across every file instead of a per-file one.Comparing a lane against main
getHeadOnMain/importMainHeads) falls back to the remote default-lane ref and bulk-imports the missing main heads, so lane-only components whose main version was never fetched are compared correctly instead of being misclassified as new or "no changes".skipUpToDatewas dropping every component of a lane that is forked and ahead of main: a forward-only lane has no target-only snaps, so each read as up-to-date and got filtered out; source-ahead components are now kept. Second, thediffStatusresult-memo key was derived fromLane.hash(a method, not the raw hash), so it never invalidated when the lane or main advanced and served a stale empty result; it is now keyed on the real hash plus main heads.getHeadOnMain, so "merge from main" no longer drops components without a local main head.baseSource: 'workspace' | 'scope'): a small cloud badge on the sidebar row marks bases resolved from the remote scope versus the local workspace.Bulk data fetching
CompareDataProviderplus a bulkcompareComponents(pairs)GraphQL query fetch code/aspect/test diffs for all component pairs in one paginated call, keyed bycompareId; inline panels read from this context instead of firing per-file queries.FileRegistrycontext feeds the sidebar file tree from the same bulk result.changesderivation dropped from about 47s to about 8s: diff rawVersionobjects instead of fully loading both components, defer the (UI-unused) API change-type off the critical path, and lift the artificial concurrency cap. A client render loop in the file registry was also removed viauseSyncExternalStoreplus a version-guarded notify.New code diff viewer (no Monaco)
teambit.code/ui/diff-viewerbuilt on shiki'screateHighlighterCoreplus@shikijs/engine-javascript(WASM-free JS regex engine, grammars code-split per language), far leaner than the Monaco diff it replaces.inline-code-compare(Monaco still backs the live code editor, untouched). Addsjsonc/json5highlighting for config files.Semantic API diff view
API diff correctness
X (default)added to public API" on docs-only diffs:export default Xwas extracted inconsistently across builds (UnImplementedSchemaversus a"X (default)"TypeRefSchemaalias). The extractor now emits a stabledefaultre-reference, and the diff is made robust to historical artifacts: it filtersUnImplementedSchemaexports, dedupes redundant re-export aliases, and surfaces genuinely-unresolved exports instead of diffing them.@see, and so on), not just the description.Base / compare resolution
?version) points the compare side at that clean version instead.computeComparediffs against on-disk files when the compare side is the checked-out workspace (compareIsLiveWorkspace); those mutable diffs are never cached.Shared overviews
workspace-overviewandlane-overviewnow consume one sharedcomponents-overviewcomponent instead of duplicating the grid/card logic.