Skip to content

feat(workspace): version history timeline backed by kernel events#593

Open
v-rdyy wants to merge 3 commits into
ThinkEx-OSS:mainfrom
v-rdyy:feat/workspace-version-history
Open

feat(workspace): version history timeline backed by kernel events#593
v-rdyy wants to merge 3 commits into
ThinkEx-OSS:mainfrom
v-rdyy:feat/workspace-version-history

Conversation

@v-rdyy

@v-rdyy v-rdyy commented Jul 5, 2026

Copy link
Copy Markdown

Root cause

The workspace root menu's Version history action was a disabled stub (trailing: "Soon" in WorkspaceContextBar.tsx), and kernel_events rows — which already record every mutation with a monotonic revision, actor, and payload — were only ever consumed for realtime cache sync via getEventsSince(). Nothing read the log backwards for humans.

Approach and the inspection-only scoping decision

This v1 is inspection-only: no restore, no snapshots, no diffs. Event payloads are written after the mutation (commitItemEvent reads the row post-update), so they carry only post-change summaries — no previous name on renames, and content.updated has no content snapshot at all. Past state cannot be reconstructed from the log, so restore is out of scope by design, and the dialog says so in its description ("History is inspection-only for now — restoring earlier versions isn't supported yet"). Copy consequence: renames read "renamed to X", never "renamed from Y to X".

The implementation is one additive read path:

  • Kernel: WorkspaceKernelEventBus.listEventsPage({ beforeRevision?, limit }) pages kernel_events by ORDER BY revision DESC with WHERE revision < cursor, limit clamped to 1–100 (default 50), fetching one lookahead row so nextBeforeRevision is only advertised when a next page actually exists. Exposed on WorkspaceKernel next to getEventsSince, which is untouched. The event log is strictly read-only for this feature.
  • Server: getWorkspaceHistoryPageFn (GET) → getWorkspaceHistoryPageForCurrentUsergetWorkspaceKernelHistoryPage, gated by the same assertCanReadWorkspace check that getWorkspacePageFn uses. Input validated with zod (beforeRevision positive int, limit 1–100).
  • View model: pure mapWorkspaceHistoryEvents in model/workspace-history.ts turns raw events into readable entries and coalesces consecutive content.updated bursts for the same item by the same actor within 5 minutes into one "edited “X” (N changes)" entry. Grouping happens client-side; the API returns raw events.
  • UI: WorkspaceVersionHistoryDialog opened from the (now enabled) menu action, TanStack infinite query keyed on the revision cursor, actor names resolved from the existing members query, relative timestamps via formatWorkspaceRecency, loading/empty/error states, and a Load more button. No route changes; nothing fetches until the dialog opens, so baseline workspace load is unchanged (getPage() untouched).

Graceful degradation is layered: a malformed payload_json row falls back to a generic record inside listEventsPage (the shared mapKernelEventRow stays strict for realtime), and the view model routes unknown event types or unexpected payload shapes to a generic "made a change" row — so future event types can never break the dialog.

Decisions and rejected alternatives

  • No itemId filter parameter yet. The issue floats per-item history later; I rejected shipping a dead parameter that doesn't filter (a trap for callers). Adding an optional itemId to the schema later is backward-compatible.
  • Dialog over side panel — matches how Share and Settings already hang off this menu, and needs no layout or route work.
  • Lookahead-row pagination over hasMore count queries — one query per page, and no trailing empty page when the total is an exact multiple of the page size.
  • Burst grouping in the view model, not SQL — keeps the API raw and the policy (5-minute window) a pure, fixture-tested function.
  • Delete rows count root itemIds, not deletedItemIds — "deleted 2 items" matches what the user selected; descendants are an implementation detail of recursive delete.
  • No AI badge. AI tool mutations run with the requesting user's id (createThreadWorkspaceAccessContext passes thread.userId), so AI edits are indistinguishable from manual edits in the log today — faking a badge would be wrong. This answers the issue's open question; distinguishing them needs an actor-kind column or a clientMutationId convention (follow-up).

