Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/review-anchors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@foldworks/diff-viewer": minor
"@foldworks/review": patch
---

Map review anchors across revisions conservatively and expand supplied context
with stale-gap validation; document outdated thread composition.
7 changes: 7 additions & 0 deletions .changeset/review-chrome.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@foldworks/review": minor
"@foldworks/ui": minor
---

Add changed-file navigation, checks, merge requirements and change-request
headers. Extend checkable Tree composition and timeline activity body slots.
6 changes: 6 additions & 0 deletions .changeset/review-diff-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@foldworks/diff-viewer": minor
---

Produce and serialize text diffs; add intraline markup, full-row line slots,
annotations and a semantic highlighting hook.
11 changes: 11 additions & 0 deletions .changeset/review-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@foldworks/review": patch
"@foldworks/diff-viewer": patch
"@foldworks/diagram": patch
---

Exercise configuration review end to end in a persistent local reference host.
Retain suggestion drafts through saves, read current replacement text on submit,
restore composer focus after commit, and harden patch and highlight rendering.

Use the current Foldkit subscription lifting API in the graph primitive.
6 changes: 6 additions & 0 deletions .changeset/review-suggestions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@foldworks/review": minor
---

Add checked, atomic suggested changes, pending comment display, controlled
review submission and reviewer status views.
6 changes: 6 additions & 0 deletions .changeset/review-threads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@foldworks/review": minor
---

Add generic review-thread schemas, acknowledged drafts, mutation out-messages,
accessible thread, composer and grouped list views.
6 changes: 6 additions & 0 deletions .changeset/review-values.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@foldworks/diff-viewer": minor
---

Compare decoded configuration values by path and array identity, render semantic
value changes, and map UTF-16 diagnostics to diff annotations.
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ contract:

## The stack

| Layer | What it provides | Packages |
| -------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| 05. Workbenches | Complete tools composed from the layers below | `agent`, `generative-ui`, `codebase` |
| 04. Application primitives | Product surfaces with models, messages, and updates included | `editor`, `code-editor`, `outliner`, `data-table`, `data-grid`, `query-builder`, and more |
| 03. Behaviors | Headless engines that more than one surface shares | `keyboard`, `history`, `text-intelligence`, `diagram`, `pdf` |
| 02. Components | Accessible controls, overlays, and application chrome | `ui`, `sidebar` |
| 01. Tokens | Semantic CSS variables in light and dark, with Shadcn, Polaris, Apple, and other themes | `ui` |
| Layer | What it provides | Packages |
| -------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| 05. Workbenches | Complete tools composed from the layers below | `agent`, `generative-ui`, `codebase` |
| 04. Application primitives | Product surfaces with models, messages, and updates included | `editor`, `code-editor`, `outliner`, `data-table`, `data-grid`, `query-builder`, `review`, and more |
| 03. Behaviors | Headless engines that more than one surface shares | `keyboard`, `history`, `text-intelligence`, `diagram`, `pdf` |
| 02. Components | Accessible controls, overlays, and application chrome | `ui`, `sidebar` |
| 01. Tokens | Semantic CSS variables in light and dark, with Shadcn, Polaris, Apple, and other themes | `ui` |

### Components

Expand Down Expand Up @@ -82,6 +82,7 @@ contract:
- [`@foldworks/workflow`](./packages/workflow): recursive workflows,
branch-aware operations, layout, and drag-and-drop primitives, built on the
`@foldworks/diagram` scene contract.
- [`@foldworks/review`](./packages/review): review threads, drafts, and host-owned change-request workflows.
- [`@foldworks/diff-viewer`](./packages/diff-viewer): a controlled unified and
split diff renderer with line selection for code review workflows.
- [`@foldworks/pdf-viewer`](./packages/pdf-viewer): read-only PDF viewing
Expand Down
44 changes: 43 additions & 1 deletion apps/demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A browser-only demo app built with Foldkit, StyleX, and Vite. The homepage
introduces the Foldworks package family, shows a live component composition,
and explains the boundary between UI components and application primitives.
Use the sidebar to move between every package showcase, including the interactive
diff review prototype at `/diff-viewer`.
local change-request reference at `/diff-viewer`.

