From e554f5577a4d5f5b601c996a109d6529b7117537 Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Fri, 30 Oct 2026 23:55:42 -0400 Subject: [PATCH 1/3] Loosen share schemas and paint share peeks from alignment rows Released Desktops parse downloaded bundles with strict schemas, so any field a newer sharer adds fails their share import. Readers now drop unknown fields in the bundle, its snapshot and its presentation, so a Desktop opens any later bundle. A hosted share's peeks pair base and head excerpts with jsdiff, so a selection on new code, which has no base excerpt, prints as plain code. The bundle gains an optional `alignments` field, one entry per run of the sharer's diff with 1-based base and head line pairs, and the viewer paints from it when present: a null side is added or removed, a differing pair is removed then added. Ranges no run reaches keep the jsdiff pairing. Nothing writes the field yet; the exporter waits on `share-peek-rows-export` until loose-schema Desktops have rolled out. `@dev.fast/review-share-protocol` 0.4.1, `@dev.fast/canvas` 0.3.4. Agent-Session: 845c0993-e0a1-45e7-84a3-caa8eec4d49f Agent-Session: 61c94475-694a-4e46-8971-e0d703d3b344 Agent-Session: e033b653-84f5-4ec8-8eeb-d3a3af5312c5 Agent-Session: f2f8f3d8-5a50-4c15-8ad8-9c9308d4a110 Agent-Session: 64182dea-534e-45f1-8e28-c1e27a5d6bab Agent-Session: b47f56d8-24aa-4d4f-95b3-4217b7ec9e00 Agent-Session: 2b04f44e-91c6-4568-8c57-c4a01b2828ad --- packages/canvas/package.json | 2 +- packages/review-share-protocol/package.json | 2 +- .../review-share-protocol/src/index.test.ts | 12 ++ packages/review-share-protocol/src/index.ts | 40 +++++-- .../src/share/inline-editors.browser.test.ts | 90 +++++++++++++++ .../review/app/src/share/inline-editors.ts | 108 ++++++++++++++++-- packages/review/app/src/share/open-share.ts | 6 +- packages/review/src/sharing/import.ts | 12 +- 8 files changed, 244 insertions(+), 28 deletions(-) diff --git a/packages/canvas/package.json b/packages/canvas/package.json index 4a2998cc1..d7541523d 100644 --- a/packages/canvas/package.json +++ b/packages/canvas/package.json @@ -1,6 +1,6 @@ { "name": "@dev.fast/canvas", - "version": "0.3.2", + "version": "0.3.4", "license": "MIT", "repository": { "type": "git", diff --git a/packages/review-share-protocol/package.json b/packages/review-share-protocol/package.json index b901eae04..ea15c605f 100644 --- a/packages/review-share-protocol/package.json +++ b/packages/review-share-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@dev.fast/review-share-protocol", - "version": "0.3.1", + "version": "0.4.1", "description": "Portable immutable Review sharing envelope.", "license": "MIT", "repository": { diff --git a/packages/review-share-protocol/src/index.test.ts b/packages/review-share-protocol/src/index.test.ts index 357e1591b..7222737ce 100644 --- a/packages/review-share-protocol/src/index.test.ts +++ b/packages/review-share-protocol/src/index.test.ts @@ -65,6 +65,7 @@ const bundle: ShareBundle = { lines: ["const a = 1;"], }, ], + alignments: [{ file: "src/a.ts", head: "a".repeat(40), rows: [[null, 3]] }], }; it("rejects an older format and an unknown source kind", () => { @@ -80,6 +81,17 @@ it("rejects an older format and an unknown source kind", () => { ).toBe(false); }); +it("drops fields a newer sharer adds instead of rejecting the bundle", () => { + const parsed = shareBundleSchema.parse({ + ...bundle, + later: true, + sources: [{ ...bundle.sources[0], later: true }], + alignments: [{ ...bundle.alignments![0], later: true }], + }); + + expect(parsed).toEqual(bundle); +}); + it("normalizes GitHub transports without publishing credentials", () => { for (const remote of [ "git@github.com:owner/repo.git", diff --git a/packages/review-share-protocol/src/index.ts b/packages/review-share-protocol/src/index.ts index 88a7d6efa..694756f56 100644 --- a/packages/review-share-protocol/src/index.ts +++ b/packages/review-share-protocol/src/index.ts @@ -71,7 +71,7 @@ const sideSchema = z.enum(["base", "head"]); /** An excerpt is the lines a board references. */ export const shareSourceSchema = z.discriminatedUnion("kind", [ - z.strictObject({ + z.object({ kind: z.literal("excerpt"), file: sharePathSchema, commit: commitSchema, @@ -80,13 +80,13 @@ export const shareSourceSchema = z.discriminatedUnion("kind", [ }), ]); -const lineRangeSchema = z.strictObject({ +const lineRangeSchema = z.object({ side: sideSchema, file: sharePathSchema, fromLine: z.number().int().positive(), toLine: z.number().int().positive(), pins: z - .strictObject({ + .object({ repositoryId: z.string(), head: z.string(), base: z.string().optional(), @@ -94,30 +94,52 @@ const lineRangeSchema = z.strictObject({ .optional(), }); -export const shareBundleSchema = z.strictObject({ +/** A base line and a head line the sharer's diff paired, 1-based; null is a + * line the other side lacks. */ +const alignmentRowSchema = z.tuple([ + z.number().int().positive().nullable(), + z.number().int().positive().nullable(), +]); + +/** One contiguous run of the diff alignment a board's references span, so a + * reader paints the sharer's diff instead of pairing excerpts itself. */ +export const shareAlignmentSchema = z.object({ + file: sharePathSchema, + /** The base side's path when the file was renamed. */ + previousFile: sharePathSchema.optional(), + head: z.string(), + base: z.string().optional(), + rows: z.array(alignmentRowSchema).min(1), +}); + +/** Readers drop fields from newer sharers, so a Desktop can open any later + * bundle. */ +export const shareBundleSchema = z.object({ format: z.literal(SHARE_FORMAT), reviewId: z.string().min(1).max(256), version: z.number().int().nonnegative(), title: z.string().max(4096), /** Validated against the document schema by the reader. */ snapshot: z.record(z.string(), z.unknown()), - images: z.record(z.string(), z.strictObject({ png: z.base64() })), + images: z.record(z.string(), z.object({ png: z.base64() })), traces: z.record(z.string(), z.unknown()), maps: z.record(z.string(), z.unknown()), commits: z.array(z.unknown()), /** Each reference's lines, resolved where the board was shared. */ selections: z.record(z.string(), z.array(lineRangeSchema)), sources: z.array(shareSourceSchema), - repository: z - .strictObject({ cloneUrl: gitHubRepositoryUrlSchema }) - .optional(), + /** The sharer's diff over the selections, since 0.4. */ + alignments: z.array(shareAlignmentSchema).optional(), + repository: z.object({ cloneUrl: gitHubRepositoryUrlSchema }).optional(), }); export type ShareBundle = z.infer; export type ShareSource = z.infer; -export const createShareSchema = z.strictObject({ +export type ShareAlignment = z.infer; + +export const createShareSchema = z.object({ requestId: z.uuid(), bundle: shareBundleSchema, }); diff --git a/packages/review/app/src/share/inline-editors.browser.test.ts b/packages/review/app/src/share/inline-editors.browser.test.ts index acb6f511d..485b767be 100644 --- a/packages/review/app/src/share/inline-editors.browser.test.ts +++ b/packages/review/app/src/share/inline-editors.browser.test.ts @@ -189,3 +189,93 @@ it("follows the reader's layout choice and reports focus", async () => { handle.dispose(); container.remove(); }); + +it("paints one-sided rows of the sharer's alignment with no lines to pair", async () => { + const editors = await shareInlineEditors(sources, pins, [ + { + file: "exec.ts", + base, + head, + rows: [ + [10, null], + [11, null], + [12, null], + [null, 12], + [null, 13], + [null, 14], + [null, 15], + ], + }, + ]); + + container.innerHTML = editors(settings("unified").peek).renderStatic!({ + ...spec, + ranges: [{ side: "head", startLine: 12, endLine: 15 }], + }); + + expect(lines()).toEqual([ + ["12", "added"], + ["13", "added"], + ["14", "added"], + ["15", "added"], + ]); + + container.innerHTML = editors(settings("split").peek).renderStatic!({ + ...spec, + ranges: [{ side: "base", startLine: 10, endLine: 12 }], + }); + + expect(lines('[data-column="base"]')).toEqual([ + ["10", "removed"], + ["11", "removed"], + ["12", "removed"], + ]); + expect(lines('[data-column="head"]')).toEqual([null, null, null]); +}); + +it("reads a pair with differing text as removed then added, and leaves ranges the alignment misses to pairing", async () => { + const editors = ( + await shareInlineEditors( + [ + { ...sources[0], lines: ["a", "old", "b"] }, + { ...sources[1], lines: ["a", "new", "b", "c"] }, + ], + pins, + [ + { + file: "exec.ts", + base, + head, + rows: [ + [10, 12], + [11, 13], + [12, 14], + ], + }, + ], + ) + )(settings("unified").peek); + + container.innerHTML = editors.renderStatic!({ + ...spec, + ranges: [ + { side: "base", startLine: 10, endLine: 12 }, + { side: "head", startLine: 12, endLine: 14 }, + ], + }); + + expect(lines()).toEqual([ + ["12", null], + ["11", "removed"], + ["13", "added"], + ["14", null], + ]); + expect(container.querySelectorAll("[data-range]")).toHaveLength(1); + + container.innerHTML = editors.renderStatic!({ + ...spec, + ranges: [{ side: "head", startLine: 15, endLine: 15 }], + }); + + expect(lines()).toEqual([["15", null]]); +}); diff --git a/packages/review/app/src/share/inline-editors.ts b/packages/review/app/src/share/inline-editors.ts index da700afa3..ce3789f7b 100644 --- a/packages/review/app/src/share/inline-editors.ts +++ b/packages/review/app/src/share/inline-editors.ts @@ -15,7 +15,10 @@ import type { ReviewInlineStaticSpec, ReviewSourcePins, } from "@dev.fast/review-protocol"; -import type { ShareSource } from "@dev.fast/review-share-protocol"; +import type { + ShareAlignment, + ShareSource, +} from "@dev.fast/review-share-protocol"; import { type ShjToken, tokenize } from "@speed-highlight/core"; import * as stylex from "@stylexjs/stylex"; import { diffArrays } from "diff"; @@ -136,22 +139,102 @@ function unified(base: Block, head: Block): Block[] { return [{ side: "head", rows, diff: true }]; } +type SideRange = Spec["ranges"][number] & { side: ReviewDiffSide }; + +/** The sharer's diff over the ranges an alignment reaches, one block per run of + * rows; the ranges none reaches stay in `ranges`. */ +function alignedBlocks( + excerpt: Excerpt, + pins: ReviewSourcePins | undefined, + alignments: readonly ShareAlignment[], + ranges: Set, +): Block[] { + const blocks: Block[] = []; + + if (!pins) return blocks; + const reached = new Set(); + + for (const alignment of alignments) { + if (alignment.head !== pins.head || alignment.base !== pins.base) continue; + let run: Row[] | undefined; + + for (const [b, h] of alignment.rows) { + const hit = [...ranges].filter((range) => { + const n = range.side === "base" ? b : h; + + return n !== null && range.startLine <= n && n <= range.endLine; + }); + + if (!hit.length) { + run = undefined; + + continue; + } + + for (const range of hit) reached.add(range); + + if (!run) blocks.push({ side: "head", rows: (run = []), diff: true }); + + const before = + b === null + ? undefined + : { + n: b, + ...excerpt( + alignment.previousFile ?? alignment.file, + pins.base ?? pins.head, + b, + ), + }; + + const after = + h === null + ? undefined + : { n: h, ...excerpt(alignment.file, pins.head, h) }; + + if (before && after && before.text === after.text) + run.push({ ...after, base: b! }); + else { + if (before) run.push({ ...before, change: "removed" }); + + if (after) run.push({ ...after, change: "added" }); + } + } + } + + for (const range of reached) ranges.delete(range); + + return blocks; +} + /** Each range read at its own side. A selection resolves to a base range and * a head range, which read as one diff. */ function rangeBlocks( excerpt: Excerpt, boardPins: ReviewSourcePins | undefined, + alignments: readonly ShareAlignment[], spec: Spec, ) { const pins = spec.pins ?? boardPins; + + const ranges = new Set( + spec.ranges.map((range) => ({ ...range, side: range.side ?? spec.side })), + ); + + const aligned = alignedBlocks( + excerpt, + pins, + alignments.filter((alignment) => alignment.file === spec.path), + ranges, + ); + const sides: Record = { base: [], head: [] }; - for (const range of spec.ranges) { - const side = range.side ?? spec.side; + for (const { side, startLine, endLine } of ranges) { const commit = side === "base" ? (pins?.base ?? pins?.head) : pins?.head; const rows: Row[] = []; - for (let n = Math.max(1, range.startLine); n <= range.endLine; n++) + for (let n = Math.max(1, startLine); n <= endLine; n++) rows.push({ n, ...(commit && excerpt(spec.path, commit, n)) }); sides[side].push({ side, rows }); @@ -160,6 +243,7 @@ function rangeBlocks( const { base, head } = sides; return [ + ...aligned, ...head.flatMap((block, k) => (base[k] ? unified(base[k], block) : block)), ...base.slice(head.length), ]; @@ -435,29 +519,33 @@ function peekInner( export async function shareInlineEditors( sources: readonly ShareSource[], boardPins: ReviewSourcePins | undefined, + alignments: readonly ShareAlignment[] = [], ): Promise<(settings: PeekSettings) => ReviewInlineEditorFactory> { - return excerptEditors(await excerpts(sources), boardPins); + return excerptEditors(await excerpts(sources), boardPins, alignments); } /** Peeks with no lines to show: each says its source is unavailable. */ export const sourcelessEditors = (settings: PeekSettings) => - excerptEditors(() => ({}), undefined)(settings); + excerptEditors(() => ({}), undefined, [])(settings); function excerptEditors( excerpt: Excerpt, boardPins: ReviewSourcePins | undefined, + alignments: readonly ShareAlignment[], ): (settings: PeekSettings) => ReviewInlineEditorFactory { + const blocksOf = (spec: Spec) => + rangeBlocks(excerpt, boardPins, alignments, spec); + return (settings) => ({ async find(spec, query) { return { - matchCount: find(rangeBlocks(excerpt, boardPins, spec), matcher(query)) - .count, + matchCount: find(blocksOf(spec), matcher(query)).count, }; }, renderStatic: (spec: ReviewInlineStaticSpec) => - `
${peekInner(spec.title, rangeBlocks(excerpt, boardPins, spec), find([], undefined), settings)}
`, + `
${peekInner(spec.title, blocksOf(spec), find([], undefined), settings)}
`, create(spec) { - const blocks = rangeBlocks(excerpt, boardPins, spec); + const blocks = blocksOf(spec); const listeners = new Set<(height: number) => void>(); let found = find([], undefined); let collapsed = false; diff --git a/packages/review/app/src/share/open-share.ts b/packages/review/app/src/share/open-share.ts index e967dd83e..3f0ffe2f4 100644 --- a/packages/review/app/src/share/open-share.ts +++ b/packages/review/app/src/share/open-share.ts @@ -53,7 +53,11 @@ export async function openShare( const host = readOnlyHost({ ...options, - inlineEditors: await shareInlineEditors(bundle.sources, pins), + inlineEditors: await shareInlineEditors( + bundle.sources, + pins, + bundle.alignments, + ), }); return { diff --git a/packages/review/src/sharing/import.ts b/packages/review/src/sharing/import.ts index 6fea3f7e5..57aa24aea 100644 --- a/packages/review/src/sharing/import.ts +++ b/packages/review/src/sharing/import.ts @@ -45,7 +45,7 @@ import { sharedGit, } from "./repository.js"; -export const sharedSnapshotSchema = z.strictObject({ +export const sharedSnapshotSchema = z.object({ reviewId: z.string().min(1), version: z.number().int().nonnegative(), /** Empty for a board never named; it reads "Untitled". */ @@ -60,7 +60,7 @@ export const sharedSnapshotSchema = z.strictObject({ lenses: z.array(lensSchema).optional(), createdAt: z.string(), origin: z - .strictObject({ + .object({ branch: z.string().optional(), baseRef: z.string().optional(), baseBranch: z.string().optional(), @@ -75,7 +75,7 @@ export const sharedSnapshotSchema = z.strictObject({ const count = z.number().int().nonnegative(); -const counts = z.strictObject({ additions: count, deletions: count }); +const counts = z.object({ additions: count, deletions: count }); const mapSchema = softwareModelDataSchema.extend({ commit: z.string().regex(COMMIT_ID), @@ -88,10 +88,10 @@ const mapSchema = softwareModelDataSchema.extend({ counts.extend({ file: z.string(), hunks: z.array( - z.strictObject({ + z.object({ startLine: count, lines: z.array( - z.strictObject({ + z.object({ kind: z.enum(["add", "remove"]), oldLine: count.nullable(), newLine: count.nullable(), @@ -106,7 +106,7 @@ const mapSchema = softwareModelDataSchema.extend({ ), }); -export const sharePresentationSchema = z.strictObject({ +export const sharePresentationSchema = z.object({ maps: z.record(z.string(), mapSchema), }); From 58566ae0a997bc7484faa94ee770759f3a28513f Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Mon, 2 Nov 2026 12:37:40 -0500 Subject: [PATCH 2/3] Test share peeks on real diffs and keep a change's removed lines first Six real commits of this repository, as Git patched them and with both files: an insertion, a new file, a modification, a net deletion, a rename and a deleted file. The viewer must show each hunk as Git prints it in both layouts with the real text, paint the same from one run per hunk as from the whole file, and show what a selection on one side's changed lines alone replaced. A run of changed pairs painted removed and added lines pair by pair. Added lines now wait until the change ends, so a multi-line change reads as its removed lines, then its added lines. Agent-Session: 61c94475-694a-4e46-8971-e0d703d3b344 Agent-Session: 196d7f0b-63a2-4186-84b0-5068df27bf9a Agent-Session: b47f56d8-24aa-4d4f-95b3-4217b7ec9e00 Agent-Session: f2f8f3d8-5a50-4c15-8ad8-9c9308d4a110 Agent-Session: 2b04f44e-91c6-4568-8c57-c4a01b2828ad --- .../src/share/__fixtures__/real-diffs.json | 57 ++++ .../inline-editors.real-diffs.browser.test.ts | 307 ++++++++++++++++++ .../review/app/src/share/inline-editors.ts | 18 +- 3 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 packages/review/app/src/share/__fixtures__/real-diffs.json create mode 100644 packages/review/app/src/share/inline-editors.real-diffs.browser.test.ts diff --git a/packages/review/app/src/share/__fixtures__/real-diffs.json b/packages/review/app/src/share/__fixtures__/real-diffs.json new file mode 100644 index 000000000..e0977846c --- /dev/null +++ b/packages/review/app/src/share/__fixtures__/real-diffs.json @@ -0,0 +1,57 @@ +[ + { + "name": "insert", + "base": "6eb725d4e71da77920d7345be53274bbeb2fafc3", + "head": "3a5a4d503b5602c51671858d214e02af56126296", + "file": "apps/review-desktop/code-oss/src/vs/review/common/reviewConfigurationDefaults.ts", + "baseText": "/*---------------------------------------------------------------------------------------------\n * Copyright (c) dev.fast. All rights reserved.\n * Licensed under the MIT License. See LICENSE in the repository root for license information.\n *--------------------------------------------------------------------------------------------*/\n\n/**\n * Review's configuration data: setting keys and default values, with no imports\n * and no side effects.\n *\n * This module is deliberately import-free. `vs/review/node/reviewUserConfigImport.ts`\n * reads these keys from the Electron main process, which `src/main.ts` reaches\n * before `startup()` calls `bootstrapESM()` \u2014 the step that installs\n * `globalThis._VSCODE_NLS_MESSAGES`. Anything imported here is evaluated inside\n * that pre-bootstrap window, where a packaged build's mangled `localize(2488, null)`\n * throws `!!! NLS MISSING: 2488 !!!` into a modal, before any window or log exists.\n *\n * Registration lives in `reviewConfiguration.ts`, which imports this file and owns\n * the configuration-registry calls. Never add an import here, and never re-export\n * these values from `reviewConfiguration.ts` \u2014 a re-export would put the\n * configuration registry back on the main process's import path.\n *\n * Enforced by `scripts/main-bootstrap-imports.test.mjs`.\n */\n\nexport const REVIEW_KEYMAP_SETTING = 'review.keymap';\nexport const REVIEW_TELEMETRY_SETTING = 'review.telemetry.enabled';\nexport const REVIEW_SOFTWARE_MAP_SETTING = 'review.experimental.softwareMap.enabled';\nexport const REVIEW_REMOTE_HOSTS_SETTING = 'review.remote.hosts';\nexport const REVIEW_REMOTE_INSTALL_SETTING = 'review.remote.install';\nexport const REVIEW_REMOTE_INSTALL_MODES = ['ask', 'always', 'never'] as const;\nexport const REVIEW_KEYMAPS = ['none', 'vim', 'emacs', 'sublime'] as const;\nexport type ReviewKeymap = typeof REVIEW_KEYMAPS[number];\nexport const REVIEW_OPEN_FILES_IN_SETTING = 'review.openFilesIn';\nexport const REVIEW_OPEN_FILES_IN_CHOICES = ['whiteboard', 'vscode', 'cursor', 'zed', 'application'] as const;\nexport const REVIEW_OPEN_FILES_IN_APPLICATION_SETTING = 'review.openFilesInApplication';\nexport const REVIEW_READY_NOTIFICATION_SETTING = 'review.notifications.reviewReady';\nexport const REVIEW_READY_NOTIFICATION_CHOICES = ['notificationAndBadge', 'notification', 'off'] as const;\nexport const REVIEW_DIAGRAM_FULLSCREEN_ON_CLICK_SETTING = 'review.diagrams.fullscreenOnClick';\nexport const REVIEW_DOCUMENT_WIDTH_SETTING = 'review.documentWidth';\nexport const REVIEW_DIFF_COLOR_THEME_SETTING = 'review.diffColorTheme';\nexport const REVIEW_INLINE_LINE_NUMBERS_SETTING = 'review.inlineDiffs.showLineNumbers';\nexport const REVIEW_PEEK_SIDE_BY_SIDE_SETTING = 'review.peeks.renderSideBySide';\nexport const REVIEW_DOCUMENT_WIDTH_CHOICES = ['standard', 'wide', 'full'] as const;\nexport const REVIEW_DOCUMENT_FONT_SIZE_SETTING = 'review.document.fontSize';\nexport const REVIEW_DOCUMENT_FONT_SIZE_BASE = 15;\nexport const REVIEW_READING_FONT_SETTING = 'review.readingFont';\nexport const REVIEW_READING_FONTS = ['serif', 'sans'] as const;\nexport const REVIEW_READING_FONT_FAMILY_SETTING = 'review.readingFontFamily';\nexport const EDITOR_FONT_SIZE_SETTING = 'editor.fontSize';\nexport const EDITOR_FONT_FAMILY_SETTING = 'editor.fontFamily';\nexport const EDITOR_FONT_LIGATURES_SETTING = 'editor.fontLigatures';\nexport const DIFF_EDITOR_FONT_SIZE_SETTING = 'diffEditor.fontSize';\nexport const DIFF_EDITOR_WORD_WRAP_SETTING = 'diffEditor.wordWrap';\nexport const DIFF_EDITOR_IGNORE_TRIM_WHITESPACE_SETTING = 'diffEditor.ignoreTrimWhitespace';\n\nexport const reviewConfigurationDefaults = {\n\t[REVIEW_SOFTWARE_MAP_SETTING]: false,\n\t[REVIEW_REMOTE_HOSTS_SETTING]: [],\n\t[REVIEW_REMOTE_INSTALL_SETTING]: 'ask',\n\t[REVIEW_TELEMETRY_SETTING]: true,\n\t'telemetry.telemetryLevel': 'off',\n\t'telemetry.enableTelemetry': false,\n\t'telemetry.enableCrashReporter': false,\n\t'telemetry.editStats.enabled': false,\n\t'security.workspace.trust.enabled': false,\n\t'js/ts.experimental.useTsgo': true,\n\t'typescript.experimental.useTsgo': true,\n\t// Review files are read-only: a squiggle names an error the reader cannot fix.\n\t'problems.visibility': false,\n\t'workbench.enableExperiments': false,\n\t'workbench.commandPalette.experimental.enableNaturalLanguageSearch': false,\n\t'workbench.settings.enableNaturalLanguageSearch': false,\n\t'window.autoDetectColorScheme': true,\n\t'workbench.colorTheme': 'Review Dark',\n\t'workbench.preferredDarkColorTheme': 'Review Dark',\n\t'workbench.preferredLightColorTheme': 'Review Light',\n\t'workbench.startupEditor': 'none',\n\t'workbench.activityBar.location': 'hidden',\n\t'workbench.statusBar.visible': false,\n\t'window.title': 'Whiteboard',\n\t'window.commandCenter': false,\n\t'workbench.navigationControl.enabled': true,\n\t'window.menuBarVisibility': 'hidden',\n\t'workbench.editor.showTabs': 'none',\n\t'workbench.editor.editorActionsLocation': 'hidden',\n\t// Reduce tab widths when the row fills. The native tab control truncates the\n\t// labels first. It keeps horizontal scrolling after tabs reach their minimum.\n\t'workbench.editor.tabSizing': 'shrink',\n\t// Keep wheel, trackpad, keyboard, and active-tab scrolling. Hide only the\n\t// scrollbar, which would otherwise sit across the bottom of the pill row.\n\t'workbench.editor.titleScrollbarVisibility': 'hidden',\n\t// Home is the only sticky editor in Review. \"compact\" makes a sticky tab show\n\t// its icon alone, so the Home pill lost its name. \"normal\" shows the home\n\t// icon and the word \"Home\".\n\t'workbench.editor.pinnedTabSizing': 'normal',\n\t// A sticky tab shows an unpin button in place of its close button. Home is\n\t// always sticky and the reader cannot unpin it, so the button is dead chrome.\n\t'workbench.editor.tabActionUnpinVisibility': false,\n\t'workbench.layoutControl.enabled': false,\n\t// Diffs and code peeks are Monaco editors, and Monaco takes its font from\n\t// this setting rather than from CSS. Geist Mono is bundled with the canvas\n\t// stylesheet (its @font-face rules are hoisted out of the canvas scope), so\n\t// the editors share the chrome and code face of the whiteboard design. The\n\t// tail is Monaco's own macOS default stack.\n\t'editor.fontFamily': '\"Geist Mono\", Menlo, Monaco, \"Courier New\", monospace',\n\t// Matches the canvas's code blocks. Listing it here also keeps the\n\t// first-run import from carrying over a VS Code size; Settings exposes it\n\t// instead.\n\t[EDITOR_FONT_SIZE_SETTING]: 13,\n\t[DIFF_EDITOR_FONT_SIZE_SETTING]: 12,\n\t'editor.minimap.enabled': false,\n\t// A 10px lane on both axes, as in the canvas; review.css insets the slider\n\t// to the same 6px pill.\n\t'editor.scrollbar.verticalScrollbarSize': 10,\n\t'editor.scrollbar.horizontalScrollbarSize': 10,\n\t'diffEditor.renderIndicators': false,\n\t'breadcrumbs.enabled': false,\n\t'git.enabled': false,\n\t// Retain read-only protection when opening a legacy Review-owned checkout.\n\t// New review sources use immutable virtual documents; LSP targets are local files.\n\t'files.readonlyInclude': { '/**/.git/dev-fast/worktrees/**': true },\n\t// Keep read-only badges from crowding Review's compact file tabs.\n\t'workbench.editor.decorations.badges': false,\n\t'chat.disableAIFeatures': true,\n\t'extensions.ignoreRecommendations': true,\n\t'workbench.tips.enabled': false\n} as const;\n\n/**\n * Defaults that keep the curated built-in extensions quiet.\n *\n * Review ships no marketplace, so an extension that prompts to install or update\n * something offers the reader a dead end. Most keys here close one such prompt.\n * Keep this aligned with the curated groups in\n * `scripts/curated-extensions.manifest.mjs`, whose contract test is\n * `scripts/curated-extensions.test.mjs`.\n */\nexport const curatedExtensionConfigurationDefaults = {\n\t// Otherwise ty runs any `ty` on PATH instead of its bundled build.\n\t'ty.importStrategy': 'useBundled',\n\n\t// golang.go asks users to fill in a survey on a timer.\n\t'go.survey.prompt': false,\n\t// Prompts to `go install` newer tools against a toolchain Review does not ship.\n\t'go.toolsManagement.checkForUpdates': 'off',\n\t// These run the tests or offer to scan the module.\n\t'go.enableCodeLens': { runtest: false },\n\t'go.diagnostic.vulncheck': 'Off',\n\n\t// \"This file is not linked to a Cargo project\" \u2014 routine when the review is\n\t// scoped to a subdirectory of a workspace.\n\t'rust-analyzer.showUnlinkedFileNotification': false,\n\t'rust-analyzer.notifications.cargoTomlNotFound': false,\n\t'rust-analyzer.showRequestFailedErrorNotification': false,\n\t// These build the checkout. A review only needs navigation.\n\t'rust-analyzer.checkOnSave': false,\n\t'rust-analyzer.lens.run.enable': false,\n\t'rust-analyzer.lens.debug.enable': false,\n\t// These run the checkout's build scripts and proc macros; proc macros imply build scripts.\n\t'rust-analyzer.cargo.buildScripts.enable': false,\n\t'rust-analyzer.procMacro.enable': false,\n\n\t// The Swift extension can offer to install Swiftly. Review uses the Swift\n\t// toolchain that the user installed and made available on the shell PATH.\n\t'swift.disableSwiftlyInstallPrompt': true,\n\t// These write files into the checkout or build the package.\n\t'swift.autoGenerateLaunchConfigurations': false,\n\t'swift.showTestCodeLenses': false,\n\n\t// The .NET support extension must not send its own telemetry or expose AI\n\t// tools. C# uses the system .NET SDK that Review resolves from the shell PATH.\n\t'dotnetAcquisitionExtension.enableTelemetry': false,\n\t'dotnetAcquisitionExtension.enableLanguageModelTools': false,\n\t// These download packages or build the project.\n\t'dotnet.projects.enableAutomaticRestore': false,\n\t'csharp.suppressDotnetRestoreNotification': true,\n\t'dotnet.codeLens.enableTestsCodeLens': false\n\n\t// vscodevim.vim and tuttieee.emacs-mcx contribute no prompts.\n\t// ms-vscode.sublime-keybindings asks once, on first activation, whether to\n\t// import Sublime Text settings. It has no setting to turn that off.\n} as const;\n\nexport const reviewAgentsWindowDefaultOverrides = {\n\t'window.title': reviewConfigurationDefaults['window.title'],\n\t'workbench.navigationControl.enabled': reviewConfigurationDefaults['workbench.navigationControl.enabled'],\n} as const;\n", + "headText": "/*---------------------------------------------------------------------------------------------\n * Copyright (c) dev.fast. All rights reserved.\n * Licensed under the MIT License. See LICENSE in the repository root for license information.\n *--------------------------------------------------------------------------------------------*/\n\n/**\n * Review's configuration data: setting keys and default values, with no imports\n * and no side effects.\n *\n * This module is deliberately import-free. `vs/review/node/reviewUserConfigImport.ts`\n * reads these keys from the Electron main process, which `src/main.ts` reaches\n * before `startup()` calls `bootstrapESM()` \u2014 the step that installs\n * `globalThis._VSCODE_NLS_MESSAGES`. Anything imported here is evaluated inside\n * that pre-bootstrap window, where a packaged build's mangled `localize(2488, null)`\n * throws `!!! NLS MISSING: 2488 !!!` into a modal, before any window or log exists.\n *\n * Registration lives in `reviewConfiguration.ts`, which imports this file and owns\n * the configuration-registry calls. Never add an import here, and never re-export\n * these values from `reviewConfiguration.ts` \u2014 a re-export would put the\n * configuration registry back on the main process's import path.\n *\n * Enforced by `scripts/main-bootstrap-imports.test.mjs`.\n */\n\nexport const REVIEW_KEYMAP_SETTING = 'review.keymap';\nexport const REVIEW_TELEMETRY_SETTING = 'review.telemetry.enabled';\nexport const REVIEW_SOFTWARE_MAP_SETTING = 'review.experimental.softwareMap.enabled';\n/** Code OSS's own setting, registered in `update.config.contribution.ts`. */\nexport const REVIEW_SHOW_RELEASE_NOTES_SETTING = 'update.showReleaseNotes';\nexport const REVIEW_REMOTE_HOSTS_SETTING = 'review.remote.hosts';\nexport const REVIEW_REMOTE_INSTALL_SETTING = 'review.remote.install';\nexport const REVIEW_REMOTE_INSTALL_MODES = ['ask', 'always', 'never'] as const;\nexport const REVIEW_KEYMAPS = ['none', 'vim', 'emacs', 'sublime'] as const;\nexport type ReviewKeymap = typeof REVIEW_KEYMAPS[number];\nexport const REVIEW_OPEN_FILES_IN_SETTING = 'review.openFilesIn';\nexport const REVIEW_OPEN_FILES_IN_CHOICES = ['whiteboard', 'vscode', 'cursor', 'zed', 'application'] as const;\nexport const REVIEW_OPEN_FILES_IN_APPLICATION_SETTING = 'review.openFilesInApplication';\nexport const REVIEW_READY_NOTIFICATION_SETTING = 'review.notifications.reviewReady';\nexport const REVIEW_READY_NOTIFICATION_CHOICES = ['notificationAndBadge', 'notification', 'off'] as const;\nexport const REVIEW_DIAGRAM_FULLSCREEN_ON_CLICK_SETTING = 'review.diagrams.fullscreenOnClick';\nexport const REVIEW_DOCUMENT_WIDTH_SETTING = 'review.documentWidth';\nexport const REVIEW_DIFF_COLOR_THEME_SETTING = 'review.diffColorTheme';\nexport const REVIEW_INLINE_LINE_NUMBERS_SETTING = 'review.inlineDiffs.showLineNumbers';\nexport const REVIEW_PEEK_SIDE_BY_SIDE_SETTING = 'review.peeks.renderSideBySide';\nexport const REVIEW_DOCUMENT_WIDTH_CHOICES = ['standard', 'wide', 'full'] as const;\nexport const REVIEW_DOCUMENT_FONT_SIZE_SETTING = 'review.document.fontSize';\nexport const REVIEW_DOCUMENT_FONT_SIZE_BASE = 15;\nexport const REVIEW_READING_FONT_SETTING = 'review.readingFont';\nexport const REVIEW_READING_FONTS = ['serif', 'sans'] as const;\nexport const REVIEW_READING_FONT_FAMILY_SETTING = 'review.readingFontFamily';\nexport const EDITOR_FONT_SIZE_SETTING = 'editor.fontSize';\nexport const EDITOR_FONT_FAMILY_SETTING = 'editor.fontFamily';\nexport const EDITOR_FONT_LIGATURES_SETTING = 'editor.fontLigatures';\nexport const DIFF_EDITOR_FONT_SIZE_SETTING = 'diffEditor.fontSize';\nexport const DIFF_EDITOR_WORD_WRAP_SETTING = 'diffEditor.wordWrap';\nexport const DIFF_EDITOR_IGNORE_TRIM_WHITESPACE_SETTING = 'diffEditor.ignoreTrimWhitespace';\n\nexport const reviewConfigurationDefaults = {\n\t[REVIEW_SOFTWARE_MAP_SETTING]: false,\n\t[REVIEW_REMOTE_HOSTS_SETTING]: [],\n\t[REVIEW_REMOTE_INSTALL_SETTING]: 'ask',\n\t[REVIEW_TELEMETRY_SETTING]: true,\n\t'telemetry.telemetryLevel': 'off',\n\t'telemetry.enableTelemetry': false,\n\t'telemetry.enableCrashReporter': false,\n\t'telemetry.editStats.enabled': false,\n\t'security.workspace.trust.enabled': false,\n\t'js/ts.experimental.useTsgo': true,\n\t'typescript.experimental.useTsgo': true,\n\t// Review files are read-only: a squiggle names an error the reader cannot fix.\n\t'problems.visibility': false,\n\t'workbench.enableExperiments': false,\n\t'workbench.commandPalette.experimental.enableNaturalLanguageSearch': false,\n\t'workbench.settings.enableNaturalLanguageSearch': false,\n\t'window.autoDetectColorScheme': true,\n\t'workbench.colorTheme': 'Review Dark',\n\t'workbench.preferredDarkColorTheme': 'Review Dark',\n\t'workbench.preferredLightColorTheme': 'Review Light',\n\t'workbench.startupEditor': 'none',\n\t'workbench.activityBar.location': 'hidden',\n\t'workbench.statusBar.visible': false,\n\t'window.title': 'Whiteboard',\n\t'window.commandCenter': false,\n\t'workbench.navigationControl.enabled': true,\n\t'window.menuBarVisibility': 'hidden',\n\t'workbench.editor.showTabs': 'none',\n\t'workbench.editor.editorActionsLocation': 'hidden',\n\t// Reduce tab widths when the row fills. The native tab control truncates the\n\t// labels first. It keeps horizontal scrolling after tabs reach their minimum.\n\t'workbench.editor.tabSizing': 'shrink',\n\t// Keep wheel, trackpad, keyboard, and active-tab scrolling. Hide only the\n\t// scrollbar, which would otherwise sit across the bottom of the pill row.\n\t'workbench.editor.titleScrollbarVisibility': 'hidden',\n\t// Home is the only sticky editor in Review. \"compact\" makes a sticky tab show\n\t// its icon alone, so the Home pill lost its name. \"normal\" shows the home\n\t// icon and the word \"Home\".\n\t'workbench.editor.pinnedTabSizing': 'normal',\n\t// A sticky tab shows an unpin button in place of its close button. Home is\n\t// always sticky and the reader cannot unpin it, so the button is dead chrome.\n\t'workbench.editor.tabActionUnpinVisibility': false,\n\t'workbench.layoutControl.enabled': false,\n\t// Diffs and code peeks are Monaco editors, and Monaco takes its font from\n\t// this setting rather than from CSS. Geist Mono is bundled with the canvas\n\t// stylesheet (its @font-face rules are hoisted out of the canvas scope), so\n\t// the editors share the chrome and code face of the whiteboard design. The\n\t// tail is Monaco's own macOS default stack.\n\t'editor.fontFamily': '\"Geist Mono\", Menlo, Monaco, \"Courier New\", monospace',\n\t// Matches the canvas's code blocks. Listing it here also keeps the\n\t// first-run import from carrying over a VS Code size; Settings exposes it\n\t// instead.\n\t[EDITOR_FONT_SIZE_SETTING]: 13,\n\t[DIFF_EDITOR_FONT_SIZE_SETTING]: 12,\n\t'editor.minimap.enabled': false,\n\t// A 10px lane on both axes, as in the canvas; review.css insets the slider\n\t// to the same 6px pill.\n\t'editor.scrollbar.verticalScrollbarSize': 10,\n\t'editor.scrollbar.horizontalScrollbarSize': 10,\n\t'diffEditor.renderIndicators': false,\n\t'breadcrumbs.enabled': false,\n\t'git.enabled': false,\n\t// Retain read-only protection when opening a legacy Review-owned checkout.\n\t// New review sources use immutable virtual documents; LSP targets are local files.\n\t'files.readonlyInclude': { '/**/.git/dev-fast/worktrees/**': true },\n\t// Keep read-only badges from crowding Review's compact file tabs.\n\t'workbench.editor.decorations.badges': false,\n\t'chat.disableAIFeatures': true,\n\t'extensions.ignoreRecommendations': true,\n\t'workbench.tips.enabled': false\n} as const;\n\n/**\n * Defaults that keep the curated built-in extensions quiet.\n *\n * Review ships no marketplace, so an extension that prompts to install or update\n * something offers the reader a dead end. Most keys here close one such prompt.\n * Keep this aligned with the curated groups in\n * `scripts/curated-extensions.manifest.mjs`, whose contract test is\n * `scripts/curated-extensions.test.mjs`.\n */\nexport const curatedExtensionConfigurationDefaults = {\n\t// Otherwise ty runs any `ty` on PATH instead of its bundled build.\n\t'ty.importStrategy': 'useBundled',\n\n\t// golang.go asks users to fill in a survey on a timer.\n\t'go.survey.prompt': false,\n\t// Prompts to `go install` newer tools against a toolchain Review does not ship.\n\t'go.toolsManagement.checkForUpdates': 'off',\n\t// These run the tests or offer to scan the module.\n\t'go.enableCodeLens': { runtest: false },\n\t'go.diagnostic.vulncheck': 'Off',\n\n\t// \"This file is not linked to a Cargo project\" \u2014 routine when the review is\n\t// scoped to a subdirectory of a workspace.\n\t'rust-analyzer.showUnlinkedFileNotification': false,\n\t'rust-analyzer.notifications.cargoTomlNotFound': false,\n\t'rust-analyzer.showRequestFailedErrorNotification': false,\n\t// These build the checkout. A review only needs navigation.\n\t'rust-analyzer.checkOnSave': false,\n\t'rust-analyzer.lens.run.enable': false,\n\t'rust-analyzer.lens.debug.enable': false,\n\t// These run the checkout's build scripts and proc macros; proc macros imply build scripts.\n\t'rust-analyzer.cargo.buildScripts.enable': false,\n\t'rust-analyzer.procMacro.enable': false,\n\n\t// The Swift extension can offer to install Swiftly. Review uses the Swift\n\t// toolchain that the user installed and made available on the shell PATH.\n\t'swift.disableSwiftlyInstallPrompt': true,\n\t// These write files into the checkout or build the package.\n\t'swift.autoGenerateLaunchConfigurations': false,\n\t'swift.showTestCodeLenses': false,\n\n\t// The .NET support extension must not send its own telemetry or expose AI\n\t// tools. C# uses the system .NET SDK that Review resolves from the shell PATH.\n\t'dotnetAcquisitionExtension.enableTelemetry': false,\n\t'dotnetAcquisitionExtension.enableLanguageModelTools': false,\n\t// These download packages or build the project.\n\t'dotnet.projects.enableAutomaticRestore': false,\n\t'csharp.suppressDotnetRestoreNotification': true,\n\t'dotnet.codeLens.enableTestsCodeLens': false\n\n\t// vscodevim.vim and tuttieee.emacs-mcx contribute no prompts.\n\t// ms-vscode.sublime-keybindings asks once, on first activation, whether to\n\t// import Sublime Text settings. It has no setting to turn that off.\n} as const;\n\nexport const reviewAgentsWindowDefaultOverrides = {\n\t'window.title': reviewConfigurationDefaults['window.title'],\n\t'workbench.navigationControl.enabled': reviewConfigurationDefaults['workbench.navigationControl.enabled'],\n} as const;\n", + "patch": "diff --git a/apps/review-desktop/code-oss/src/vs/review/common/reviewConfigurationDefaults.ts b/apps/review-desktop/code-oss/src/vs/review/common/reviewConfigurationDefaults.ts\nindex cf187654e..98588ec41 100644\n--- a/apps/review-desktop/code-oss/src/vs/review/common/reviewConfigurationDefaults.ts\n+++ b/apps/review-desktop/code-oss/src/vs/review/common/reviewConfigurationDefaults.ts\n@@ -25,6 +25,8 @@\n export const REVIEW_KEYMAP_SETTING = 'review.keymap';\n export const REVIEW_TELEMETRY_SETTING = 'review.telemetry.enabled';\n export const REVIEW_SOFTWARE_MAP_SETTING = 'review.experimental.softwareMap.enabled';\n+/** Code OSS's own setting, registered in `update.config.contribution.ts`. */\n+export const REVIEW_SHOW_RELEASE_NOTES_SETTING = 'update.showReleaseNotes';\n export const REVIEW_REMOTE_HOSTS_SETTING = 'review.remote.hosts';\n export const REVIEW_REMOTE_INSTALL_SETTING = 'review.remote.install';\n export const REVIEW_REMOTE_INSTALL_MODES = ['ask', 'always', 'never'] as const;\n" + }, + { + "name": "new-file", + "base": "6eb725d4e71da77920d7345be53274bbeb2fafc3", + "head": "3a5a4d503b5602c51671858d214e02af56126296", + "file": "apps/review-desktop/code-oss/src/vs/review/common/reviewReleaseNotes.ts", + "baseText": null, + "headText": "/*---------------------------------------------------------------------------------------------\n * Copyright (c) dev.fast. All rights reserved.\n * Licensed under the MIT License. See LICENSE in the repository root for license information.\n *--------------------------------------------------------------------------------------------*/\n\nimport { type ReviewCanvasRelease, whiteboardReleaseApiUrl } from './reviewProtocol.js';\n\n/** Opens the Release notes tab: for the version given, else the running one. */\nexport const REVIEW_SHOW_RELEASE_NOTES_COMMAND_ID = 'review.showReleaseNotes';\n\nexport async function fetchWhiteboardRelease(version: string): Promise {\n\tconst response = await fetch(whiteboardReleaseApiUrl(version), { headers: { Accept: 'application/vnd.github+json' } });\n\tif (!response.ok) {\n\t\tthrow new Error(response.status === 404 ? `Whiteboard ${version} has no release notes yet.` : `GitHub answered ${response.status}.`);\n\t}\n\treturn response.json();\n}\n", + "patch": "diff --git a/apps/review-desktop/code-oss/src/vs/review/common/reviewReleaseNotes.ts b/apps/review-desktop/code-oss/src/vs/review/common/reviewReleaseNotes.ts\nnew file mode 100644\nindex 000000000..a8a9b699f\n--- /dev/null\n+++ b/apps/review-desktop/code-oss/src/vs/review/common/reviewReleaseNotes.ts\n@@ -0,0 +1,17 @@\n+/*---------------------------------------------------------------------------------------------\n+ * Copyright (c) dev.fast. All rights reserved.\n+ * Licensed under the MIT License. See LICENSE in the repository root for license information.\n+ *--------------------------------------------------------------------------------------------*/\n+\n+import { type ReviewCanvasRelease, whiteboardReleaseApiUrl } from './reviewProtocol.js';\n+\n+/** Opens the Release notes tab: for the version given, else the running one. */\n+export const REVIEW_SHOW_RELEASE_NOTES_COMMAND_ID = 'review.showReleaseNotes';\n+\n+export async function fetchWhiteboardRelease(version: string): Promise {\n+\tconst response = await fetch(whiteboardReleaseApiUrl(version), { headers: { Accept: 'application/vnd.github+json' } });\n+\tif (!response.ok) {\n+\t\tthrow new Error(response.status === 404 ? `Whiteboard ${version} has no release notes yet.` : `GitHub answered ${response.status}.`);\n+\t}\n+\treturn response.json();\n+}\n" + }, + { + "name": "modify", + "base": "080f292b14921d66f7f98f5a9c57cb4efefba134", + "head": "4a9c32cb78bc7415e83cabfb94e1da447ec269c9", + "file": "apps/review-desktop/scripts/prune-review-runtime.mjs", + "baseText": "import { lstat, readdir, unlink } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nconst developmentTestPackages = new Set([\n \"@agentclientprotocol/claude-agent-acp\",\n \"@agentclientprotocol/sdk\",\n \"@pinojs/redact\",\n \"call-bind-apply-helpers\",\n \"call-bound\",\n \"dunder-proto\",\n \"es-define-property\",\n \"es-errors\",\n \"es-object-atoms\",\n \"fast-uri\",\n \"function-bind\",\n \"get-intrinsic\",\n \"get-proto\",\n \"gopd\",\n \"has-symbols\",\n \"isexe\",\n \"json-schema-traverse\",\n \"math-intrinsics\",\n \"object-inspect\",\n \"on-exit-leak-free\",\n \"pi-acp\",\n \"pino\",\n \"pino-abstract-transport\",\n \"pino-std-serializers\",\n \"process-warning\",\n \"qs\",\n \"quick-format-unescaped\",\n \"setprototypeof\",\n \"side-channel\",\n \"side-channel-list\",\n \"side-channel-map\",\n \"side-channel-weakmap\",\n \"sonic-boom\",\n \"thread-stream\",\n \"zod\",\n]);\n\nexport async function pruneReviewRuntime(root) {\n const removedBytes = {};\n\n async function visit(directory) {\n for (const entry of await readdir(directory, { withFileTypes: true })) {\n const file = path.join(directory, entry.name);\n\n if (entry.isSymbolicLink()) continue;\n\n if (entry.isDirectory()) {\n await visit(file);\n continue;\n }\n\n if (!entry.isFile()) continue;\n const relative = path.relative(root, file).split(path.sep).join(\"/\");\n const parts = relative.split(\"/\");\n\n if (\n entry.name.endsWith(\".map\") ||\n /^(license|notice|copying)/i.test(entry.name)\n )\n continue;\n const packageParts = parts.slice(parts.lastIndexOf(\"node_modules\") + 1);\n\n const packageName = packageParts[0].startsWith(\"@\")\n ? packageParts.slice(0, 2).join(\"/\")\n : packageParts[0];\n\n let category;\n\n if (relative.startsWith(\"src/\")) category = \"own-source\";\n else if (relative.startsWith(\"node_modules/\")) {\n if (parts.some((part) => part === \"sharp\" || part.startsWith(\"sharp-\")))\n continue;\n\n if (/\\.d\\.(ts|cts|mts)$/.test(entry.name)) category = \"declarations\";\n else if (\n developmentTestPackages.has(packageName) &&\n (parts.some((part) =>\n [\"test\", \"tests\", \"__tests__\", \"__fixtures__\", \"fixtures\"].includes(\n part,\n ),\n ) ||\n /\\.test\\./.test(entry.name))\n )\n category = \"tests\";\n else if (relative.includes(\"/zod/src/\")) category = \"zod-source\";\n else if (\n relative.startsWith(\n \"node_modules/@modelcontextprotocol/sdk/dist/cjs/\",\n )\n )\n category = \"mcp-cjs\";\n else if (relative.startsWith(\"node_modules/hono/dist/cjs/\"))\n category = \"hono-cjs\";\n }\n\n if (!category) continue;\n const { size } = await lstat(file);\n await unlink(file);\n removedBytes[category] = (removedBytes[category] ?? 0) + size;\n }\n }\n\n await visit(root);\n\n return removedBytes;\n}\n", + "headText": "import { lstat, readdir, unlink } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nconst developmentTestPackages = new Set([\n \"@agentclientprotocol/claude-agent-acp\",\n \"@agentclientprotocol/sdk\",\n \"@pinojs/redact\",\n \"call-bind-apply-helpers\",\n \"call-bound\",\n \"dunder-proto\",\n \"es-define-property\",\n \"es-errors\",\n \"es-object-atoms\",\n \"fast-uri\",\n \"function-bind\",\n \"get-intrinsic\",\n \"get-proto\",\n \"gopd\",\n \"has-symbols\",\n \"isexe\",\n \"json-schema-traverse\",\n \"math-intrinsics\",\n \"object-inspect\",\n \"on-exit-leak-free\",\n \"pi-acp\",\n \"pino\",\n \"pino-abstract-transport\",\n \"pino-std-serializers\",\n \"process-warning\",\n \"qs\",\n \"quick-format-unescaped\",\n \"setprototypeof\",\n \"side-channel\",\n \"side-channel-list\",\n \"side-channel-map\",\n \"side-channel-weakmap\",\n \"sonic-boom\",\n \"thread-stream\",\n \"zod\",\n]);\n\nexport async function pruneReviewRuntime(root) {\n const removedBytes = {};\n\n async function visit(directory) {\n for (const entry of await readdir(directory, { withFileTypes: true })) {\n const file = path.join(directory, entry.name);\n\n if (entry.isSymbolicLink()) continue;\n\n if (entry.isDirectory()) {\n await visit(file);\n continue;\n }\n\n if (!entry.isFile()) continue;\n const relative = path.relative(root, file).split(path.sep).join(\"/\");\n const parts = relative.split(\"/\");\n\n if (\n entry.name.endsWith(\".map\") ||\n /^(license|notice|copying)/i.test(entry.name)\n )\n continue;\n const packageParts = parts.slice(parts.lastIndexOf(\"node_modules\") + 1);\n\n const packageName = packageParts[0].startsWith(\"@\")\n ? packageParts.slice(0, 2).join(\"/\")\n : packageParts[0];\n\n let category;\n\n if (relative.startsWith(\"src/\")) category = \"own-source\";\n else if (relative.startsWith(\"node_modules/\")) {\n if (parts.some((part) => part === \"sharp\" || part.startsWith(\"sharp-\")))\n continue;\n\n if (/\\.d\\.(ts|cts|mts)$/.test(entry.name)) category = \"declarations\";\n else if (\n developmentTestPackages.has(packageName) &&\n (parts.some((part) =>\n [\"test\", \"tests\", \"__tests__\", \"__fixtures__\", \"fixtures\"].includes(\n part,\n ),\n ) ||\n /\\.test\\./.test(entry.name))\n )\n category = \"tests\";\n else if (relative.includes(\"/zod/src/\")) category = \"zod-source\";\n else if (\n relative.startsWith(\"node_modules/@modelcontextprotocol/\") &&\n entry.name.endsWith(\".cjs\")\n )\n category = \"mcp-cjs\";\n else if (relative.startsWith(\"node_modules/hono/dist/cjs/\"))\n category = \"hono-cjs\";\n }\n\n if (!category) continue;\n const { size } = await lstat(file);\n await unlink(file);\n removedBytes[category] = (removedBytes[category] ?? 0) + size;\n }\n }\n\n await visit(root);\n\n return removedBytes;\n}\n", + "patch": "diff --git a/apps/review-desktop/scripts/prune-review-runtime.mjs b/apps/review-desktop/scripts/prune-review-runtime.mjs\nindex b3ea492b4..68af321fd 100644\n--- a/apps/review-desktop/scripts/prune-review-runtime.mjs\n+++ b/apps/review-desktop/scripts/prune-review-runtime.mjs\n@@ -88,9 +88,8 @@ export async function pruneReviewRuntime(root) {\n category = \"tests\";\n else if (relative.includes(\"/zod/src/\")) category = \"zod-source\";\n else if (\n- relative.startsWith(\n- \"node_modules/@modelcontextprotocol/sdk/dist/cjs/\",\n- )\n+ relative.startsWith(\"node_modules/@modelcontextprotocol/\") &&\n+ entry.name.endsWith(\".cjs\")\n )\n category = \"mcp-cjs\";\n else if (relative.startsWith(\"node_modules/hono/dist/cjs/\"))\n" + }, + { + "name": "net-delete", + "base": "c0b71cdea1de068ac2a3df743dafdf23af880082", + "head": "e374e51df42c682a8641e90df33fd27b4030b211", + "file": "apps/review-desktop/code-oss/src/vs/review/common/reviewConfiguration.ts", + "baseText": "/*---------------------------------------------------------------------------------------------\n * Copyright (c) dev.fast. All rights reserved.\n * Licensed under the MIT License. See LICENSE in the repository root for license information.\n *--------------------------------------------------------------------------------------------*/\n\n/**\n * Registers Review's configuration with the workbench.\n *\n * Importing this module pulls in the configuration registry, which constructs\n * itself \u2014 and localizes \u2014 at module scope. That makes it unsafe anywhere the\n * Electron main process can reach before `bootstrapESM()`; see the header of\n * `reviewConfigurationDefaults.ts`. Renderer code reaches it through the bare\n * side-effect import in `review.common.main.ts`.\n *\n * The setting keys and default values live in `reviewConfigurationDefaults.ts`\n * and are deliberately *not* re-exported from here: a re-export would let a\n * main-process consumer import data through this module and put the registry\n * back on its import path.\n */\nimport { localize } from '../../nls.js';\nimport { Registry } from '../../platform/registry/common/platform.js';\nimport { ConfigurationScope, Extensions, type IConfigurationRegistry } from '../../platform/configuration/common/configurationRegistry.js';\nimport { REVIEW_DIAGRAM_FULLSCREEN_ON_CLICK_SETTING, REVIEW_DIFF_COLOR_THEME_SETTING, REVIEW_INLINE_LINE_NUMBERS_SETTING, DIFF_EDITOR_FONT_SIZE_SETTING, REVIEW_DOCUMENT_FONT_SIZE_BASE, REVIEW_DOCUMENT_FONT_SIZE_SETTING, REVIEW_DOCUMENT_WIDTH_CHOICES, REVIEW_DOCUMENT_WIDTH_SETTING, REVIEW_KEYMAPS, REVIEW_KEYMAP_SETTING, REVIEW_OPEN_FILES_IN_APPLICATION_SETTING, REVIEW_OPEN_FILES_IN_CHOICES, REVIEW_OPEN_FILES_IN_SETTING, REVIEW_PEEK_SIDE_BY_SIDE_SETTING, REVIEW_READING_FONTS, REVIEW_READING_FONT_FAMILY_SETTING, REVIEW_READING_FONT_SETTING, REVIEW_READY_NOTIFICATION_CHOICES, REVIEW_READY_NOTIFICATION_SETTING, REVIEW_REMOTE_HOSTS_SETTING, REVIEW_REMOTE_INSTALL_MODES, REVIEW_REMOTE_INSTALL_SETTING, REVIEW_SHARING_SETTING, REVIEW_SOFTWARE_MAP_SETTING, REVIEW_TELEMETRY_SETTING, curatedExtensionConfigurationDefaults, reviewConfigurationDefaults } from './reviewConfigurationDefaults.js';\n\nconst configurationRegistry = Registry.as(Extensions.Configuration);\n\nconfigurationRegistry.registerConfiguration({\n\tid: 'review',\n\ttitle: localize('reviewConfigurationTitle', \"Whiteboard\"),\n\ttype: 'object',\n\tscope: ConfigurationScope.APPLICATION,\n\tproperties: {\n\t\t[REVIEW_KEYMAP_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_KEYMAPS],\n\t\t\tdefault: 'none',\n\t\t\tdescription: localize('review.keymap', \"Select the curated keymap extension Whiteboard enables.\"),\n\t\t},\n\t\t[REVIEW_OPEN_FILES_IN_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_OPEN_FILES_IN_CHOICES],\n\t\t\tenumDescriptions: [\n\t\t\t\tlocalize('review.openFilesIn.whiteboard', \"Open files in Whiteboard's source window.\"),\n\t\t\t\tlocalize('review.openFilesIn.vscode', \"Open files in Visual Studio Code.\"),\n\t\t\t\tlocalize('review.openFilesIn.cursor', \"Open files in Cursor.\"),\n\t\t\t\tlocalize('review.openFilesIn.zed', \"Open files in Zed.\"),\n\t\t\t\tlocalize('review.openFilesIn.application', \"Open files in the application set in `#review.openFilesInApplication#`, without a line.\"),\n\t\t\t],\n\t\t\tdefault: 'whiteboard',\n\t\t\tdescription: localize('review.openFilesIn', \"Where Open file sends a file. A file at a pinned revision opens from the copy Whiteboard keeps for the review. Diffs always open in Whiteboard.\"),\n\t\t},\n\t\t[REVIEW_OPEN_FILES_IN_APPLICATION_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tdefault: '',\n\t\t\t// Names a program to run, as `workbench.externalBrowser` does.\n\t\t\trestricted: true,\n\t\t\tdescription: localize('review.openFilesInApplication', \"The application Open file uses when `#review.openFilesIn#` is `application`: an `.app` on macOS, or a program elsewhere.\"),\n\t\t},\n\t\t[REVIEW_DIAGRAM_FULLSCREEN_ON_CLICK_SETTING]: {\n\t\t\ttype: 'boolean',\n\t\t\tdefault: false,\n\t\t\tdescription: localize('review.diagrams.fullscreenOnClick', \"Open inline diagrams fullscreen when selecting a node. When off, select the node and open its sidebar.\"),\n\t\t},\n\t\t[REVIEW_DOCUMENT_WIDTH_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_DOCUMENT_WIDTH_CHOICES],\n\t\t\tenumDescriptions: [\n\t\t\t\tlocalize('review.documentWidth.standard', \"A reading-width column.\"),\n\t\t\t\tlocalize('review.documentWidth.wide', \"More room for diagrams and code.\"),\n\t\t\t\tlocalize('review.documentWidth.full', \"Diagrams and code use the full width; text keeps a reading measure.\"),\n\t\t\t],\n\t\t\tdefault: 'standard',\n\t\t\tdescription: localize('review.documentWidth', \"Width of the whiteboard document.\"),\n\t\t},\n\t\t[REVIEW_DIFF_COLOR_THEME_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tdefault: '',\n\t\t\tdescription: localize('review.diffColorTheme', \"The color theme for backgrounds and change colors in the Diff tab. Empty, or a theme not as light or dark as `#workbench.colorTheme#`, follows that theme; syntax colors always do.\"),\n\t\t},\n\t\t[REVIEW_DOCUMENT_FONT_SIZE_SETTING]: {\n\t\t\ttype: 'number',\n\t\t\tminimum: 10,\n\t\t\tmaximum: 32,\n\t\t\tdefault: REVIEW_DOCUMENT_FONT_SIZE_BASE,\n\t\t\tdescription: localize('review.document.fontSize', \"Paragraph size on whiteboards, in pixels. Headings scale with it.\"),\n\t\t},\n\t\t[REVIEW_READING_FONT_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_READING_FONTS],\n\t\t\tenumDescriptions: [\n\t\t\t\tlocalize('review.readingFont.serif', \"Newsreader.\"),\n\t\t\t\tlocalize('review.readingFont.sans', \"Inter, the interface face.\"),\n\t\t\t],\n\t\t\tdefault: 'serif',\n\t\t\tdescription: localize('review.readingFont', \"The face for whiteboard prose, Ask answers and trace notes.\"),\n\t\t},\n\t\t[REVIEW_READING_FONT_FAMILY_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tdefault: '',\n\t\t\tdescription: localize('review.readingFontFamily', \"A font family for whiteboard prose, Ask answers and trace notes. When set, it replaces the reading typeface.\"),\n\t\t},\n\t\t// Diffs merge `diffEditor.*` over `editor.*`.\n\t\t[DIFF_EDITOR_FONT_SIZE_SETTING]: {\n\t\t\ttype: 'number',\n\t\t\tminimum: 6,\n\t\t\tmaximum: 100,\n\t\t\tdefault: 12,\n\t\t\tdescription: localize('diffEditor.fontSize', \"Font size in the Diff tab, diff blocks and code peeks, in pixels.\"),\n\t\t},\n\t\t[REVIEW_INLINE_LINE_NUMBERS_SETTING]: {\n type: 'boolean',\n default: false,\n description: localize('review.inlineDiffs.showLineNumbers', \"Show line numbers in Whiteboard document diffs and code peeks. The Diff view keeps its own line numbers.\"),\n },\n\t\t[REVIEW_PEEK_SIDE_BY_SIDE_SETTING]: {\n\t\t\ttype: 'boolean',\n\t\t\tdefault: false,\n\t\t\tdescription: localize('review.peeks.renderSideBySide', \"Show code peeks side by side. When off, code peeks show a unified diff.\"),\n\t\t},\n\t\t[REVIEW_READY_NOTIFICATION_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_READY_NOTIFICATION_CHOICES],\n\t\t\tenumDescriptions: [\n\t\t\t\tlocalize('review.notifications.reviewReady.notificationAndBadge', \"Show a notification and badge the Dock icon.\"),\n\t\t\t\tlocalize('review.notifications.reviewReady.notification', \"Show a notification only.\"),\n\t\t\t\tlocalize('review.notifications.reviewReady.off', \"Don't notify.\"),\n\t\t\t],\n\t\t\tdefault: 'off',\n\t\t\tdescription: localize('review.notifications.reviewReady', \"How Whiteboard tells you an agent finished a review.\"),\n\t\t},\n\t\t[REVIEW_TELEMETRY_SETTING]: {\n\t\t\ttype: 'boolean',\n\t\t\tdefault: true,\n\t\t\tdescription: localize('review.telemetry.enabled', \"Send anonymous Whiteboard usage data.\"),\n\t\t},\n\t\t[REVIEW_SOFTWARE_MAP_SETTING]: {\n\t\t\ttype: 'boolean',\n\t\t\tdefault: false,\n\t\t\tdescription: localize('review.experimental.softwareMap.enabled', \"Show the experimental Software Map view in sessions.\"),\n\t\t},\n\t\t[REVIEW_SHARING_SETTING]: {\n\t\t\ttype: 'boolean',\n\t\t\tdefault: false,\n\t\t\tdescription: localize('review.experimental.sharing.enabled', \"Show Share on whiteboards.\"),\n\t\t},\n\t\t[REVIEW_REMOTE_HOSTS_SETTING]: {\n\t\t\ttype: 'array',\n\t\t\titems: { type: 'string' },\n\t\t\tdefault: [],\n\t\t\tscope: ConfigurationScope.APPLICATION,\n\t\t\tdescription: localize('review.remote.hosts', \"SSH host aliases, from your SSH configuration, whose Whiteboard reviews to show.\"),\n\t\t},\n\t\t[REVIEW_REMOTE_INSTALL_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_REMOTE_INSTALL_MODES],\n\t\t\tenumDescriptions: [\n\t\t\t\tlocalize('review.remote.install.ask', \"Ask once per host before installing.\"),\n\t\t\t\tlocalize('review.remote.install.always', \"Install without asking.\"),\n\t\t\t\tlocalize('review.remote.install.never', \"Never install; use the Whiteboard you installed on the host.\"),\n\t\t\t],\n\t\t\tdefault: 'ask',\n\t\t\tscope: ConfigurationScope.APPLICATION,\n\t\t\tdescription: localize('review.remote.install', \"Whether Whiteboard installs its own version on a remote host, under ~/.dev/whiteboard-remote, when the host does not have it.\"),\n\t\t},\n\t},\n});\n\nconfigurationRegistry.registerDefaultConfigurations([\n\t{ overrides: reviewConfigurationDefaults },\n\t{ overrides: curatedExtensionConfigurationDefaults }\n]);\n", + "headText": "/*---------------------------------------------------------------------------------------------\n * Copyright (c) dev.fast. All rights reserved.\n * Licensed under the MIT License. See LICENSE in the repository root for license information.\n *--------------------------------------------------------------------------------------------*/\n\n/**\n * Registers Review's configuration with the workbench.\n *\n * Importing this module pulls in the configuration registry, which constructs\n * itself \u2014 and localizes \u2014 at module scope. That makes it unsafe anywhere the\n * Electron main process can reach before `bootstrapESM()`; see the header of\n * `reviewConfigurationDefaults.ts`. Renderer code reaches it through the bare\n * side-effect import in `review.common.main.ts`.\n *\n * The setting keys and default values live in `reviewConfigurationDefaults.ts`\n * and are deliberately *not* re-exported from here: a re-export would let a\n * main-process consumer import data through this module and put the registry\n * back on its import path.\n */\nimport { localize } from '../../nls.js';\nimport { Registry } from '../../platform/registry/common/platform.js';\nimport { ConfigurationScope, Extensions, type IConfigurationRegistry } from '../../platform/configuration/common/configurationRegistry.js';\nimport { REVIEW_DIAGRAM_FULLSCREEN_ON_CLICK_SETTING, REVIEW_DIFF_COLOR_THEME_SETTING, REVIEW_INLINE_LINE_NUMBERS_SETTING, DIFF_EDITOR_FONT_SIZE_SETTING, REVIEW_DOCUMENT_FONT_SIZE_BASE, REVIEW_DOCUMENT_FONT_SIZE_SETTING, REVIEW_DOCUMENT_WIDTH_CHOICES, REVIEW_DOCUMENT_WIDTH_SETTING, REVIEW_KEYMAPS, REVIEW_KEYMAP_SETTING, REVIEW_OPEN_FILES_IN_APPLICATION_SETTING, REVIEW_OPEN_FILES_IN_CHOICES, REVIEW_OPEN_FILES_IN_SETTING, REVIEW_PEEK_SIDE_BY_SIDE_SETTING, REVIEW_READING_FONTS, REVIEW_READING_FONT_FAMILY_SETTING, REVIEW_READING_FONT_SETTING, REVIEW_READY_NOTIFICATION_CHOICES, REVIEW_READY_NOTIFICATION_SETTING, REVIEW_REMOTE_HOSTS_SETTING, REVIEW_REMOTE_INSTALL_MODES, REVIEW_REMOTE_INSTALL_SETTING, REVIEW_SOFTWARE_MAP_SETTING, REVIEW_TELEMETRY_SETTING, curatedExtensionConfigurationDefaults, reviewConfigurationDefaults } from './reviewConfigurationDefaults.js';\n\nconst configurationRegistry = Registry.as(Extensions.Configuration);\n\nconfigurationRegistry.registerConfiguration({\n\tid: 'review',\n\ttitle: localize('reviewConfigurationTitle', \"Whiteboard\"),\n\ttype: 'object',\n\tscope: ConfigurationScope.APPLICATION,\n\tproperties: {\n\t\t[REVIEW_KEYMAP_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_KEYMAPS],\n\t\t\tdefault: 'none',\n\t\t\tdescription: localize('review.keymap', \"Select the curated keymap extension Whiteboard enables.\"),\n\t\t},\n\t\t[REVIEW_OPEN_FILES_IN_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_OPEN_FILES_IN_CHOICES],\n\t\t\tenumDescriptions: [\n\t\t\t\tlocalize('review.openFilesIn.whiteboard', \"Open files in Whiteboard's source window.\"),\n\t\t\t\tlocalize('review.openFilesIn.vscode', \"Open files in Visual Studio Code.\"),\n\t\t\t\tlocalize('review.openFilesIn.cursor', \"Open files in Cursor.\"),\n\t\t\t\tlocalize('review.openFilesIn.zed', \"Open files in Zed.\"),\n\t\t\t\tlocalize('review.openFilesIn.application', \"Open files in the application set in `#review.openFilesInApplication#`, without a line.\"),\n\t\t\t],\n\t\t\tdefault: 'whiteboard',\n\t\t\tdescription: localize('review.openFilesIn', \"Where Open file sends a file. A file at a pinned revision opens from the copy Whiteboard keeps for the review. Diffs always open in Whiteboard.\"),\n\t\t},\n\t\t[REVIEW_OPEN_FILES_IN_APPLICATION_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tdefault: '',\n\t\t\t// Names a program to run, as `workbench.externalBrowser` does.\n\t\t\trestricted: true,\n\t\t\tdescription: localize('review.openFilesInApplication', \"The application Open file uses when `#review.openFilesIn#` is `application`: an `.app` on macOS, or a program elsewhere.\"),\n\t\t},\n\t\t[REVIEW_DIAGRAM_FULLSCREEN_ON_CLICK_SETTING]: {\n\t\t\ttype: 'boolean',\n\t\t\tdefault: false,\n\t\t\tdescription: localize('review.diagrams.fullscreenOnClick', \"Open inline diagrams fullscreen when selecting a node. When off, select the node and open its sidebar.\"),\n\t\t},\n\t\t[REVIEW_DOCUMENT_WIDTH_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_DOCUMENT_WIDTH_CHOICES],\n\t\t\tenumDescriptions: [\n\t\t\t\tlocalize('review.documentWidth.standard', \"A reading-width column.\"),\n\t\t\t\tlocalize('review.documentWidth.wide', \"More room for diagrams and code.\"),\n\t\t\t\tlocalize('review.documentWidth.full', \"Diagrams and code use the full width; text keeps a reading measure.\"),\n\t\t\t],\n\t\t\tdefault: 'standard',\n\t\t\tdescription: localize('review.documentWidth', \"Width of the whiteboard document.\"),\n\t\t},\n\t\t[REVIEW_DIFF_COLOR_THEME_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tdefault: '',\n\t\t\tdescription: localize('review.diffColorTheme', \"The color theme for backgrounds and change colors in the Diff tab. Empty, or a theme not as light or dark as `#workbench.colorTheme#`, follows that theme; syntax colors always do.\"),\n\t\t},\n\t\t[REVIEW_DOCUMENT_FONT_SIZE_SETTING]: {\n\t\t\ttype: 'number',\n\t\t\tminimum: 10,\n\t\t\tmaximum: 32,\n\t\t\tdefault: REVIEW_DOCUMENT_FONT_SIZE_BASE,\n\t\t\tdescription: localize('review.document.fontSize', \"Paragraph size on whiteboards, in pixels. Headings scale with it.\"),\n\t\t},\n\t\t[REVIEW_READING_FONT_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_READING_FONTS],\n\t\t\tenumDescriptions: [\n\t\t\t\tlocalize('review.readingFont.serif', \"Newsreader.\"),\n\t\t\t\tlocalize('review.readingFont.sans', \"Inter, the interface face.\"),\n\t\t\t],\n\t\t\tdefault: 'serif',\n\t\t\tdescription: localize('review.readingFont', \"The face for whiteboard prose, Ask answers and trace notes.\"),\n\t\t},\n\t\t[REVIEW_READING_FONT_FAMILY_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tdefault: '',\n\t\t\tdescription: localize('review.readingFontFamily', \"A font family for whiteboard prose, Ask answers and trace notes. When set, it replaces the reading typeface.\"),\n\t\t},\n\t\t// Diffs merge `diffEditor.*` over `editor.*`.\n\t\t[DIFF_EDITOR_FONT_SIZE_SETTING]: {\n\t\t\ttype: 'number',\n\t\t\tminimum: 6,\n\t\t\tmaximum: 100,\n\t\t\tdefault: 12,\n\t\t\tdescription: localize('diffEditor.fontSize', \"Font size in the Diff tab, diff blocks and code peeks, in pixels.\"),\n\t\t},\n\t\t[REVIEW_INLINE_LINE_NUMBERS_SETTING]: {\n type: 'boolean',\n default: false,\n description: localize('review.inlineDiffs.showLineNumbers', \"Show line numbers in Whiteboard document diffs and code peeks. The Diff view keeps its own line numbers.\"),\n },\n\t\t[REVIEW_PEEK_SIDE_BY_SIDE_SETTING]: {\n\t\t\ttype: 'boolean',\n\t\t\tdefault: false,\n\t\t\tdescription: localize('review.peeks.renderSideBySide', \"Show code peeks side by side. When off, code peeks show a unified diff.\"),\n\t\t},\n\t\t[REVIEW_READY_NOTIFICATION_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_READY_NOTIFICATION_CHOICES],\n\t\t\tenumDescriptions: [\n\t\t\t\tlocalize('review.notifications.reviewReady.notificationAndBadge', \"Show a notification and badge the Dock icon.\"),\n\t\t\t\tlocalize('review.notifications.reviewReady.notification', \"Show a notification only.\"),\n\t\t\t\tlocalize('review.notifications.reviewReady.off', \"Don't notify.\"),\n\t\t\t],\n\t\t\tdefault: 'off',\n\t\t\tdescription: localize('review.notifications.reviewReady', \"How Whiteboard tells you an agent finished a review.\"),\n\t\t},\n\t\t[REVIEW_TELEMETRY_SETTING]: {\n\t\t\ttype: 'boolean',\n\t\t\tdefault: true,\n\t\t\tdescription: localize('review.telemetry.enabled', \"Send anonymous Whiteboard usage data.\"),\n\t\t},\n\t\t[REVIEW_SOFTWARE_MAP_SETTING]: {\n\t\t\ttype: 'boolean',\n\t\t\tdefault: false,\n\t\t\tdescription: localize('review.experimental.softwareMap.enabled', \"Show the experimental Software Map view in sessions.\"),\n\t\t},\n\t\t[REVIEW_REMOTE_HOSTS_SETTING]: {\n\t\t\ttype: 'array',\n\t\t\titems: { type: 'string' },\n\t\t\tdefault: [],\n\t\t\tscope: ConfigurationScope.APPLICATION,\n\t\t\tdescription: localize('review.remote.hosts', \"SSH host aliases, from your SSH configuration, whose Whiteboard reviews to show.\"),\n\t\t},\n\t\t[REVIEW_REMOTE_INSTALL_SETTING]: {\n\t\t\ttype: 'string',\n\t\t\tenum: [...REVIEW_REMOTE_INSTALL_MODES],\n\t\t\tenumDescriptions: [\n\t\t\t\tlocalize('review.remote.install.ask', \"Ask once per host before installing.\"),\n\t\t\t\tlocalize('review.remote.install.always', \"Install without asking.\"),\n\t\t\t\tlocalize('review.remote.install.never', \"Never install; use the Whiteboard you installed on the host.\"),\n\t\t\t],\n\t\t\tdefault: 'ask',\n\t\t\tscope: ConfigurationScope.APPLICATION,\n\t\t\tdescription: localize('review.remote.install', \"Whether Whiteboard installs its own version on a remote host, under ~/.dev/whiteboard-remote, when the host does not have it.\"),\n\t\t},\n\t},\n});\n\nconfigurationRegistry.registerDefaultConfigurations([\n\t{ overrides: reviewConfigurationDefaults },\n\t{ overrides: curatedExtensionConfigurationDefaults }\n]);\n", + "patch": "diff --git a/apps/review-desktop/code-oss/src/vs/review/common/reviewConfiguration.ts b/apps/review-desktop/code-oss/src/vs/review/common/reviewConfiguration.ts\nindex 84fa47c8a..09431c290 100644\n--- a/apps/review-desktop/code-oss/src/vs/review/common/reviewConfiguration.ts\n+++ b/apps/review-desktop/code-oss/src/vs/review/common/reviewConfiguration.ts\n@@ -20,7 +20,7 @@\n import { localize } from '../../nls.js';\n import { Registry } from '../../platform/registry/common/platform.js';\n import { ConfigurationScope, Extensions, type IConfigurationRegistry } from '../../platform/configuration/common/configurationRegistry.js';\n-import { REVIEW_DIAGRAM_FULLSCREEN_ON_CLICK_SETTING, REVIEW_DIFF_COLOR_THEME_SETTING, REVIEW_INLINE_LINE_NUMBERS_SETTING, DIFF_EDITOR_FONT_SIZE_SETTING, REVIEW_DOCUMENT_FONT_SIZE_BASE, REVIEW_DOCUMENT_FONT_SIZE_SETTING, REVIEW_DOCUMENT_WIDTH_CHOICES, REVIEW_DOCUMENT_WIDTH_SETTING, REVIEW_KEYMAPS, REVIEW_KEYMAP_SETTING, REVIEW_OPEN_FILES_IN_APPLICATION_SETTING, REVIEW_OPEN_FILES_IN_CHOICES, REVIEW_OPEN_FILES_IN_SETTING, REVIEW_PEEK_SIDE_BY_SIDE_SETTING, REVIEW_READING_FONTS, REVIEW_READING_FONT_FAMILY_SETTING, REVIEW_READING_FONT_SETTING, REVIEW_READY_NOTIFICATION_CHOICES, REVIEW_READY_NOTIFICATION_SETTING, REVIEW_REMOTE_HOSTS_SETTING, REVIEW_REMOTE_INSTALL_MODES, REVIEW_REMOTE_INSTALL_SETTING, REVIEW_SHARING_SETTING, REVIEW_SOFTWARE_MAP_SETTING, REVIEW_TELEMETRY_SETTING, curatedExtensionConfigurationDefaults, reviewConfigurationDefaults } from './reviewConfigurationDefaults.js';\n+import { REVIEW_DIAGRAM_FULLSCREEN_ON_CLICK_SETTING, REVIEW_DIFF_COLOR_THEME_SETTING, REVIEW_INLINE_LINE_NUMBERS_SETTING, DIFF_EDITOR_FONT_SIZE_SETTING, REVIEW_DOCUMENT_FONT_SIZE_BASE, REVIEW_DOCUMENT_FONT_SIZE_SETTING, REVIEW_DOCUMENT_WIDTH_CHOICES, REVIEW_DOCUMENT_WIDTH_SETTING, REVIEW_KEYMAPS, REVIEW_KEYMAP_SETTING, REVIEW_OPEN_FILES_IN_APPLICATION_SETTING, REVIEW_OPEN_FILES_IN_CHOICES, REVIEW_OPEN_FILES_IN_SETTING, REVIEW_PEEK_SIDE_BY_SIDE_SETTING, REVIEW_READING_FONTS, REVIEW_READING_FONT_FAMILY_SETTING, REVIEW_READING_FONT_SETTING, REVIEW_READY_NOTIFICATION_CHOICES, REVIEW_READY_NOTIFICATION_SETTING, REVIEW_REMOTE_HOSTS_SETTING, REVIEW_REMOTE_INSTALL_MODES, REVIEW_REMOTE_INSTALL_SETTING, REVIEW_SOFTWARE_MAP_SETTING, REVIEW_TELEMETRY_SETTING, curatedExtensionConfigurationDefaults, reviewConfigurationDefaults } from './reviewConfigurationDefaults.js';\n \n const configurationRegistry = Registry.as(Extensions.Configuration);\n \n@@ -138,11 +138,6 @@ configurationRegistry.registerConfiguration({\n \t\t\tdefault: false,\n \t\t\tdescription: localize('review.experimental.softwareMap.enabled', \"Show the experimental Software Map view in sessions.\"),\n \t\t},\n-\t\t[REVIEW_SHARING_SETTING]: {\n-\t\t\ttype: 'boolean',\n-\t\t\tdefault: false,\n-\t\t\tdescription: localize('review.experimental.sharing.enabled', \"Show Share on whiteboards.\"),\n-\t\t},\n \t\t[REVIEW_REMOTE_HOSTS_SETTING]: {\n \t\t\ttype: 'array',\n \t\t\titems: { type: 'string' },\n" + }, + { + "name": "rename", + "base": "3e31ee30c2635fdd1ab1259a2bbc460f7a3e5660", + "head": "35b9fc054890b44625ad06cbe531416da01092ff", + "file": "packages/whiteboard-document/src/blocks/code_peek.ts", + "previousFile": "packages/review/src/review-api/blocks/code_peek.ts", + "baseText": "import { anchorSchema } from \"@review/lens-selection.js\";\n\nimport { defineBlock, elementPins, text } from \"./definition.js\";\n\nexport const code_peek = {\n type: \"code_peek\",\n schema: defineBlock(\"code_peek\", {\n source: anchorSchema,\n pins: elementPins,\n // Not rendered yet.\n caption: text.optional(),\n }),\n} as const;\n", + "headText": "import { anchorSchema } from \"../lens-selection.js\";\nimport { defineBlock, elementPins, text } from \"./definition.js\";\n\nexport const code_peek = {\n type: \"code_peek\",\n schema: defineBlock(\"code_peek\", {\n source: anchorSchema,\n pins: elementPins,\n // Not rendered yet.\n caption: text.optional(),\n }),\n} as const;\n", + "patch": "diff --git a/packages/review/src/review-api/blocks/code_peek.ts b/packages/whiteboard-document/src/blocks/code_peek.ts\nsimilarity index 82%\nrename from packages/review/src/review-api/blocks/code_peek.ts\nrename to packages/whiteboard-document/src/blocks/code_peek.ts\nindex a46b61315..ce998ea26 100644\n--- a/packages/review/src/review-api/blocks/code_peek.ts\n+++ b/packages/whiteboard-document/src/blocks/code_peek.ts\n@@ -1,5 +1,4 @@\n-import { anchorSchema } from \"@review/lens-selection.js\";\n-\n+import { anchorSchema } from \"../lens-selection.js\";\n import { defineBlock, elementPins, text } from \"./definition.js\";\n \n export const code_peek = {\n" + }, + { + "name": "deleted-file", + "base": "197ef526f8e320f501482be233d26efd4659e4ed", + "head": "080f292b14921d66f7f98f5a9c57cb4efefba134", + "file": "packages/review/src/review-api/component-reference.ts", + "baseText": "/** The animation component, shown only while Animations are on. */\nconst ANIMATION = [\n '- animation {title, html, frames: {key, title, label?, caption?, duration? (ms)}[] (1 to 60), code?: {name: anchor}, height?: \"auto\" (default) | number (120 to 1200 CSS px), pins?}; declare every possible frame',\n 'Use an animation for change over time that a static diagram cannot show: state before and after, a cache filling, data changing as a request crosses layers. It can sit in design beside the diagram, or replace it when the motion is the point. If nothing changes between frames, use a sequence or flow_diagram instead: their steps and nodes link to code. Read session_get_instructions({topic:\"animation\"}) before writing one.',\n];\n\n/** Every component's fields, for session_get_instructions: the edit tool's\n * description must stay short enough that clients don't cut it off. */\nexport const COMPONENT_REFERENCE = [\n \"## Components\",\n \"\",\n \"session_edit's content is one component, named by its type. Fields, with ? for optional and anchor for a source anchor string:\",\n \"\",\n \"- markdown {markdown, pins?}\",\n \"- code {text, language?, caption?}\",\n \"- divider {}\",\n \"- code_peek {source: anchor, caption?, pins?}\",\n \"- section {title, children: component[], defaultCollapsed?}\",\n \"- callout {children: component[], title?, tone?: info|warning|danger|success}\",\n \"- image {assetId from session_upload, alt, caption?}\",\n \"- trace_quote {traceId, eventId, text}\",\n \"- software_map {mapVersionId, focusElementId?}\",\n \"- sequence {title, actors: {key: label}, steps: step[], pins?}\",\n \"- step {from, to (actor keys), label, style?: call|return|async, and exactly one of source: anchor or anchor[] (at most 4), explanation, or code: {text, language?}; pins?}\",\n \"A step's source is the line where its arrow leaves `from` (the call, return or emit). Add an additional range only when that line can't be read without it: the type or schema of what it passes, or the code that builds its argument. Never the callee's body; that is the next step.\",\n \"- flow_diagram {title, nodes: flow_node[] (1 to 100), edges: flow_edge[], description?, direction?: right|down, pins?}\",\n \"- flow_node {key, label, description?, kind?: process|decision|terminal, attachments?: {label, sources: anchor[], pins?}[], link?: {from or to, label?, style?}}\",\n \"- flow_edge {from, to (node keys), label?, style?: solid|dashed}\",\n \"- call_stack_diff {title, base: frame[], head: frame[], pins?}; either side may be empty\",\n \"- frame {source: anchor, label?, key?, parentKey? (an earlier frame on that side), callSite?: anchor, contextSources?: anchor[], via?: {kind: call|queue|callback|rpc, reason}, pins?}\",\n \"- database_lens {title, actors: {key: label}, stores: {key: store}, useCases: useCase[] (1 or more), pins?}\",\n \"- store {label, storage: relational|document, collections: {key: {label, fields: {key: field}, source?: anchor of its schema declaration}}, dataStoreKind?: database|objectStore|bucket|artifactStore|fileStore}\",\n \"- field {label, dataType, nullable?, primaryKey?, references?: {store, collection, field}, fields?: {key: field}}\",\n \"- useCase {label, summary?, operations: operation[] (1 or more)}; operation {kind: read|write, actor, store, collection, label, source: anchor, field?, detail?, pins?}, naming declared keys\",\n ...ANIMATION,\n \"\",\n 'Example (replace its path and lines with verified ones): {\"type\":\"sequence\",\"title\":\"Request\",\"actors\":{\"a\":\"Client\",\"b\":\"Server\"},\"steps\":[{\"from\":\"a\",\"to\":\"b\",\"label\":\"Send\",\"source\":\"head/src/app.ts#L10-L20\"}]}',\n].join(\"\\n\");\n\n/** Keep disabled experimental components out of agent guidance. */\nexport function componentReference(animationsEnabled: boolean): string {\n return animationsEnabled\n ? COMPONENT_REFERENCE\n : COMPONENT_REFERENCE.split(\"\\n\")\n .filter((line) => !ANIMATION.includes(line))\n .join(\"\\n\");\n}\n", + "headText": null, + "patch": "diff --git a/packages/review/src/review-api/component-reference.ts b/packages/review/src/review-api/component-reference.ts\ndeleted file mode 100644\nindex 19af9649f..000000000\n--- a/packages/review/src/review-api/component-reference.ts\n+++ /dev/null\n@@ -1,47 +0,0 @@\n-/** The animation component, shown only while Animations are on. */\n-const ANIMATION = [\n- '- animation {title, html, frames: {key, title, label?, caption?, duration? (ms)}[] (1 to 60), code?: {name: anchor}, height?: \"auto\" (default) | number (120 to 1200 CSS px), pins?}; declare every possible frame',\n- 'Use an animation for change over time that a static diagram cannot show: state before and after, a cache filling, data changing as a request crosses layers. It can sit in design beside the diagram, or replace it when the motion is the point. If nothing changes between frames, use a sequence or flow_diagram instead: their steps and nodes link to code. Read session_get_instructions({topic:\"animation\"}) before writing one.',\n-];\n-\n-/** Every component's fields, for session_get_instructions: the edit tool's\n- * description must stay short enough that clients don't cut it off. */\n-export const COMPONENT_REFERENCE = [\n- \"## Components\",\n- \"\",\n- \"session_edit's content is one component, named by its type. Fields, with ? for optional and anchor for a source anchor string:\",\n- \"\",\n- \"- markdown {markdown, pins?}\",\n- \"- code {text, language?, caption?}\",\n- \"- divider {}\",\n- \"- code_peek {source: anchor, caption?, pins?}\",\n- \"- section {title, children: component[], defaultCollapsed?}\",\n- \"- callout {children: component[], title?, tone?: info|warning|danger|success}\",\n- \"- image {assetId from session_upload, alt, caption?}\",\n- \"- trace_quote {traceId, eventId, text}\",\n- \"- software_map {mapVersionId, focusElementId?}\",\n- \"- sequence {title, actors: {key: label}, steps: step[], pins?}\",\n- \"- step {from, to (actor keys), label, style?: call|return|async, and exactly one of source: anchor or anchor[] (at most 4), explanation, or code: {text, language?}; pins?}\",\n- \"A step's source is the line where its arrow leaves `from` (the call, return or emit). Add an additional range only when that line can't be read without it: the type or schema of what it passes, or the code that builds its argument. Never the callee's body; that is the next step.\",\n- \"- flow_diagram {title, nodes: flow_node[] (1 to 100), edges: flow_edge[], description?, direction?: right|down, pins?}\",\n- \"- flow_node {key, label, description?, kind?: process|decision|terminal, attachments?: {label, sources: anchor[], pins?}[], link?: {from or to, label?, style?}}\",\n- \"- flow_edge {from, to (node keys), label?, style?: solid|dashed}\",\n- \"- call_stack_diff {title, base: frame[], head: frame[], pins?}; either side may be empty\",\n- \"- frame {source: anchor, label?, key?, parentKey? (an earlier frame on that side), callSite?: anchor, contextSources?: anchor[], via?: {kind: call|queue|callback|rpc, reason}, pins?}\",\n- \"- database_lens {title, actors: {key: label}, stores: {key: store}, useCases: useCase[] (1 or more), pins?}\",\n- \"- store {label, storage: relational|document, collections: {key: {label, fields: {key: field}, source?: anchor of its schema declaration}}, dataStoreKind?: database|objectStore|bucket|artifactStore|fileStore}\",\n- \"- field {label, dataType, nullable?, primaryKey?, references?: {store, collection, field}, fields?: {key: field}}\",\n- \"- useCase {label, summary?, operations: operation[] (1 or more)}; operation {kind: read|write, actor, store, collection, label, source: anchor, field?, detail?, pins?}, naming declared keys\",\n- ...ANIMATION,\n- \"\",\n- 'Example (replace its path and lines with verified ones): {\"type\":\"sequence\",\"title\":\"Request\",\"actors\":{\"a\":\"Client\",\"b\":\"Server\"},\"steps\":[{\"from\":\"a\",\"to\":\"b\",\"label\":\"Send\",\"source\":\"head/src/app.ts#L10-L20\"}]}',\n-].join(\"\\n\");\n-\n-/** Keep disabled experimental components out of agent guidance. */\n-export function componentReference(animationsEnabled: boolean): string {\n- return animationsEnabled\n- ? COMPONENT_REFERENCE\n- : COMPONENT_REFERENCE.split(\"\\n\")\n- .filter((line) => !ANIMATION.includes(line))\n- .join(\"\\n\");\n-}\n" + } +] diff --git a/packages/review/app/src/share/inline-editors.real-diffs.browser.test.ts b/packages/review/app/src/share/inline-editors.real-diffs.browser.test.ts new file mode 100644 index 000000000..0a22382b4 --- /dev/null +++ b/packages/review/app/src/share/inline-editors.real-diffs.browser.test.ts @@ -0,0 +1,307 @@ +import type { + ReviewDiffLayout, + ReviewInlineStaticSpec, +} from "@dev.fast/review-protocol"; +import type { + ShareAlignment, + ShareSource, +} from "@dev.fast/review-share-protocol"; +import { textualRows } from "@review/review-api/lens-alignment"; +import { type DiffHunk, parseUnifiedPatch } from "@review/unified-diff"; +import { afterEach, describe, expect, it } from "vitest"; + +import fixtures from "./__fixtures__/real-diffs.json"; +import { type PeekSettings, shareInlineEditors } from "./inline-editors"; + +/** Real commits of this repository: the patch Git wrote and both files. */ +interface Fixture { + name: string; + base: string; + head: string; + file: string; + previousFile?: string; + baseText: string | null; + headText: string | null; + patch: string; +} + +type Rendered = (string | null)[] | null; + +const settings = ( + layout: ReviewDiffLayout, + lineNumbers = true, +): PeekSettings => ({ + currentDiffLayout: () => layout, + currentInlineLineNumbers: () => lineNumbers, + onDidChangeDiffLayout: () => ({ dispose() {} }), + onDidChangeInlineLineNumbers: () => ({ dispose() {} }), +}); + +const container = document.createElement("div"); + +afterEach(() => container.replaceChildren()); + +const lines = (selector = ""): Rendered[] => + [ + ...container.querySelectorAll( + `${selector} [data-line], ${selector} [data-filler]`, + ), + ].map((row) => + row.hasAttribute("data-filler") + ? null + : [row.getAttribute("data-line"), row.getAttribute("data-change")], + ); + +const texts = () => + [...container.querySelectorAll("[data-line]")].map((row) => row.textContent); + +const lineCount = (text: string | null) => + text === null ? 0 : text.split("\n").length; + +/** The bundle a sharer would cut for the whole file, with the rows every + * selection in it resolves through. */ +function share(fixture: Fixture) { + const pins = { repositoryId: "repo", base: fixture.base, head: fixture.head }; + + const rows = textualRows( + fixture.file, + fixture.patch, + lineCount(fixture.baseText), + lineCount(fixture.headText), + ); + + const sources: ShareSource[] = []; + + if (fixture.baseText !== null) + sources.push({ + kind: "excerpt", + file: fixture.previousFile ?? fixture.file, + commit: fixture.base, + fromLine: 1, + lines: fixture.baseText.split("\n"), + }); + + if (fixture.headText !== null) + sources.push({ + kind: "excerpt", + file: fixture.file, + commit: fixture.head, + fromLine: 1, + lines: fixture.headText.split("\n"), + }); + + const alignment: ShareAlignment = { + file: fixture.file, + ...(fixture.previousFile && { previousFile: fixture.previousFile }), + head: fixture.head, + base: fixture.base, + rows: rows.map(([base, head]) => [ + base === null ? null : base + 1, + head === null ? null : head + 1, + ]), + }; + + return { + pins, + sources, + alignment, + hunks: parseUnifiedPatch(fixture.file, fixture.patch), + }; +} + +/** A selection over every line of a hunk, as Desktop resolves it. */ +function hunkSpec(file: string, hunk: DiffHunk): ReviewInlineStaticSpec { + return { + path: file, + title: file, + side: "head", + heightMode: "content", + ranges: [ + ...(hunk.oldLines + ? [ + { + side: "base" as const, + startLine: hunk.oldStart, + endLine: hunk.oldStart + hunk.oldLines - 1, + }, + ] + : []), + ...(hunk.newLines + ? [ + { + side: "head" as const, + startLine: hunk.newStart, + endLine: hunk.newStart + hunk.newLines - 1, + }, + ] + : []), + ], + }; +} + +/** The hunk as Git prints it: context, then each change's removed lines + * before its added lines. */ +const unified = (hunk: DiffHunk): Rendered[] => + hunk.lines.map((line) => + line.kind === "context" + ? [String(line.newLine), null] + : line.kind === "remove" + ? [String(line.oldLine), "removed"] + : [String(line.newLine), "added"], + ); + +/** Each change's removed and added lines side by side, padded with fillers. */ +function split(hunk: DiffHunk) { + const columns: Record<"base" | "head", Rendered[]> = { base: [], head: [] }; + let removed: Rendered[] = []; + let added: Rendered[] = []; + + const flush = () => { + for (let i = 0; i < Math.max(removed.length, added.length); i++) { + columns.base.push(removed[i] ?? null); + columns.head.push(added[i] ?? null); + } + + [removed, added] = [[], []]; + }; + + for (const line of hunk.lines) + if (line.kind === "remove") removed.push([String(line.oldLine), "removed"]); + else if (line.kind === "add") added.push([String(line.newLine), "added"]); + else { + flush(); + columns.base.push([String(line.oldLine), null]); + columns.head.push([String(line.newLine), null]); + } + + flush(); + + return columns; +} + +describe.each(fixtures as Fixture[])("$name", (fixture) => { + const { pins, sources, alignment, hunks } = share(fixture); + + it("shows each hunk as Git does, in both layouts, with its text", async () => { + const editors = await shareInlineEditors(sources, pins, [alignment]); + + for (const hunk of hunks) { + const spec = hunkSpec(fixture.file, hunk); + + container.innerHTML = editors(settings("unified")).renderStatic!(spec); + expect(lines()).toEqual(unified(hunk)); + expect(container.querySelectorAll("[data-range]")).toHaveLength(1); + + container.innerHTML = editors(settings("unified", false)).renderStatic!( + spec, + ); + expect(texts()).toEqual(hunk.lines.map((line) => line.text)); + + container.innerHTML = editors(settings("split")).renderStatic!(spec); + const columns = split(hunk); + expect(lines('[data-column="base"]')).toEqual(columns.base); + expect(lines('[data-column="head"]')).toEqual(columns.head); + } + }); + + it("paints the same from one run per hunk as from the whole file", async () => { + const whole = await shareInlineEditors(sources, pins, [alignment]); + + const runs = await shareInlineEditors( + sources, + pins, + hunks.map((hunk) => ({ + ...alignment, + rows: alignment.rows.filter( + ([base, head]) => + (base !== null && + hunk.oldStart <= base && + base < hunk.oldStart + hunk.oldLines) || + (head !== null && + hunk.newStart <= head && + head < hunk.newStart + hunk.newLines), + ), + })), + ); + + for (const hunk of hunks) { + const spec = hunkSpec(fixture.file, hunk); + container.innerHTML = whole(settings("unified")).renderStatic!(spec); + const expected = lines(); + container.innerHTML = runs(settings("unified")).renderStatic!(spec); + expect(lines()).toEqual(expected); + } + }); + + it("shows what a selection on one side's changed lines alone replaced", async () => { + const editors = await shareInlineEditors(sources, pins, [alignment]); + + for (const hunk of hunks) { + // The hunk's first change: its lines up to the next context line. + const start = hunk.lines.findIndex((line) => line.kind !== "context"); + let end = start; + + while (hunk.lines[end + 1] && hunk.lines[end + 1]!.kind !== "context") + end++; + + const change = hunk.lines.slice(start, end + 1); + const removed = change.filter((line) => line.kind === "remove"); + const added = change.filter((line) => line.kind === "add"); + // The patch pairs its i-th removed line with its i-th added line. + const pairs = Math.min(removed.length, added.length); + + const row = (line: DiffHunk["lines"][number]): Rendered => + line.kind === "remove" + ? [String(line.oldLine), "removed"] + : [String(line.newLine), "added"]; + + for (const [side, own, expected] of [ + [ + "head", + added, + [...removed.slice(0, pairs).map(row), ...added.map(row)], + ], + [ + "base", + removed, + [...removed.map(row), ...added.slice(0, pairs).map(row)], + ], + ] as const) { + if (!own.length) continue; + const at = side === "head" ? "newLine" : "oldLine"; + + container.innerHTML = editors(settings("unified")).renderStatic!({ + ...hunkSpec(fixture.file, hunk), + ranges: [ + { side, startLine: own[0]![at]!, endLine: own.at(-1)![at]! }, + ], + }); + + expect(lines()).toEqual(expected); + } + } + }); +}); + +it("prints new code as plain lines without the sharer's rows", async () => { + const fixture = (fixtures as Fixture[]).find( + (entry) => entry.name === "insert", + )!; + + const { pins, sources, hunks } = share(fixture); + const editors = await shareInlineEditors(sources, pins); + const added = hunks[0]!.lines.filter((line) => line.kind === "add"); + + container.innerHTML = editors(settings("unified")).renderStatic!({ + ...hunkSpec(fixture.file, hunks[0]!), + ranges: [ + { + side: "head", + startLine: added[0]!.newLine!, + endLine: added.at(-1)!.newLine!, + }, + ], + }); + + expect(lines().map((row) => row?.[1])).toEqual(added.map(() => null)); +}); diff --git a/packages/review/app/src/share/inline-editors.ts b/packages/review/app/src/share/inline-editors.ts index ce3789f7b..c82a1b762 100644 --- a/packages/review/app/src/share/inline-editors.ts +++ b/packages/review/app/src/share/inline-editors.ts @@ -158,6 +158,14 @@ function alignedBlocks( if (alignment.head !== pins.head || alignment.base !== pins.base) continue; let run: Row[] | undefined; + // A change's removed lines come before its added lines, as in a patch. + let added: Row[] = []; + + const flush = () => { + run?.push(...added); + added = []; + }; + for (const [b, h] of alignment.rows) { const hit = [...ranges].filter((range) => { const n = range.side === "base" ? b : h; @@ -166,6 +174,7 @@ function alignedBlocks( }); if (!hit.length) { + flush(); run = undefined; continue; @@ -192,14 +201,17 @@ function alignedBlocks( ? undefined : { n: h, ...excerpt(alignment.file, pins.head, h) }; - if (before && after && before.text === after.text) + if (before && after && before.text === after.text) { + flush(); run.push({ ...after, base: b! }); - else { + } else { if (before) run.push({ ...before, change: "removed" }); - if (after) run.push({ ...after, change: "added" }); + if (after) added.push({ ...after, change: "added" }); } } + + flush(); } for (const range of reached) ranges.delete(range); From d67c3fa317f9c84c97d702b491ee524fd4fd920d Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Mon, 2 Nov 2026 13:59:04 -0500 Subject: [PATCH 3/3] Trim share peek tests and comments Agent-Session: 61c94475-694a-4e46-8971-e0d703d3b344 Agent-Session: b47f56d8-24aa-4d4f-95b3-4217b7ec9e00 Agent-Session: f2f8f3d8-5a50-4c15-8ad8-9c9308d4a110 Agent-Session: 2b04f44e-91c6-4568-8c57-c4a01b2828ad --- packages/review-share-protocol/src/index.ts | 9 +-- .../src/share/inline-editors.browser.test.ts | 43 ------------- .../inline-editors.real-diffs.browser.test.ts | 60 +------------------ .../review/app/src/share/inline-editors.ts | 3 +- 4 files changed, 4 insertions(+), 111 deletions(-) diff --git a/packages/review-share-protocol/src/index.ts b/packages/review-share-protocol/src/index.ts index 694756f56..9f31a0dd6 100644 --- a/packages/review-share-protocol/src/index.ts +++ b/packages/review-share-protocol/src/index.ts @@ -94,26 +94,21 @@ const lineRangeSchema = z.object({ .optional(), }); -/** A base line and a head line the sharer's diff paired, 1-based; null is a - * line the other side lacks. */ +/** 1-based base and head lines; null when a side lacks the line. */ const alignmentRowSchema = z.tuple([ z.number().int().positive().nullable(), z.number().int().positive().nullable(), ]); -/** One contiguous run of the diff alignment a board's references span, so a - * reader paints the sharer's diff instead of pairing excerpts itself. */ export const shareAlignmentSchema = z.object({ file: sharePathSchema, - /** The base side's path when the file was renamed. */ previousFile: sharePathSchema.optional(), head: z.string(), base: z.string().optional(), rows: z.array(alignmentRowSchema).min(1), }); -/** Readers drop fields from newer sharers, so a Desktop can open any later - * bundle. */ +/** Unknown fields drop, so a reader opens bundles from newer sharers. */ export const shareBundleSchema = z.object({ format: z.literal(SHARE_FORMAT), reviewId: z.string().min(1).max(256), diff --git a/packages/review/app/src/share/inline-editors.browser.test.ts b/packages/review/app/src/share/inline-editors.browser.test.ts index 485b767be..5ede7db71 100644 --- a/packages/review/app/src/share/inline-editors.browser.test.ts +++ b/packages/review/app/src/share/inline-editors.browser.test.ts @@ -190,49 +190,6 @@ it("follows the reader's layout choice and reports focus", async () => { container.remove(); }); -it("paints one-sided rows of the sharer's alignment with no lines to pair", async () => { - const editors = await shareInlineEditors(sources, pins, [ - { - file: "exec.ts", - base, - head, - rows: [ - [10, null], - [11, null], - [12, null], - [null, 12], - [null, 13], - [null, 14], - [null, 15], - ], - }, - ]); - - container.innerHTML = editors(settings("unified").peek).renderStatic!({ - ...spec, - ranges: [{ side: "head", startLine: 12, endLine: 15 }], - }); - - expect(lines()).toEqual([ - ["12", "added"], - ["13", "added"], - ["14", "added"], - ["15", "added"], - ]); - - container.innerHTML = editors(settings("split").peek).renderStatic!({ - ...spec, - ranges: [{ side: "base", startLine: 10, endLine: 12 }], - }); - - expect(lines('[data-column="base"]')).toEqual([ - ["10", "removed"], - ["11", "removed"], - ["12", "removed"], - ]); - expect(lines('[data-column="head"]')).toEqual([null, null, null]); -}); - it("reads a pair with differing text as removed then added, and leaves ranges the alignment misses to pairing", async () => { const editors = ( await shareInlineEditors( diff --git a/packages/review/app/src/share/inline-editors.real-diffs.browser.test.ts b/packages/review/app/src/share/inline-editors.real-diffs.browser.test.ts index 0a22382b4..ef0f225f7 100644 --- a/packages/review/app/src/share/inline-editors.real-diffs.browser.test.ts +++ b/packages/review/app/src/share/inline-editors.real-diffs.browser.test.ts @@ -13,7 +13,6 @@ import { afterEach, describe, expect, it } from "vitest"; import fixtures from "./__fixtures__/real-diffs.json"; import { type PeekSettings, shareInlineEditors } from "./inline-editors"; -/** Real commits of this repository: the patch Git wrote and both files. */ interface Fixture { name: string; base: string; @@ -58,8 +57,6 @@ const texts = () => const lineCount = (text: string | null) => text === null ? 0 : text.split("\n").length; -/** The bundle a sharer would cut for the whole file, with the rows every - * selection in it resolves through. */ function share(fixture: Fixture) { const pins = { repositoryId: "repo", base: fixture.base, head: fixture.head }; @@ -109,7 +106,6 @@ function share(fixture: Fixture) { }; } -/** A selection over every line of a hunk, as Desktop resolves it. */ function hunkSpec(file: string, hunk: DiffHunk): ReviewInlineStaticSpec { return { path: file, @@ -139,8 +135,7 @@ function hunkSpec(file: string, hunk: DiffHunk): ReviewInlineStaticSpec { }; } -/** The hunk as Git prints it: context, then each change's removed lines - * before its added lines. */ +/** The hunk as Git prints it. */ const unified = (hunk: DiffHunk): Rendered[] => hunk.lines.map((line) => line.kind === "context" @@ -150,7 +145,6 @@ const unified = (hunk: DiffHunk): Rendered[] => : [String(line.newLine), "added"], ); -/** Each change's removed and added lines side by side, padded with fillers. */ function split(hunk: DiffHunk) { const columns: Record<"base" | "head", Rendered[]> = { base: [], head: [] }; let removed: Rendered[] = []; @@ -204,35 +198,6 @@ describe.each(fixtures as Fixture[])("$name", (fixture) => { } }); - it("paints the same from one run per hunk as from the whole file", async () => { - const whole = await shareInlineEditors(sources, pins, [alignment]); - - const runs = await shareInlineEditors( - sources, - pins, - hunks.map((hunk) => ({ - ...alignment, - rows: alignment.rows.filter( - ([base, head]) => - (base !== null && - hunk.oldStart <= base && - base < hunk.oldStart + hunk.oldLines) || - (head !== null && - hunk.newStart <= head && - head < hunk.newStart + hunk.newLines), - ), - })), - ); - - for (const hunk of hunks) { - const spec = hunkSpec(fixture.file, hunk); - container.innerHTML = whole(settings("unified")).renderStatic!(spec); - const expected = lines(); - container.innerHTML = runs(settings("unified")).renderStatic!(spec); - expect(lines()).toEqual(expected); - } - }); - it("shows what a selection on one side's changed lines alone replaced", async () => { const editors = await shareInlineEditors(sources, pins, [alignment]); @@ -282,26 +247,3 @@ describe.each(fixtures as Fixture[])("$name", (fixture) => { } }); }); - -it("prints new code as plain lines without the sharer's rows", async () => { - const fixture = (fixtures as Fixture[]).find( - (entry) => entry.name === "insert", - )!; - - const { pins, sources, hunks } = share(fixture); - const editors = await shareInlineEditors(sources, pins); - const added = hunks[0]!.lines.filter((line) => line.kind === "add"); - - container.innerHTML = editors(settings("unified")).renderStatic!({ - ...hunkSpec(fixture.file, hunks[0]!), - ranges: [ - { - side: "head", - startLine: added[0]!.newLine!, - endLine: added.at(-1)!.newLine!, - }, - ], - }); - - expect(lines().map((row) => row?.[1])).toEqual(added.map(() => null)); -}); diff --git a/packages/review/app/src/share/inline-editors.ts b/packages/review/app/src/share/inline-editors.ts index c82a1b762..9a102e5ef 100644 --- a/packages/review/app/src/share/inline-editors.ts +++ b/packages/review/app/src/share/inline-editors.ts @@ -141,8 +141,7 @@ function unified(base: Block, head: Block): Block[] { type SideRange = Spec["ranges"][number] & { side: ReviewDiffSide }; -/** The sharer's diff over the ranges an alignment reaches, one block per run of - * rows; the ranges none reaches stay in `ranges`. */ +/** Blocks for the ranges an alignment reaches; the rest stay in `ranges`. */ function alignedBlocks( excerpt: Excerpt, pins: ReviewSourcePins | undefined,