diff --git a/apps/web/package.json b/apps/web/package.json
index 958f0d540d..bed8262eb9 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -23,9 +23,11 @@
 		"vite": "catalog:"
 	},
 	"dependencies": {
+		"@rails/actioncable": "^7.2.100",
 		"@sentry/sveltekit": "^8.9.2",
 		"highlight.js": "^11.10.0",
 		"marked": "^10.0.0",
-		"moment": "^2.30.1"
+		"moment": "^2.30.1",
+		"svelte-gravatar": "^1.0.3"
 	}
 }
diff --git a/apps/web/src/lib/components/DiffPatch.svelte b/apps/web/src/lib/components/DiffPatch.svelte
new file mode 100644
index 0000000000..11f08a29b6
--- /dev/null
+++ b/apps/web/src/lib/components/DiffPatch.svelte
@@ -0,0 +1,304 @@
+<script lang="ts">
+	// Define types for the parsed lines and hunk information
+	type DiffLine = {
+		type: 'added' | 'removed' | 'context' | 'header' | 'hunk';
+		content: string;
+		lineNumber: number;
+		leftLineNumber: number | null; // Use null for lines without line numbers
+		rightLineNumber: number | null; // Use null for lines without line numbers
+	};
+
+	// Props for the component (with type annotations)
+	export let diff: string = '';
+	export let diffPath: string = '';
+	export let diffSha: string = '';
+
+	// eslint-disable-next-line func-style
+	export let onRangeSelect: (range: string, diff_path: string, diff_sha: string) => void = () => {};
+
+	let selectedRange: { startLine: number | null; endLine: number | null } = {
+		startLine: null,
+		endLine: null
+	};
+
+	$: selectedRange.startLine === null; // just to trigger reactivity
+
+	// Handle click event on the gutter line number
+	function handleLineNumberClick(line: DiffLine, event: MouseEvent) {
+		if (line === null) return;
+		if (line.type === 'header' || line.type === 'hunk') return;
+		/* dont highlight text when clicking on line number */
+		document.getSelection()?.removeAllRanges();
+
+		// Check if Shift key is held to extend selection range
+		if (event.shiftKey && selectedRange.startLine !== null) {
+			if (line.lineNumber < selectedRange.startLine) {
+				selectedRange.endLine = selectedRange.startLine;
+				selectedRange.startLine = line.lineNumber;
+			} else {
+				selectedRange.endLine = line.lineNumber;
+			}
+			let range = rangeToString(selectedRange);
+			onRangeSelect(range, diffPath, diffSha);
+		} else {
+			// Start a new selection range
+			if (selectedRange.startLine === line.lineNumber) {
+				selectedRange = { startLine: null, endLine: null };
+				onRangeSelect('', '', '');
+			} else {
+				selectedRange = { startLine: line.lineNumber, endLine: null };
+				let range = rangeToString(selectedRange);
+				onRangeSelect(range, diffPath, diffSha);
+			}
+		}
+	}
+
+	function rangeToString(range: { startLine: number | null; endLine: number | null }): string {
+		let rangeString = '';
+		parsedLines.forEach((line) => {
+			if (line.lineNumber === range.startLine) {
+				if (line.leftLineNumber !== null) {
+					rangeString = `L${line.leftLineNumber}`;
+				}
+				if (line.rightLineNumber !== null) {
+					rangeString = `R${line.rightLineNumber}`;
+				} else {
+					rangeString = ''; // selected a header or something
+				}
+			}
+		});
+		if (range.endLine !== null) {
+			parsedLines.forEach((line) => {
+				if (line.lineNumber === range.endLine) {
+					if (line.leftLineNumber !== null) {
+						rangeString += `-L${line.leftLineNumber}`;
+					} else {
+						rangeString += `-R${line.rightLineNumber}`;
+					}
+				}
+			});
+		}
+		return rangeString;
+	}
+
+	// Function to parse the diff string and extract meaningful lines and line numbers
+	function parseDiff(diff: string): DiffLine[] {
+		const lines = diff.split('\n');
+		const parsedLines: DiffLine[] = [];
+		let lineNumber: number = 0;
+		let leftLineNumber: number = 0;
+		let rightLineNumber: number = 0;
+
+		for (const line of lines) {
+			lineNumber++;
+			// Skip the diff header lines
+			if (
+				line.startsWith('diff ') ||
+				line.startsWith('index ') ||
+				line.startsWith('---') ||
+				line.startsWith('+++')
+			) {
+				parsedLines.push({
+					type: 'header',
+					content: line,
+					lineNumber,
+					leftLineNumber: null,
+					rightLineNumber: null
+				});
+				continue;
+			}
+
+			// If the line starts with '@@', it's a hunk header; extract the starting line number
+			if (line.startsWith('@@')) {
+				const match = line.match(/@@ -(\d+)(,\d+)? \+(\d+)(,\d+)? @@/);
+				if (match) {
+					console.log(match);
+					leftLineNumber = parseInt(match[1], 10);
+					rightLineNumber = parseInt(match[3], 10);
+				}
+				parsedLines.push({
+					type: 'hunk',
+					content: line,
+					lineNumber,
+					leftLineNumber: null,
+					rightLineNumber: null
+				}); // Display hunk header with no line number
+				continue;
+			}
+
+			// Determine the type of each line and assign line numbers accordingly
+			let type: 'added' | 'removed' | 'context' = 'context';
+			let showLeftLineNumber: number | null = null;
+			let showRightLineNumber: number | null = null;
+			if (line.startsWith('+') && !line.startsWith('+++')) {
+				type = 'added';
+				rightLineNumber++;
+				showRightLineNumber = rightLineNumber - 1;
+			} else if (line.startsWith('-') && !line.startsWith('---')) {
+				type = 'removed';
+				leftLineNumber++;
+				showLeftLineNumber = leftLineNumber - 1;
+			} else {
+				type = 'context';
+				rightLineNumber++;
+				leftLineNumber++;
+				showLeftLineNumber = leftLineNumber - 1;
+				showRightLineNumber = rightLineNumber - 1;
+			}
+
+			parsedLines.push({
+				type,
+				content: line,
+				lineNumber,
+				leftLineNumber: showLeftLineNumber,
+				rightLineNumber: showRightLineNumber
+			});
+		}
+
+		return parsedLines;
+	}
+
+	function inRangeClass(lineNumber: number): string {
+		if (selectedRange.startLine === null) {
+			return '';
+		}
+		if (selectedRange.endLine === null) {
+			if (lineNumber === selectedRange.startLine) {
+				return 'inRange startRange endRange';
+			}
+			return '';
+		}
+		if (lineNumber >= selectedRange.startLine && lineNumber <= selectedRange.endLine) {
+			let rangeClasses = 'inRange';
+			if (lineNumber === selectedRange.startLine) {
+				rangeClasses += ' startRange';
+			}
+			if (lineNumber === selectedRange.endLine) {
+				rangeClasses += ' endRange';
+			}
+			return rangeClasses;
+		}
+		return '';
+	}
+
+	// Store parsed lines in a reactive variable
+	let parsedLines: DiffLine[] = parseDiff(diff);
+</script>
+
+<div class="diff-container">
+	<!-- Gutter with line numbers -->
+	<div class="gutter">
+		{#each parsedLines as line}
+			{#if line.type !== 'header'}
+				<!-- svelte-ignore a11y_click_events_have_key_events -->
+				<!-- svelte-ignore a11y_no_static_element_interactions -->
+				<div
+					class={`gutterEntry ${line.type}`}
+					on:click={(event) => handleLineNumberClick(line, event)}
+				>
+					{line.leftLineNumber !== null ? line.leftLineNumber : ' '}
+				</div>
+			{/if}
+		{/each}
+	</div>
+
+	<div class="gutter">
+		{#each parsedLines as line}
+			{#if line.type !== 'header'}
+				<!-- svelte-ignore a11y_click_events_have_key_events -->
+				<!-- svelte-ignore a11y_no_static_element_interactions -->
+				<div
+					class={`gutterEntry ${line.type}`}
+					on:click={(event) => handleLineNumberClick(line, event)}
+				>
+					{line.rightLineNumber !== null ? line.rightLineNumber : ' '}
+				</div>
+			{/if}
+		{/each}
+	</div>
+
+	<!-- Content of the diff -->
+	<div class="content">
+		{#each parsedLines as line}
+			{#if line.type !== 'header'}
+				<div class={`line ${line.type} ${inRangeClass(line.lineNumber)}`}>
+					͏{line.content}
+				</div>
+			{/if}
+		{/each}
+	</div>
+</div>
+
+<style>
+	.diff-container {
+		display: flex;
+		font-family: monospace;
+	}
+
+	.gutter {
+		width: 50px;
+		background-color: #f7f7f7;
+		padding: 0 10px;
+		text-align: right;
+		color: #999;
+		border-right: 1px solid #ddd;
+		cursor: pointer;
+	}
+
+	.content {
+		width: 100%;
+		overflow-x: auto;
+	}
+
+	.line {
+		display: flex;
+		white-space: pre; /* Preserve whitespace */
+		padding: 4px;
+	}
+
+	.line.added {
+		background-color: #e6ffed;
+	}
+
+	.line.removed {
+		background-color: #ffeef0;
+	}
+
+	.line.header {
+		color: #999;
+	}
+
+	.line.hunk {
+		color: #556;
+		background-color: #cef;
+	}
+
+	.gutterEntry {
+		padding: 4px;
+	}
+
+	.startRange {
+		border-top: 2px solid #2076e7;
+	}
+
+	.endRange {
+		border-bottom: 2px solid #2076e7;
+	}
+
+	.inRange {
+		border-left: 2px solid #2076e7;
+		border-right: 2px solid #2076e7;
+		background-color: #e4e4e4;
+		color: #000000;
+	}
+
+	.inRange.line.added {
+		background-color: #9be19b;
+		color: #1e4505;
+	}
+
+	.inRange.line.removed {
+		background-color: #f0bcc2;
+		color: rgb(79, 5, 5);
+	}
+</style>
diff --git a/apps/web/src/lib/components/DiffPatchArray.svelte b/apps/web/src/lib/components/DiffPatchArray.svelte
new file mode 100644
index 0000000000..94b1f6d291
--- /dev/null
+++ b/apps/web/src/lib/components/DiffPatchArray.svelte
@@ -0,0 +1,46 @@
+<script lang="ts">
+	// Props for the component (with type annotations)
+	export let diffArray: string[] = [];
+	console.log(diffArray);
+</script>
+
+<div class="diff">
+	<div class="gutter">
+		{#each diffArray as line}
+			<div class="line">{line.right}</div>
+		{/each}
+	</div>
+	<div class="lines">
+		{#each diffArray as line}
+			<div class="line {line.type}">{line.line}</div>
+		{/each}
+	</div>
+</div>
+
+<style>
+	.diff {
+		display: flex;
+		font-family: monospace;
+		white-space: pre;
+		background-color: #fff;
+		padding: 8px;
+		border: 1px solid #ccc;
+		border-radius: 10px;
+		width: 100%;
+	}
+	.gutter {
+		padding: 0 8px;
+	}
+	.lines {
+		width: 100%;
+	}
+	.line {
+		padding: 2px;
+	}
+	.diff .added {
+		background-color: #dfd;
+	}
+	.diff .removed {
+		background-color: #fdd;
+	}
+</style>
diff --git a/apps/web/src/lib/components/Navigation.svelte b/apps/web/src/lib/components/Navigation.svelte
index 9e5757e14e..1110628825 100644
--- a/apps/web/src/lib/components/Navigation.svelte
+++ b/apps/web/src/lib/components/Navigation.svelte
@@ -19,8 +19,8 @@
 
 <header>
 	<a href="/" class="nav__left">
-		<img src={GitButler} width="48" alt="github" />
-		<h2>GitButler</h2>
+		<img src={GitButler} width="32" alt="gitbutler" />
+		<div class="word-mark">GitButler</div>
 	</a>
 	<div>
 		<a href="/downloads">Downloads</a>
@@ -45,13 +45,18 @@
 
 <style>
 	header {
-		max-width: 64rem;
 		width: 100%;
 		margin: 0 auto;
 		display: flex;
 		justify-content: space-between;
 		align-items: center;
-		padding: 16px;
+		padding: 8px;
+		background: var(--clr-bg-1-muted);
+	}
+
+	.word-mark {
+		font-size: 1.5rem;
+		font-weight: bold;
 	}
 
 	a {
diff --git a/apps/web/src/lib/styles/global.css b/apps/web/src/lib/styles/global.css
index 452518c9ca..6d0b5e734e 100644
--- a/apps/web/src/lib/styles/global.css
+++ b/apps/web/src/lib/styles/global.css
@@ -21,14 +21,7 @@ body {
 	min-height: 100vh;
 	margin: 0;
 	background-attachment: fixed;
-	background-color: var(--color-bg-1);
 	background-size: 100vw 100vh;
-	background-image: radial-gradient(
-			50% 50% at 50% 50%,
-			rgba(255, 255, 255, 0.75) 0%,
-			rgba(255, 255, 255, 0) 100%
-		),
-		linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%);
 }
 
 h1,
diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte
index a95ae031e7..be50284023 100644
--- a/apps/web/src/routes/+layout.svelte
+++ b/apps/web/src/routes/+layout.svelte
@@ -21,6 +21,7 @@
 			const token = $page.url.searchParams.get('gb_access_token');
 			if (token && token.length > 0) {
 				$page.data.authService.setToken(token);
+				localStorage.setItem('gb_access_token', token);
 
 				$page.url.searchParams.delete('gb_access_token');
 				goto(`?${$page.url.searchParams.toString()}`);
@@ -35,10 +36,6 @@
 	<main>
 		{@render children()}
 	</main>
-
-	<footer>
-		<p>GitButler</p>
-	</footer>
 </div>
 
 <style>
@@ -57,18 +54,4 @@
 		max-width: 84rem;
 		margin: 0 auto;
 	}
-
-	footer {
-		display: flex;
-		flex-direction: column;
-		justify-content: center;
-		align-items: center;
-		padding: 12px;
-	}
-
-	@media (min-width: 480px) {
-		footer {
-			padding: 12px 0;
-		}
-	}
 </style>
diff --git a/apps/web/src/routes/projects/[projectId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/+page.svelte
index 9ff60573ea..670e7e3666 100644
--- a/apps/web/src/routes/projects/[projectId]/+page.svelte
+++ b/apps/web/src/routes/projects/[projectId]/+page.svelte
@@ -94,7 +94,7 @@
 	<div>{project.name}</div>
 	<div class="columns">
 		<div class="column">
-			<h2>Patch Stacks</h2>
+			<h3>Patch Stacks</h3>
 			{#each patchStacks as stack}
 				<div>
 					{stack.title}<br />
diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte
index 58a7585f0e..d3fae94529 100644
--- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte
+++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/+page.svelte
@@ -1,7 +1,9 @@
 <script lang="ts">
 	import moment from 'moment';
 	import { onMount } from 'svelte';
+	import Gravatar from 'svelte-gravatar';
 	import { env } from '$env/dynamic/public';
+
 	// load moment
 	let state = 'loading';
 	let stackData: any = {};
@@ -48,7 +50,7 @@
 {:else}
 	<div><a href="/projects/{data.projectId}">project</a></div>
 
-	<h1>Patch Stack</h1>
+	<h2>Patch Stack</h2>
 	<div class="columns">
 		<div class="column">
 			Title: <strong>{stackData.title}</strong><br />
@@ -73,23 +75,79 @@
 
 	<h2>Patches</h2>
 	{#each stackData.patches as patch}
-		<div class="columns patch">
-			<div class="column">
-				<div>Title: <strong>{patch.title}</strong></div>
-				<div>Change Id: <code><a href="./stack/{patch.change_id}">{patch.change_id}</a></code></div>
-				<div>Commit: <code>{patch.commit_sha}</code></div>
-				<div>Version: {patch.version}</div>
-				<div><strong>Files:</strong></div>
-				{#each patch.statistics.files as file}
-					<div><code>{file}</code></div>
-				{/each}
-			</div>
-			<div class="column">
-				<div>Created: <span class="dtime">{patch.created_at}</span></div>
-				<div>Contributors: {patch.contributors}</div>
-				<div>
-					Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch
-						.statistics.deletions}, Files: {patch.statistics.file_count}
+		<div class="patch">
+			{#if patch.review_all.rejected.length > 0}
+				<div class="patchHeader rejected">X</div>
+			{:else if patch.review_all.signed_off.length > 0}
+				<div class="patchHeader signoff">✓</div>
+			{:else}
+				<div class="patchHeader unreviewed">?</div>
+			{/if}
+			<div class="columns patchData">
+				<div class="column">
+					<div>Title: <strong>{patch.title}</strong></div>
+					<div>
+						Change Id: <code><a href="./stack/{patch.change_id}">{patch.change_id}</a></code>
+					</div>
+					<div>Commit: <code>{patch.commit_sha}</code></div>
+					<div>Version: {patch.version}</div>
+					<div><strong>Files:</strong></div>
+					{#each patch.statistics.files as file}
+						<div><code>{file}</code></div>
+					{/each}
+				</div>
+				<div class="column">
+					<div>Created: <span class="dtime">{patch.created_at}</span></div>
+					<div>Contributors: {patch.contributors}</div>
+					<div>
+						Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch
+							.statistics.deletions}, Files: {patch.statistics.file_count}
+					</div>
+					<hr />
+					<div class="columns">
+						<div class="column">
+							<h3>This Version</h3>
+							<div>
+								<div class="title">Viewed:</div>
+								{#each patch.review.viewed as email}
+									<Gravatar {email} size={20} />
+								{/each}
+							</div>
+							<div>
+								<div class="title">Signed Off:</div>
+								{#each patch.review.signed_off as email}
+									<Gravatar {email} size={20} />
+								{/each}
+							</div>
+							<div>
+								<div class="title">Rejected:</div>
+								{#each patch.review.rejected as email}
+									<Gravatar {email} size={20} />
+								{/each}
+							</div>
+						</div>
+						<div class="column">
+							<h3>All Versions</h3>
+							<div>
+								<div class="title">Viewed:</div>
+								{#each patch.review_all.viewed as email}
+									<Gravatar {email} size={20} />
+								{/each}
+							</div>
+							<div>
+								<div class="title">Signed Off:</div>
+								{#each patch.review_all.signed_off as email}
+									<Gravatar {email} size={20} />
+								{/each}
+							</div>
+							<div>
+								<div class="title">Rejected:</div>
+								{#each patch.review_all.rejected as email}
+									<Gravatar {email} size={20} />
+								{/each}
+							</div>
+						</div>
+					</div>
 				</div>
 			</div>
 		</div>
@@ -103,6 +161,10 @@
 	h2 {
 		font-size: 1.5rem;
 	}
+	h3 {
+		font-size: 1.1rem;
+		font-weight: bold;
+	}
 	.columns {
 		display: flex;
 	}
@@ -113,10 +175,33 @@
 		margin: 4px 0;
 	}
 	.patch {
-		background-color: #fff;
+		background-color: var(--clr-bg-1-muted);
 		border: 1px solid #ccc;
-		padding: 15px 20px;
 		margin: 10px 0;
 		border-radius: 10px;
 	}
+	.patchData {
+		padding: 15px 20px;
+	}
+	.patchHeader {
+		padding: 5px;
+		border-radius: 5px 5px 0 0;
+	}
+	.rejected {
+		background-color: rgb(224, 92, 92);
+		color: white;
+	}
+	.signoff {
+		background-color: rgb(77, 219, 77);
+		color: white;
+	}
+	.unreviewed {
+		background-color: rgb(215, 215, 144);
+		color: black;
+	}
+	.title {
+		min-width: 100px;
+		display: inline-block;
+		border-right: 1px solid #eee;
+	}
 </style>
diff --git a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte
index a8f0359f64..70f20be350 100644
--- a/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte
+++ b/apps/web/src/routes/projects/[projectId]/branches/[branchId]/stack/[changeId]/+page.svelte
@@ -1,23 +1,34 @@
 <script lang="ts">
-	import hljs from 'highlight.js';
+	import DiffPatch from '$lib/components/DiffPatch.svelte';
+	import DiffPatchArray from '$lib/components/DiffPatchArray.svelte';
+	import { createConsumer } from '@rails/actioncable';
 	import { marked } from 'marked';
 	import { onMount } from 'svelte';
+	import Gravatar from 'svelte-gravatar';
 	import { env } from '$env/dynamic/public';
 
 	let state = 'loading';
 	let patch: any = {};
 	let stack: any = {};
+	let status: any = {};
+	let events: any = [];
 	let key: any = '';
 	let uuid: any = '';
+	let consumer;
 
 	export let data: any;
 
 	onMount(() => {
 		key = localStorage.getItem('gb_access_token');
+
+		listenToChat(key);
+
 		let projectId = data.projectId;
 		let branchId = data.branchId;
 		let changeId = data.changeId;
 
+		// scroll chatWindow to bottom
+
 		if (key) {
 			fetch(env.PUBLIC_APP_HOST + 'api/patch_stack/' + projectId + '/' + branchId, {
 				method: 'GET',
@@ -31,12 +42,36 @@
 					stack = data;
 					uuid = data.uuid;
 					fetchPatch(data.uuid, changeId, key);
+					getPatchStatus();
+					fetchAndUpdateChat();
 				});
 		} else {
 			state = 'unauthorized';
 		}
 	});
 
+	function listenToChat(token: string) {
+		// connect to actioncable to subscribe to chat events
+		let wsHost = env.PUBLIC_APP_HOST.replace('http', 'ws') + 'cable';
+		consumer = createConsumer(wsHost + '?token=' + token);
+		consumer.subscriptions.create(
+			{ channel: 'ChatChannel', change_id: data.changeId, project_id: data.projectId },
+			{
+				received(data: any) {
+					// todo: update chat window with new message
+					console.log('received', data);
+				}
+			}
+		);
+	}
+
+	function scrollToBottom() {
+		let chatWindow = document.querySelector<HTMLElement>('.chatWindow');
+		if (chatWindow) {
+			chatWindow.scrollTop = chatWindow.scrollHeight;
+		}
+	}
+
 	function fetchPatch(uuid: string, changeId: string, key: string) {
 		fetch(env.PUBLIC_APP_HOST + 'api/patch_stack/' + uuid + '/patch/' + changeId, {
 			method: 'GET',
@@ -51,8 +86,6 @@
 				state = 'loaded';
 				// wait a second
 				setTimeout(() => {
-					console.log('Highlighting code');
-					hljs.highlightAll();
 					// render markdowns
 					let markdowns = document.querySelectorAll('.markdown');
 					markdowns.forEach((markdown) => {
@@ -144,6 +177,7 @@
 				});
 		}
 	}
+
 	function moveSection(position: number, change: number) {
 		console.log('Moving section at position', position, 'by', change);
 		let ids = patch.sections.map((section: any) => section.identifier);
@@ -209,11 +243,162 @@
 			}
 		}
 	}
+
 	function updatePatch() {
 		setTimeout(() => {
 			fetchPatch(uuid, data.changeId, key);
 		}, 500);
 	}
+
+	function getPatchStatus() {
+		//GET        /api/patch_stack/:project_id/:branch_id/patch_status
+		fetch(
+			env.PUBLIC_APP_HOST +
+				'api/patch_stack/' +
+				data.projectId +
+				'/' +
+				data.branchId +
+				'/patch_status',
+			{
+				method: 'GET',
+				headers: {
+					'X-AUTH-TOKEN': key || ''
+				}
+			}
+		)
+			.then(async (response) => await response.json())
+			.then((data) => {
+				status = data;
+				console.log('patch status');
+				console.log(data);
+			});
+	}
+
+	function fetchAndUpdateChat() {
+		fetch(env.PUBLIC_APP_HOST + 'api/patch_events/' + data.projectId + '/patch/' + data.changeId, {
+			method: 'GET',
+			headers: {
+				'X-AUTH-TOKEN': key || ''
+			}
+		})
+			.then(async (response) => await response.json())
+			.then((data) => {
+				console.log(data);
+				setTimeout(() => {
+					events = data;
+					setTimeout(() => {
+						scrollToBottom();
+					}, 150); // I don't know how to DOM in Svelte, but it takes a second
+				}, 50); // I don't know how to DOM in Svelte, but it takes a second
+			});
+	}
+
+	function createChatMessage() {
+		let chatBox = document.querySelector<HTMLElement>('.chatBox');
+		if (chatBox) {
+			// check if this is an issue
+			var checkbox = document.getElementById('issue');
+			let is_issue = false;
+			if ((checkbox as HTMLInputElement).checked) {
+				is_issue = true;
+			}
+
+			let text = chatBox.querySelector('textarea')!.value;
+			let params: {
+				chat: string;
+				change_id: any;
+				range?: string;
+				diff_path?: string;
+				diff_sha?: string;
+				issue?: boolean;
+			} = {
+				chat: text,
+				change_id: data.changeId,
+				issue: is_issue
+			};
+			if (commentRange.length > 0) {
+				params.range = commentRange;
+				params.diff_path = diffPath;
+				params.diff_sha = diffSha;
+			}
+			let opts = {
+				method: 'POST',
+				headers: {
+					'X-AUTH-TOKEN': key || '',
+					'Content-Type': 'application/json'
+				},
+				body: JSON.stringify(params)
+			};
+			if (key) {
+				fetch(
+					env.PUBLIC_APP_HOST + 'api/chat_messages/' + data.projectId + '/branch/' + data.branchId,
+					opts
+				)
+					.then(async (response) => await response.json())
+					.then((data) => {
+						chatBox.querySelector('textarea')!.value = '';
+						fetchAndUpdateChat();
+						console.log(data);
+					});
+			}
+		}
+	}
+
+	function signOff(signoff: boolean) {
+		let opts = {
+			method: 'PATCH',
+			headers: {
+				'X-AUTH-TOKEN': key || '',
+				'Content-Type': 'application/json'
+			},
+			body: JSON.stringify({
+				sign_off: signoff
+			})
+		};
+		if (key) {
+			fetch(env.PUBLIC_APP_HOST + 'api/patch_stack/' + uuid + '/patch/' + data.changeId, opts)
+				.then(async (response) => await response.json())
+				.then((data) => {
+					console.log('sign off', data);
+					getPatchStatus();
+					fetchAndUpdateChat();
+				});
+		}
+	}
+
+	function resolveIssue(uuid: string) {
+		//:project_id/chat/:chat_uuid
+		console.log('Resolving issue', uuid);
+		let opts = {
+			method: 'PATCH',
+			headers: {
+				'X-AUTH-TOKEN': key || '',
+				'Content-Type': 'application/json'
+			},
+			body: JSON.stringify({
+				resolved: true
+			})
+		};
+		if (key) {
+			fetch(env.PUBLIC_APP_HOST + 'api/chat_messages/' + data.projectId + '/chat/' + uuid, opts)
+				.then(async (response) => await response.json())
+				.then((data) => {
+					console.log('resolved', data);
+					fetchAndUpdateChat();
+				});
+		}
+	}
+
+	let commentRange: string = '';
+	let diffPath: string = '';
+	let diffSha: string = '';
+
+	function rangeSelect(range: string, diff_path: string, diff_sha: string) {
+		console.log('range selected', range, diff_path, diff_sha);
+		commentRange = range;
+		diffPath = diff_path;
+		diffSha = diff_sha;
+	}
 </script>
 
 {#if state === 'loading'}
@@ -221,58 +406,103 @@
 {:else if state === 'unauthorized'}
 	<p>Unauthorized</p>
 {:else}
-	<h2>Patch Stack: <a href="../stack">{stack.title}</a></h2>
-	{#each stack.patches as stackPatch}
-		<div>
-			<code
-				><a href="/projects/{data.projectId}/branches/{data.branchId}/stack/{stackPatch.change_id}"
-					>{stackPatch.change_id.substr(0, 8)}</a
-				></code
-			>:
-			{#if patch.change_id === stackPatch.change_id}
-				<strong>{stackPatch.title}</strong>
-			{:else}
-				{stackPatch.title}
-			{/if}
-		</div>
-	{/each}
-	<hr />
-
-	<h2>Patch</h2>
-	<div class="columns">
-		<div class="column">
-			<div>Title: <strong>{patch.title}</strong></div>
-			{#if patch.description}
-				<div>Desc: {patch.description}</div>
-			{/if}
-			<div>Change Id: <code>{patch.change_id}</code></div>
-			<div>Commit: <code>{patch.commit_sha}</code></div>
-		</div>
-		<div class="column">
-			<div>Patch Version: {patch.version}</div>
-			<div>Stack Position: {patch.position + 1}/{stack.stack_size}</div>
-			<div>Contributors: {patch.contributors}</div>
-			<div>
-				Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch
-					.statistics.deletions}, Files: {patch.statistics.file_count}
-			</div>
-		</div>
-	</div>
-
 	<div class="columns">
-		<div class="column outline">
-			<h3>Outline</h3>
-			<div class="sections">
-				{#each patch.sections as section}
-					{#if section.section_type === 'diff'}
-						<div><a href="#section-{section.id}">{section.new_path}</a></div>
+		<div class="column patchArea">
+			<h3>Patch Series: <a href="../stack">{stack.title}</a></h3>
+			{#each stack.patches as stackPatch}
+				<div>
+					<code
+						><a
+							rel="external"
+							href="/projects/{data.projectId}/branches/{data.branchId}/stack/{stackPatch.change_id}"
+							>{stackPatch.change_id.substr(0, 8)}</a
+						></code
+					>:
+					{#if patch.change_id === stackPatch.change_id}
+						<strong>{stackPatch.title}</strong>
 					{:else}
-						<div><a href="#section-{section.id}">{section.title}</a></div>
+						{stackPatch.title}
 					{/if}
-				{/each}
+				</div>
+			{/each}
+			<hr />
+
+			<h3>Patch</h3>
+			<div class="columns">
+				<div class="column">
+					<div>Title: <strong>{patch.title}</strong></div>
+					{#if patch.description}
+						<div>Desc: {patch.description}</div>
+					{/if}
+					<div>Change Id: <code>{patch.change_id.substr(0, 13)}</code></div>
+					<div>Commit SHA: <code>{patch.commit_sha.substr(0, 10)}</code></div>
+					<div>Patch Version: {patch.version}</div>
+					<div>Series Position: {patch.position + 1}/{stack.stack_size}</div>
+					<div>
+						Contributors:
+						{#each patch.contributors as email}
+							<Gravatar {email} size={20} />
+						{/each}
+					</div>
+					<div>
+						Additions: {patch.statistics.lines - patch.statistics.deletions}, Deletions: {patch
+							.statistics.deletions}, Files: {patch.statistics.file_count}
+					</div>
+				</div>
+				<div class="column">
+					{#if patch.review.viewed.length > 0}
+						<div>
+							<div class="title">Viewed:</div>
+							{#each patch.review.viewed as email}
+								<Gravatar {email} size={20} />
+							{/each}
+						</div>
+					{/if}
+
+					{#if patch.review.signed_off.length > 0}
+						<div>
+							<div class="title">Signed Off:</div>
+							{#each patch.review.signed_off as email}
+								<Gravatar {email} size={20} />
+							{/each}
+						</div>
+					{/if}
+
+					{#if patch.review.length > 0}
+						<div>
+							<div class="title">Rejected:</div>
+							{#each patch.review.rejected as email}
+								<Gravatar {email} size={20} />
+							{/each}
+						</div>
+					{/if}
+
+					<h3>Your sign off status</h3>
+					{#if status[data.changeId]}
+						{#if status[data.changeId].last_signoff}
+							<div class="signoff">You signed off on this patch</div>
+						{/if}
+						{#if !status[data.changeId].last_reviewed}
+							<div class="rejected">You have not reviewed this patch</div>
+						{:else if !status[data.changeId].last_signoff}
+							<div class="rejected">You have rejected this patch</div>
+						{/if}
+					{/if}
+					<div>
+						{#if status[data.changeId]}
+							{#if !status[data.changeId].last_signoff}
+								<button class="button" on:click={() => signOff(true)}>Sign Off</button>
+							{/if}
+							{#if status[data.changeId].last_signoff || !status[data.changeId].last_reviewed}
+								<button class="button" on:click={() => signOff(false)}>Reject</button>
+							{/if}
+						{/if}
+					</div>
+				</div>
 			</div>
-		</div>
-		<div class="column">
+
+			<hr />
+
 			<div class="patch">
 				{#each patch.sections as section}
 					<div id="section-{section.id}">
@@ -286,10 +516,18 @@
 									>down</button
 								>]
 							</div>
+							<div class="filePath">
+								{section.new_path}
+							</div>
 							<div>
-								<strong>{section.new_path}</strong>
+								<DiffPatch
+									onRangeSelect={(range, diff_path, diff_sha) =>
+										rangeSelect(range, diff_path, diff_sha)}
+									diffPath={section.new_path}
+									diffSha={section.diff_sha}
+									diff={section.diff_patch}
+								/>
 							</div>
-							<div><pre><code>{section.diff_patch}</code></pre></div>
 						{:else}
 							<div class="right">
 								<button class="action" on:click={() => addSection(section.position)}>add</button>
@@ -316,14 +554,168 @@
 				</div>
 			</div>
 		</div>
+		<div class="column chatArea">
+			<h3>Chat</h3>
+			<div class="chatWindow">
+				{#each events as event}
+					{#if event.event_type === 'chat'}
+						<div
+							class="chatEntry {event.object.issue ? 'issue' : ''} {event.object.resolved
+								? 'resolved'
+								: ''}"
+						>
+							<div class="chatHeader">
+								<div class="avatar">
+									<Gravatar email={event.object.user.email} size={20} />
+								</div>
+								<div>{event.object.created_at}</div>
+							</div>
+							{#if event.object.diff_patch_array}
+								<div>
+									<div class="diffPath">{event.object.diff_path}</div>
+									<!-- {chat.diff_sha} -->
+									<DiffPatchArray diffArray={event.object.diff_patch_array} />
+								</div>
+							{/if}
+							<div class="chatComment">{event.object.comment}</div>
+							{#if event.object.issue}
+								{#if event.object.resolved}
+									<div class="right">resolved</div>
+								{:else}
+									<button class="action" on:click={() => resolveIssue(event.object.uuid)}
+										>resolve</button
+									>
+								{/if}
+							{/if}
+						</div>
+					{/if}
+					{#if event.event_type === 'issue_status'}
+						{#if event.data.resolution}
+							<div class="issueEvent event">
+								<div class="eventDetail">
+									{event.user.email} resolved issue {event.object.uuid.substr(0, 8)}
+								</div>
+								<div class="eventDate">
+									{event.created_at}
+								</div>
+							</div>
+						{/if}
+					{/if}
+
+					{#if event.event_type === 'patch_status'}
+						{#if event.data.status}
+							<div class="signoffEvent event">
+								<div class="eventDetail">
+									{event.user.email}
+									signed off
+								</div>
+								<div class="eventDate">
+									{event.created_at}
+								</div>
+							</div>
+						{:else}
+							<div class="rejectEvent event">
+								<div class="eventDetail">
+									{event.user.email}
+									requested changes
+								</div>
+								<div class="eventDate">
+									{event.created_at}
+								</div>
+							</div>
+						{/if}
+					{/if}
+
+					{#if event.event_type === 'patch_version'}
+						<div class="versionEvent event">
+							<div class="eventDetail">
+								new patch version: v{event.object.version}
+							</div>
+							<div class="eventDate">
+								{event.created_at}
+							</div>
+						</div>
+					{/if}
+				{/each}
+			</div>
+			<div class="chatBox">
+				<div class="input">
+					<div class="messageBox">
+						<textarea></textarea>
+					</div>
+					<div class="chatOptions">
+						<div class="issueOptions">
+							<input type="checkbox" id="issue" name="issue" value="issue" />
+							<div>issue</div>
+						</div>
+						<button class="actionChat" on:click={() => createChatMessage()}>send</button>
+					</div>
+				</div>
+			</div>
+		</div>
 	</div>
 {/if}
-<link
-	rel="stylesheet"
-	href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/default.min.css"
-/>
 
 <style>
+	.event {
+		display: flex;
+		flex-direction: row;
+		justify-content: space-between;
+		margin-bottom: 0.5em;
+	}
+	.eventDate {
+		color: #888;
+		font-size: small;
+	}
+
+	.issueEvent {
+		background-color: #dbecff;
+		padding: 5px;
+		border-radius: 5px;
+	}
+	.signoffEvent {
+		background-color: #e6ffed;
+		padding: 5px;
+		border-radius: 5px;
+	}
+	.rejectEvent {
+		background-color: #ffeef0;
+		padding: 5px;
+		border-radius: 5px;
+	}
+	.versionEvent {
+		padding: 5px;
+		border-bottom: 1px solid rgb(96, 43, 43);
+	}
+	.versionEvent .eventDetail {
+		color: #844;
+	}
+
+	.actionChat {
+		cursor: pointer;
+		color: #ffffff;
+		padding: 5px 10px;
+		background-color: var(--clr-theme-pop-element);
+		border-radius: 5px;
+	}
+	.issueOptions {
+		display: flex;
+		flex-direction: row;
+		gap: 5px;
+		color: #626262;
+		/* justify vertically */
+		align-items: center;
+	}
+	.chatOptions {
+		padding: 4px;
+		display: flex;
+		flex-direction: row;
+		justify-content: space-between;
+	}
+	.patchArea {
+		max-width: 800px;
+		overflow-x: scroll;
+	}
 	hr {
 		margin: 1rem 0;
 	}
@@ -375,4 +767,102 @@
 		border-radius: 10px;
 		padding: 10px 20px;
 	}
+	.patch-diff {
+		font-family: monospace;
+		font-size: small;
+	}
+	h3 {
+		margin-bottom: 0.5rem;
+		font-weight: bold;
+	}
+	.button {
+		background-color: #f4f4f4;
+		border: 1px solid #ccc;
+		padding: 5px;
+	}
+	.chatArea {
+		width: 600px;
+		min-width: 600px;
+	}
+	.chatWindow {
+		border: 1px solid #ccc;
+		border-radius: 5px;
+		padding: 5px;
+		margin: 5px 0;
+		max-height: 600px;
+		height: 600px;
+		overflow-y: scroll;
+	}
+	.chatEntry {
+		border: 1px solid #ccc;
+		border-radius: 5px;
+		background-color: #f4f4f4;
+		padding: 5px;
+		margin: 5px 0;
+	}
+	.chatEntry.issue {
+		background-color: #ffeef0;
+	}
+	.chatEntry.issue.resolved {
+		background-color: #eeeeee;
+	}
+	.chatHeader {
+		display: flex;
+		flex-direction: row;
+		justify-content: space-between;
+		font-size: small;
+	}
+	.chatComment {
+		margin-top: 5px;
+		background-color: #ffffff;
+		padding: 5px;
+	}
+	.chatBox {
+		border: 1px solid #ccc;
+		border-radius: 5px;
+		background-color: #f4f4f4;
+		padding: 5px;
+		margin: 5px 0;
+	}
+	.chatBox textarea {
+		width: 100%;
+		height: 40px;
+		border-radius: 5px;
+		font-family: monospace;
+		font-size: large;
+		padding: 5px;
+	}
+	.avatar {
+		border-radius: 50%;
+		overflow: hidden;
+	}
+
+	.filePath {
+		font-family: monospace;
+		font-weight: bold;
+		font-size: 1.4em;
+		margin-bottom: 10px;
+	}
+	.signoff {
+		background-color: #e6ffed;
+		padding: 5px;
+		border-radius: 5px;
+	}
+	.rejected {
+		background-color: #ffeef0;
+		padding: 5px;
+		border-radius: 5px;
+	}
+	.title {
+		min-width: 100px;
+		display: inline-block;
+		border-right: 1px solid #eee;
+	}
+	.diffPath {
+		font-family: monospace;
+		font-weight: bold;
+		font-size: 1.1em;
+		margin-top: 10px;
+		margin-bottom: 6px;
+	}
 </style>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f1ed1846ac..d43ed08a55 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,28 +4,6 @@ settings:
   autoInstallPeers: true
   excludeLinksFromLockfile: false
 
-catalogs:
-  default:
-    vite:
-      specifier: 5.2.13
-      version: 5.2.13
-  svelte:
-    '@sveltejs/adapter-static':
-      specifier: 3.0.4
-      version: 3.0.4
-    '@sveltejs/kit':
-      specifier: 2.5.25
-      version: 2.5.25
-    '@sveltejs/vite-plugin-svelte':
-      specifier: 4.0.0-next.6
-      version: 4.0.0-next.6
-    svelte:
-      specifier: 5.0.0-next.243
-      version: 5.0.0-next.243
-    svelte-check:
-      specifier: 4.0.1
-      version: 4.0.1
-
 importers:
 
   .:
@@ -295,6 +273,9 @@ importers:
 
   apps/web:
     dependencies:
+      '@rails/actioncable':
+        specifier: ^7.2.100
+        version: 7.2.100
       '@sentry/sveltekit':
         specifier: ^8.9.2
         version: 8.9.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.0)(@sveltejs/kit@2.5.25(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@5.0.0-next.243)(vite@5.2.13(@types/node@22.3.0)))(svelte@5.0.0-next.243)(vite@5.2.13(@types/node@22.3.0)))(svelte@5.0.0-next.243)
@@ -307,6 +288,9 @@ importers:
       moment:
         specifier: ^2.30.1
         version: 2.30.1
+      svelte-gravatar:
+        specifier: ^1.0.3
+        version: 1.0.3(svelte@5.0.0-next.243)
     devDependencies:
       '@fontsource/fira-mono':
         specifier: ^4.5.10
@@ -1219,6 +1203,9 @@ packages:
     engines: {node: '>=16.3.0'}
     hasBin: true
 
+  '@rails/actioncable@7.2.100':
+    resolution: {integrity: sha512-7xtIENf0Yw59AFDM3+xqxPCZxev3QVAqjPmUzmgsB9eL8S/zTpB0IU9srNc7XknzJI4e09XKNnCaJRx3gfYzXA==}
+
   '@replit/codemirror-lang-svelte@6.0.0':
     resolution: {integrity: sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==}
     peerDependencies:
@@ -2525,6 +2512,9 @@ packages:
   chardet@0.7.0:
     resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
 
+  charenc@0.0.2:
+    resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
+
   check-error@2.1.1:
     resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
     engines: {node: '>= 16'}
@@ -2666,6 +2656,9 @@ packages:
     resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
     engines: {node: '>= 8'}
 
+  crypt@0.0.2:
+    resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
+
   css-shorthand-properties@1.1.1:
     resolution: {integrity: sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A==}
 
@@ -3752,6 +3745,9 @@ packages:
     resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==}
     engines: {node: '>= 0.4'}
 
+  is-buffer@1.1.6:
+    resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
+
   is-callable@1.2.7:
     resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
     engines: {node: '>= 0.4'}
@@ -4164,6 +4160,9 @@ packages:
     engines: {node: '>= 18'}
     hasBin: true
 
+  md5@2.3.0:
+    resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
+
   media-typer@0.3.0:
     resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
     engines: {node: '>= 0.6'}
@@ -5312,6 +5311,11 @@ packages:
     peerDependencies:
       svelte: ^3.57.0 || ^4.0.0
 
+  svelte-gravatar@1.0.3:
+    resolution: {integrity: sha512-CNxIV2lAuiqwdaPrGAM/BFj5U1dNNQXzeyh+HVi/48BODFXoDy0L1CMqYyvM+aKiF4ideZUBwT0S9/C1BeL5oA==}
+    peerDependencies:
+      svelte: '*'
+
   svelte-preprocess@5.1.3:
     resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==}
     engines: {node: '>= 16.0.0', pnpm: ^8.0.0}
@@ -5349,6 +5353,9 @@ packages:
       typescript:
         optional: true
 
+  svelte-waypoint@0.1.4:
+    resolution: {integrity: sha512-UEqoXZjJeKj2sWlAIsBOFjxjMn+KP8aFCc/zjdmZi1cCOE59z6T2C+I6ZaAf8EmNQqNzfZVB/Lci4Ci9spzXAw==}
+
   svelte-writable-derived@3.1.0:
     resolution: {integrity: sha512-cTvaVFNIJ036vSDIyPxJYivKC7ZLtcFOPm1Iq6qWBDo1fOHzfk6ZSbwaKrxhjgy52Rbl5IHzRcWgos6Zqn9/rg==}
     peerDependencies:
@@ -5975,6 +5982,28 @@ packages:
     resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
     engines: {node: '>= 14'}
 
+catalogs:
+  default:
+    vite:
+      specifier: 5.2.13
+      version: 5.2.13
+  svelte:
+    '@sveltejs/adapter-static':
+      specifier: 3.0.4
+      version: 3.0.4
+    '@sveltejs/kit':
+      specifier: 2.5.25
+      version: 2.5.25
+    '@sveltejs/vite-plugin-svelte':
+      specifier: 4.0.0-next.6
+      version: 4.0.0-next.6
+    svelte:
+      specifier: 5.0.0-next.243
+      version: 5.0.0-next.243
+    svelte-check:
+      specifier: 4.0.1
+      version: 4.0.1
+
 snapshots:
 
   '@aashutoshrathi/word-wrap@1.2.6': {}
@@ -6959,6 +6988,8 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@rails/actioncable@7.2.100': {}
+
   '@replit/codemirror-lang-svelte@6.0.0(@codemirror/autocomplete@6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1))(@codemirror/lang-css@6.2.1(@codemirror/view@6.26.3))(@codemirror/lang-html@6.4.9)(@codemirror/lang-javascript@6.2.2)(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)(@lezer/highlight@1.2.0)(@lezer/javascript@1.4.16)(@lezer/lr@1.4.1)':
     dependencies:
       '@codemirror/autocomplete': 6.16.2(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
@@ -8639,6 +8670,8 @@ snapshots:
 
   chardet@0.7.0: {}
 
+  charenc@0.0.2: {}
+
   check-error@2.1.1: {}
 
   chokidar@3.6.0:
@@ -8782,6 +8815,8 @@ snapshots:
       shebang-command: 2.0.0
       which: 2.0.2
 
+  crypt@0.0.2: {}
+
   css-shorthand-properties@1.1.1: {}
 
   css-value@0.0.1: {}
@@ -10152,6 +10187,8 @@ snapshots:
       call-bind: 1.0.7
       has-tostringtag: 1.0.2
 
+  is-buffer@1.1.6: {}
+
   is-callable@1.2.7: {}
 
   is-core-module@2.13.1:
@@ -10531,6 +10568,12 @@ snapshots:
 
   marked@10.0.0: {}
 
+  md5@2.3.0:
+    dependencies:
+      charenc: 0.0.2
+      crypt: 0.0.2
+      is-buffer: 1.1.6
+
   media-typer@0.3.0: {}
 
   memoizerific@1.11.3:
@@ -11738,6 +11781,12 @@ snapshots:
       svelte: 5.0.0-next.243
       svelte-writable-derived: 3.1.0(svelte@5.0.0-next.243)
 
+  svelte-gravatar@1.0.3(svelte@5.0.0-next.243):
+    dependencies:
+      md5: 2.3.0
+      svelte: 5.0.0-next.243
+      svelte-waypoint: 0.1.4
+
   svelte-preprocess@5.1.3(@babel/core@7.24.7)(postcss-load-config@5.1.0(postcss@8.4.39))(postcss@8.4.39)(svelte@5.0.0-next.243)(typescript@5.4.5):
     dependencies:
       '@types/pug': 2.0.6
@@ -11752,6 +11801,8 @@ snapshots:
       postcss-load-config: 5.1.0(postcss@8.4.39)
       typescript: 5.4.5
 
+  svelte-waypoint@0.1.4: {}
+
   svelte-writable-derived@3.1.0(svelte@5.0.0-next.243):
     dependencies:
       svelte: 5.0.0-next.243