Foldkit's bidirectional router owns navigational state. The project overview
lives at `/`; demos live at `/ui-kit`, `/editor`, `/codebase`, `/data-table`, `/data-grid`, `/query-builder`,
Expand Down Expand Up @@ -172,3 +172,45 @@ The `/ui-kit` Desktop workspace demonstrates shared keyboard commands, measured
breadcrumb/action rows, inline editing, token entry, a nested inspector, context
menus, modal variants and a controlled date input. See
[the integration guide](../../packages/ui/docs/desktop.md).

## Local change-request reference

`/diff-viewer` implements edit → diff → change request → pending line comments →
suggestions → checks → approve → merge → history. It has two synthetic JSON
configuration files validated by Effect schemas. All transport, identity,
persistence and policy are local demo host code; there is no external repository
or Git provider integration.

Change checkout to true and rollout to 100 in CodeEditor (a string for checkout
shows a schema diagnostic). Open the request and switch to the local reviewer.
Comment on new line 4, then select new lines 3–4 and suggest:

```json
"rollout": 25,
"reviewedBy": "Local reviewer"
```

Submit Request changes, switch to author and apply the suggestion. Its thread
is reanchored to the replacement revision; the old note discussion is outdated.
Approve as the reviewer, switch back to author, then merge. Local history,
comments, source text, review drafts and viewed files survive reload. Approval
only counts while its captured configuration text matches the working files. The example policy excludes outdated threads from
blocking requirements. Failed storage writes report an error and retain the
current in-memory session; this adapter has no multi-tab concurrency control.

Keyboard users activate a gutter with Enter, extend an existing selection with
Shift+Arrow, save with Mod+Enter, and close with Escape to return to the gutter.
Closing retains drafts for the same anchor and revision. Thread lists use one
tab stop with Arrow/Home/End and Enter to reveal an anchor. Changed-file trees
use Space for viewed state. The reference includes local dark appearance and a
single-column narrow layout.

Run the focused scenario with:

```sh
pnpm --filter @foldworks/demo exec vitest run --config vitest.e2e.config.ts -t 'change request reference'
```

`pnpm test:e2e` also runs it. Documentation screenshots (animations disabled, not
pixel comparisons) are saved under `.context/demo-screenshots/change-request-*.png`.
Reset simulation clears this example's saved state.
203 changes: 172 additions & 31 deletions apps/demo/e2e/diff-viewer.scenarios.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,183 @@
import type { Page } from "playwright";
import { describe, expect, it } from "vitest";