Fact corrections vs the task prompt

  • mapKernelEventRow does not already degrade gracefully — its payload parse is a bare JSON.parse that throws on malformed rows. It is reused as directed, but wrapped per-row in listEventsPage so one bad row can't kill a history page.
  • Collaborative document edits commit content.updated with actorUserId: null (DocumentSession.checkpointToKernel), so most document-edit history is unattributed. The UI renders these as "Someone"; null actors still group into bursts. (AI edits, by contrast, carry the requesting user's id.)

Follow-ups noticed (not in this PR)

  • Restore (the issue's stated end goal): needs either content snapshots in event payloads / a snapshot table, or — cheapest first step — a restoreDeletedItems kernel command leveraging the existing deleted_at soft delete, which requires no schema change. Any snapshot/retention schema should be designed before that implementation, per the issue.
  • AI attribution: record an actor kind (user / ai / system) on kernel_events so AI changes can be visually distinguished.
  • Retention/compaction for high-frequency document edits (issue open question) — unbounded event growth in the DO's SQLite will eventually need a policy.
  • Pre-existing: parseWorkspaceEventPayload in workspace-kernel-rows.ts can throw on a corrupt row in the realtime getEventsSince path too; left untouched here since changing realtime semantics is out of scope.

How verified

  • pnpm check clean (format, lint, types); pnpm test clean — 30 tests, 7 files, including the 17 new ones.
  • New unit tests cover: every known event type's copy, single-item vs bulk move rendering, delete counts, burst grouping (and its splits across actors, items, >5-min gaps, and interleaved events), null-actor grouping, unknown event types, malformed payloads (both JSON-invalid rows at the kernel layer and shape-invalid payloads at the view-model layer), cursor math, limit clamping (0 → 1, 5000 → 100), negative-cursor clamping, and that history reads never touch the revision counter or broadcast (fakes throw if called).
  • pnpm build passes end-to-end (including prerender), run with CLOUDFLARE_VITE_FORCE_LOCAL=true and Docker per the recent CI setup.
  • Manual UI verification (create/rename/move/recolor/edit/delete, then paginate history) requires a seeded local environment with auth secrets; not run here — happy to record a screencap if a maintainer wants one before merge.

Closes #566

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a workspace Version history view from the workspace actions menu, including a dialog with scrollable history, “load more” pagination, and loading/error retry.
    • History entries now display improved details, including actor name (with sensible fallbacks) and relative time, with cleaner grouping of closely related edits.
  • Bug Fixes
    • History loading is resilient to malformed or unexpected event data, preventing the timeline from breaking.
  • Tests
    • Added coverage for history pagination and event-to-history mapping.

The workspace root menu's Version history action was a disabled stub,
and kernel_events rows were only consumed for realtime cache sync.
This turns the stub into a paginated, inspection-only history dialog.

- WorkspaceKernelEventBus.listEventsPage: additive, read-only reverse
  paging over kernel_events (revision cursor, limit clamped to 100,
  lookahead row so the cursor is only advertised when a next page
  exists); malformed payload rows degrade to a generic record.
- getWorkspaceHistoryPageFn server function, gated by the same
  assertCanReadWorkspace check as workspace page loads.
- Pure event-to-timeline mapping in model/workspace-history.ts with
  burst grouping for consecutive content edits; unknown event types
  render as a generic change row.
- WorkspaceVersionHistoryDialog with infinite query, actor names from
  the members query, and loading/empty/error states.

Inspection-only v1: event payloads carry post-change summaries only,
so previous content cannot be reconstructed from the log. getPage()
and realtime sync are untouched; history loads only on demand.

Closes ThinkEx-OSS#566

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@capy-ai

capy-ai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7762415-73ea-4f5f-ab52-7144304b94d4

📥 Commits

Reviewing files that changed from the base of the PR and between 7313134 and 7586565.

📒 Files selected for processing (2)
  • src/features/workspaces/components/WorkspaceVersionHistoryDialog.tsx
  • src/features/workspaces/kernel/workspace-kernel-events.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/features/workspaces/kernel/workspace-kernel-events.ts
  • src/features/workspaces/components/WorkspaceVersionHistoryDialog.tsx

📝 Walkthrough

Walkthrough

This PR adds workspace version history: kernel event pagination, history entry mapping, server-side history fetching, and a dialog opened from the workspace root menu to browse paginated history.

Changes

Workspace Version History

Layer / File(s) Summary
History domain model and event mapping
src/features/workspaces/model/workspace-history.ts, src/features/workspaces/model/workspace-history.test.ts
Adds history types, page-size and burst-window constants, and mapWorkspaceHistoryEvents, plus tests for readable summaries, edit grouping, and malformed payload handling.
Kernel event pagination
src/features/workspaces/kernel/workspace-kernel-events.ts, src/features/workspaces/kernel/workspace-kernel-types.ts, src/features/workspaces/kernel/workspace-kernel.ts, src/features/workspaces/kernel/workspace-kernel-events.test.ts
Adds history pagination args, implements listEventsPage with cursor paging and fallback row mapping, exposes it on WorkspaceKernel, and covers the behavior with tests.
Kernel access and server wiring
src/features/workspaces/kernel/workspace-kernel-access.ts, src/features/workspaces/server/functions.ts, src/features/workspaces/server/queries.ts
Adds permission-checked kernel history access, a validated GET endpoint, and a current-user query that forwards the history pagination arguments.
Version history dialog and menu wiring
src/features/workspaces/components/WorkspaceVersionHistoryDialog.tsx, src/features/workspaces/components/WorkspaceContextBar.tsx
Adds the version history dialog with loading, error, empty, and pagination states, and wires the previously disabled menu item to open it.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WorkspaceContextBar
  participant WorkspaceVersionHistoryDialog
  participant getWorkspaceHistoryPageFn
  participant WorkspaceKernel

  User->>WorkspaceContextBar: select "Version history"
  WorkspaceContextBar->>WorkspaceVersionHistoryDialog: open dialog(workspaceId)
  WorkspaceVersionHistoryDialog->>getWorkspaceHistoryPageFn: fetch history page(beforeRevision, limit)
  getWorkspaceHistoryPageFn->>WorkspaceKernel: listEventsPage(args)
  WorkspaceKernel-->>getWorkspaceHistoryPageFn: WorkspaceHistoryPage
  getWorkspaceHistoryPageFn-->>WorkspaceVersionHistoryDialog: WorkspaceHistoryPage
  WorkspaceVersionHistoryDialog-->>User: render timeline entries
  User->>WorkspaceVersionHistoryDialog: click "Load more"
  WorkspaceVersionHistoryDialog->>getWorkspaceHistoryPageFn: fetch next page(nextBeforeRevision)
  getWorkspaceHistoryPageFn-->>WorkspaceVersionHistoryDialog: additional WorkspaceHistoryPage
Loading

Suggested labels: capy

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the main change: adding workspace version history from kernel events.
Linked Issues check ✅ Passed The PR implements the version history entry point, paged history API, readable timeline, metadata, and empty/error states, and marks it inspection-only.
Out of Scope Changes check ✅ Passed The changes stay focused on workspace version history UI, data access, and tests, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an inspection-only version history view for workspaces. The main changes are:

  • A new Version history dialog opened from the workspace root menu.
  • A paginated history read path over kernel_events.
  • Server validation and workspace read authorization for history pages.
  • Client-side event copy and edit-burst grouping.
  • Tests for pagination, malformed payloads, and history entry mapping.

Confidence Score: 5/5

The changed flow looks safe to merge after a small cleanup to actor-name fallback behavior.

History reads use the existing workspace read authorization path; revision pagination uses a strict cursor and avoids duplicate pages; malformed event payloads degrade without breaking the history view.

src/features/workspaces/components/WorkspaceVersionHistoryDialog.tsx can show misleading actor labels when member lookup fails.

T-Rex T-Rex Logs

What T-Rex did

  • Checked out the base revision and observed that the workspace history API exposed nothing callable and returned 404 for the history page.
  • Checked out the head revision and ran the workspace history tests; all 7 tests passed with exit code 0.
  • On the base revision, the workspace history model import failed with MODULE_NOT_FOUND for ./src/features/workspaces/model/workspace-history.ts (exit code 1).
  • On the head revision, the focused harness printed mapped entries and burst/split outputs, concluding with ALL_ASSERTIONS_PASSED and exit code 0.
  • The validation log notes the attempted command, working directory, server startup, and the reason no contract mismatch is reported because the UI path did not complete in Chromium.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src/features/workspaces/components/WorkspaceContextBar.tsx Enables the Version history menu item and wires dialog open state.
src/features/workspaces/components/WorkspaceVersionHistoryDialog.tsx Adds the history dialog, infinite query, member-name lookup, list states, and load-more UI.
src/features/workspaces/kernel/workspace-kernel-access.ts Adds an authorized kernel history access helper using the existing workspace read check.
src/features/workspaces/kernel/workspace-kernel-events.ts Adds descending event-log pagination with lookahead cursoring and malformed-payload fallback.
src/features/workspaces/kernel/workspace-kernel-types.ts Adds the history pagination argument type.
src/features/workspaces/kernel/workspace-kernel.ts Exposes listEventsPage on the workspace kernel agent.
src/features/workspaces/model/workspace-history.ts Adds pure event-to-history-entry mapping with defensive payload handling and edit grouping.
src/features/workspaces/server/functions.ts Adds the validated GET server function for workspace history pages.
src/features/workspaces/server/queries.ts Adds the current-user history query wrapper.
src/features/workspaces/kernel/workspace-kernel-events.test.ts Adds tests for history pagination, clamping, cursor behavior, and malformed rows.
src/features/workspaces/model/workspace-history.test.ts Adds tests for history copy, grouping, malformed payloads, and unknown event types.

Reviews (1): Last reviewed commit: "feat(workspace): version history timelin..." | Re-trigger Greptile

Comment on lines +53 to +58
const membersQuery = useQuery({
...getWorkspaceMembersQueryOptions(workspaceId),
enabled: open,
});
const actorNamesById = new Map(
(membersQuery.data ?? []).map((member) => [member.userId, member.name]),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Member Lookup Failure Masquerades

When the members request fails but the history request succeeds, membersQuery.data becomes an empty list and every non-null actor is shown as "Former member". A transient member API error can make the audit timeline misidentify current users while the dialog otherwise appears loaded.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7586565 — actor names now stay neutral ("Someone") until the member list has actually loaded; "Former member" is only shown once member data is present and the id is genuinely absent.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/features/workspaces/components/WorkspaceVersionHistoryDialog.tsx (1)

57-62: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Memoize entries and actorNamesById.

Both are recomputed on every render even though WorkspaceVersionHistoryDialog is unconditionally mounted alongside other context-bar dialogs (search/share/settings), so unrelated state changes in the parent re-trigger the burst-grouping pass and Map construction here.

♻️ Proposed fix
-	const actorNamesById = new Map(
-		(membersQuery.data ?? []).map((member) => [member.userId, member.name]),
-	);
-	const entries = mapWorkspaceHistoryEvents(
-		(historyQuery.data?.pages ?? []).flatMap((page) => page.events),
-	);
+	const actorNamesById = useMemo(
+		() => new Map((membersQuery.data ?? []).map((member) => [member.userId, member.name])),
+		[membersQuery.data],
+	);
+	const entries = useMemo(
+		() => mapWorkspaceHistoryEvents((historyQuery.data?.pages ?? []).flatMap((page) => page.events)),
+		[historyQuery.data],
+	);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/components/WorkspaceVersionHistoryDialog.tsx` around
lines 57 - 62, Memoize both actorNamesById and entries in
WorkspaceVersionHistoryDialog so they are only recomputed when their inputs
change. Wrap the Map creation and the mapWorkspaceHistoryEvents result in
useMemo, keyed off membersQuery.data and historyQuery.data?.pages respectively,
to avoid repeating the burst-grouping pass and Map construction on unrelated
parent re-renders.
src/features/workspaces/kernel/workspace-kernel-events.ts (1)

84-104: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silent error swallowing in the malformed-payload fallback.

The catch block degrades any failure from mapKernelEventRow to a null-payload event, which is correct per the PR's stated goal of tolerating malformed/unknown payloads. However, it swallows the error entirely with no logging, so genuine bugs in parseWorkspaceEventPayload (unrelated to malformed input) would be indistinguishable from expected degradation and go unnoticed in production. purgeForDeletion in workspace-kernel.ts already uses console.warn for a similar graceful-degradation case — consider following that pattern here for consistency.

♻️ Suggested logging addition
-		} catch {
+		} catch (error) {
 			// Malformed payloads must never break the history view; realtime sync
 			// keeps its strict parsing.
+			console.warn("[WorkspaceKernelEventBus] Failed to parse history event payload", {
+				id: row.id,
+				revision: row.revision,
+				type: row.type,
+				error,
+			});
 			return {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/kernel/workspace-kernel-events.ts` around lines 84 -
104, The fallback in mapHistoryRow currently hides every failure from
mapKernelEventRow by returning a null-payload event, which makes real parsing
bugs invisible. Add a warning log inside the catch in
workspace-kernel-events.ts, following the same graceful-degradation style used
by purgeForDeletion in workspace-kernel.ts, and include enough context (for
example the row id/type and the caught error) so malformed input is still
tolerated but unexpected failures are visible. Keep the null-payload fallback
behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/features/workspaces/components/WorkspaceVersionHistoryDialog.tsx`:
- Around line 57-62: Memoize both actorNamesById and entries in
WorkspaceVersionHistoryDialog so they are only recomputed when their inputs
change. Wrap the Map creation and the mapWorkspaceHistoryEvents result in
useMemo, keyed off membersQuery.data and historyQuery.data?.pages respectively,
to avoid repeating the burst-grouping pass and Map construction on unrelated
parent re-renders.

In `@src/features/workspaces/kernel/workspace-kernel-events.ts`:
- Around line 84-104: The fallback in mapHistoryRow currently hides every
failure from mapKernelEventRow by returning a null-payload event, which makes
real parsing bugs invisible. Add a warning log inside the catch in
workspace-kernel-events.ts, following the same graceful-degradation style used
by purgeForDeletion in workspace-kernel.ts, and include enough context (for
example the row id/type and the caught error) so malformed input is still
tolerated but unexpected failures are visible. Keep the null-payload fallback
behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9eb21fa4-819f-43f4-b41e-0b0fa8f0357c

📥 Commits

Reviewing files that changed from the base of the PR and between fa45500 and 7313134.

📒 Files selected for processing (11)
  • src/features/workspaces/components/WorkspaceContextBar.tsx
  • src/features/workspaces/components/WorkspaceVersionHistoryDialog.tsx
  • src/features/workspaces/kernel/workspace-kernel-access.ts
  • src/features/workspaces/kernel/workspace-kernel-events.test.ts
  • src/features/workspaces/kernel/workspace-kernel-events.ts
  • src/features/workspaces/kernel/workspace-kernel-types.ts
  • src/features/workspaces/kernel/workspace-kernel.ts
  • src/features/workspaces/model/workspace-history.test.ts
  • src/features/workspaces/model/workspace-history.ts
  • src/features/workspaces/server/functions.ts
  • src/features/workspaces/server/queries.ts

- Keep actor labels neutral ("Someone") until the member list has
  loaded so a slow or failed members query cannot mislabel current
  members as "Former member" in the timeline.
- Memoize timeline entries and the actor-name map; the dialog is
  mounted alongside the context bar, so unrelated parent re-renders
  were repeating the burst-grouping pass.
- Log a warning when a history event payload fails to map, matching
  the kernel's existing graceful-degradation pattern, instead of
  swallowing the error silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/model/workspace-history.test.ts">

<violation number="1" location="src/features/workspaces/model/workspace-history.test.ts:9">
P3: The `nextRevision` counter at module scope (line 9) is decremented across every test that calls `historyEvent` without an explicit revision. This creates shared mutable state between tests — a classic testing smell that can produce order-dependent results. While none of the current assertions rely on the generated revision, future test writers may not realize that default revision values shift as tests are added, reordered, or shuffled. A simple `beforeEach(() => { nextRevision = 1000; })` or a local counter scoped to each test would keep the suite cleanly isolated.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

type WorkspaceHistoryEvent,
} from "#/features/workspaces/model/workspace-history";

let nextRevision = 1000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The nextRevision counter at module scope (line 9) is decremented across every test that calls historyEvent without an explicit revision. This creates shared mutable state between tests — a classic testing smell that can produce order-dependent results. While none of the current assertions rely on the generated revision, future test writers may not realize that default revision values shift as tests are added, reordered, or shuffled. A simple beforeEach(() => { nextRevision = 1000; }) or a local counter scoped to each test would keep the suite cleanly isolated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/model/workspace-history.test.ts, line 9:

<comment>The `nextRevision` counter at module scope (line 9) is decremented across every test that calls `historyEvent` without an explicit revision. This creates shared mutable state between tests — a classic testing smell that can produce order-dependent results. While none of the current assertions rely on the generated revision, future test writers may not realize that default revision values shift as tests are added, reordered, or shuffled. A simple `beforeEach(() => { nextRevision = 1000; })` or a local counter scoped to each test would keep the suite cleanly isolated.</comment>

<file context>
@@ -0,0 +1,238 @@
+	type WorkspaceHistoryEvent,
+} from "#/features/workspaces/model/workspace-history";
+
+let nextRevision = 1000;
+
+function historyEvent(input: {
</file context>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

Add workspace version history backed by kernel revisions

2 participants