import { proposedFlags, suggestedFlags } from "../src/diff-viewer/fixture";
const source = (page: Page) =>
page.getByRole("textbox", { name: "Configuration source", exact: true });
const diff = (page: Page) => page.locator(".cr-demo__diff > .fk-diff-viewer");
const ready = async (page: Page) =>
expect
.poll(() =>
source(page).evaluate(
(element) =>
(element as HTMLTextAreaElement & { foldkitNative?: { status: string } }).foldkitNative
?.status,
),
)
.toBe("Ready");
export const diffViewerScenarios = (
getPage: () => Page,
appUrl: string,
screenshot: (name: string) => Promise<void>,
): void => {
describe("diff review", () => {
it("changes layout, adds a line comment, and tracks viewed files", async () => {
describe("change request reference", () => {
it("edits, validates, requests changes, applies suggestions, approves, merges and reloads history", async () => {
const page = getPage();
await page.goto(`${appUrl}/diff-viewer`, { waitUntil: "networkidle" });

await expect.poll(() => page.locator('[data-diff-review-demo="true"]').isVisible()).toBe(true);
await expect.poll(() => page.getByRole("region", { name: /Changes in apps\/demo\/src\/review\/commands.ts/ }).isVisible()).toBe(true);
await page.getByRole("button", { name: "Unified diff" }).click();
await expect.poll(() => page.locator(".fk-diff-viewer").getAttribute("data-mode")).toBe("unified");

const rangeStart = page.getByRole("button", { name: "Comment on new line 20" });
const rangeEnd = page.getByRole("button", { name: "Comment on new line 24" });
const startBox = await rangeStart.boundingBox();
const endBox = await rangeEnd.boundingBox();
expect(startBox).not.toBeNull();
expect(endBox).not.toBeNull();
if (startBox === null || endBox === null) return;
await page.mouse.move(startBox.x + startBox.width / 2, startBox.y + startBox.height / 2);
await page.mouse.down();
await page.mouse.move(endBox.x + endBox.width / 2, endBox.y + endBox.height / 2, { steps: 8 });
await page.mouse.up();
await expect.poll(() => page.getByText("New lines 20–24", { exact: true }).isVisible()).toBe(true);
await page.getByRole("textbox", { name: "Review comment" }).fill("Please add a regression test for the empty review path.");
await page.getByRole("button", { name: "Add comment" }).click();
await expect.poll(() => page.getByText("Please add a regression test for the empty review path.", { exact: true }).isVisible()).toBe(true);
await expect.poll(() => page.getByText("New lines 20–24", { exact: true }).isVisible()).toBe(true);

await page.getByRole("button", { name: /Mark .*commands.ts as viewed/ }).click();
await expect.poll(() => page.locator(".review-demo__nav-footer").textContent()).toContain("2 of 5 viewed");
await expect.poll(() => page.getByRole("button", { name: /Unmark .*commands.ts as viewed/ }).isVisible()).toBe(true);
await screenshot("diff-viewer-review");
});
await ready(page);
await expect
.poll(() => page.getByText("LOCAL SIMULATION", { exact: true }).isVisible())
.toBe(true);
await source(page).fill(proposedFlags.replace('"checkout": true', '"checkout": "yes"'));
await expect.poll(() => page.locator(".native-token--issue").count()).toBeGreaterThan(0);
await expect
.poll(() => diff(page).locator('[data-annotation-tone="danger"]').count())
.toBeGreaterThan(0);
await screenshot("change-request-01-schema-error");
await source(page).fill(proposedFlags);
await expect.poll(() => page.locator(".native-token--issue").count()).toBe(0);
await page.getByRole("button", { name: "Semantic values", exact: true }).click();
await expect
.poll(() => page.getByRole("region", { name: "Value changes" }).isVisible())
.toBe(true);
await screenshot("change-request-02-values");
await page.getByRole("button", { name: "Line diff", exact: true }).click();
await page.getByRole("button", { name: "Open change request", exact: true }).click();
const files = page.getByRole("tree", { name: "Changed files", exact: true });
await files.focus();
await files.press("End");
await files.press("Enter");
await expect
.poll(() =>
page.getByRole("combobox", { name: "Configuration file", exact: true }).inputValue(),
)
.toBe("config/access.json");
await files.press(" ");
await expect
.poll(() => page.getByText("1 files viewed · 0 discussions", { exact: true }).isVisible())
.toBe(true);
await page.getByRole("button", { name: /Expand .* unchanged lines/ }).click();
await expect
.poll(() =>
diff(page)
.getByRole("button", { name: "Comment on new line 1", exact: true })
.isVisible(),
)
.toBe(true);
await page
.getByRole("combobox", { name: "Configuration file", exact: true })
.selectOption("config/flags.json");
await page.getByRole("button", { name: "Act as reviewer", exact: true }).click();
const noteLine = diff(page).getByRole("button", {
name: "Comment on new line 4",
exact: true,
});
await noteLine.focus();
await noteLine.press("Enter");
const composer = page.getByRole("textbox", { name: "Review comment", exact: true });
await expect
.poll(() => composer.evaluate((element) => element === document.activeElement))
.toBe(true);
await composer.fill("Please remove this temporary note.");
await composer.press("Control+Enter");
await expect
.poll(() => noteLine.evaluate((element) => element === document.activeElement))
.toBe(true);
const start = diff(page).getByRole("button", {
name: /^(?:Comment on|Selected) new line 3$/,
});
await start.focus();
await start.press("Enter");
await start.focus();
await start.press("Shift+ArrowDown");
await expect
.poll(() => page.getByText("New lines 3–4", { exact: true }).isVisible())
.toBe(true);
await expect
.poll(() => composer.evaluate((element) => element === document.activeElement))
.toBe(true);
await composer.fill("Keep the first rollout small and record the reviewer.");
await page.getByRole("button", { name: "Add suggestion", exact: true }).click();
const replacement = page.getByRole("textbox", { name: "Suggested replacement", exact: true });
await replacement.fill(suggestedFlags);
await replacement.press("Control+Enter");
await expect
.poll(() =>
page
.getByRole("form", { name: "Submit review", exact: true })
.getByRole("status")
.textContent(),
)
.toBe("2 pending comments");
await page.getByRole("button", { name: "Request changes", exact: true }).click();
await page.getByRole("button", { name: "Submit review", exact: true }).click();
await screenshot("change-request-03-request-changes");
await page.getByRole("button", { name: "Act as author", exact: true }).click();
await page.getByRole("button", { name: "Apply suggestion", exact: true }).click();
await expect.poll(() => page.locator('[data-anchor-state="outdated"]').count()).toBe(1);
await expect.poll(() => page.locator('[data-anchor-state="moved"]').count()).toBe(1);
await expect.poll(() => source(page).inputValue()).toContain('"rollout": 25');
await screenshot("change-request-04-suggestion");
await page.getByRole("button", { name: "Act as reviewer", exact: true }).click();
await page.getByRole("button", { name: "Approve", exact: true }).click();
await page.getByRole("button", { name: "Submit review", exact: true }).click();
await page.getByRole("button", { name: "Act as author", exact: true }).click();
await expect
.poll(() =>
page.getByRole("button", { name: "Merge change request", exact: true }).isEnabled(),
)
.toBe(true);
await page.setViewportSize({ width: 1600, height: 1200 });
await page.locator(".cr-demo").evaluate((element) => {
let node: HTMLElement | null = element as HTMLElement;
while (node) {
node.scrollTop = 0;
node = node.parentElement;
}
});
await screenshot("change-request-05-ready");
await page.getByRole("button", { name: "Dark appearance", exact: true }).click();
await screenshot("change-request-08-dark-desktop");
await page.getByRole("button", { name: "Light appearance", exact: true }).click();
await page.getByRole("button", { name: "Merge change request", exact: true }).click();
await expect.poll(() => page.locator(".fk-review__header").textContent()).toContain("merged");
await page.reload({ waitUntil: "networkidle" });
await ready(page);
await expect
.poll(() => page.getByRole("region", { name: "Change request activity" }).textContent())
.toContain("Configuration merged locally");
await expect
.poll(() => source(page).inputValue())
.toContain('"reviewedBy": "Local reviewer"');
await screenshot("change-request-06-history");
}, 90_000);
it("supports keyboard cancellation, narrow screens and dark appearance", async () => {
const page = getPage();
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(`${appUrl}/diff-viewer`, { waitUntil: "networkidle" });
await ready(page);
await source(page).fill(proposedFlags);
await page.getByRole("button", { name: "Open change request", exact: true }).click();
await page.getByRole("button", { name: "Act as reviewer", exact: true }).click();
const gutter = diff(page).getByRole("button", {
name: /^(?:Comment on|Selected) new line 3$/,
});
await gutter.focus();
await gutter.press("Enter");
const composer = page.getByRole("textbox", { name: "Review comment", exact: true });
await composer.fill("Draft survives closing");
await composer.press("Escape");
await expect
.poll(() => gutter.evaluate((element) => document.activeElement === element))
.toBe(true);
await gutter.press("Enter");
await expect.poll(() => composer.inputValue()).toBe("Draft survives closing");
await composer.press("Escape");
await page.getByRole("button", { name: "Dark appearance", exact: true }).click();
await expect.poll(() => page.locator(".cr-demo").getAttribute("data-dark")).toBe("true");
expect(
await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
).toBe(true);
await screenshot("change-request-07-dark-mobile");
}, 60_000);
});
};
1 change: 1 addition & 0 deletions apps/demo/e2e/package-demo.scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const expectedPackages = [
"@foldworks/form-builder",
"@foldworks/workflow",
"@foldworks/diff-viewer",
"@foldworks/review",
"@foldworks/pdf-viewer",
"@foldworks/pdf-annotator",
"@foldworks/keyboard",
Expand Down
3 changes: 2 additions & 1 deletion apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"@stylexjs/stylex": "catalog:",
"effect": "catalog:",
"foldkit": "catalog:",
"pdf-lib": "catalog:"
"pdf-lib": "catalog:",
"@foldworks/review": "workspace:*"
},
"devDependencies": {
"@effect/platform-browser": "catalog:",
Expand Down
Loading
Loading