Repository: fmaclen/hollama
Pull request #34 (2024-04-23 13:10:27)

feat: add ability to delete all sessions and settings

cloudflare-pages[bot]: ## Deploying hollama with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>445f772</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://c2f375d8.hollama.pages.dev'>https://c2f375d8.hollama.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://delete-all-sessions.hollama.pages.dev'>https://delete-all-sessions.hollama.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/hollama/c2f375d8-70d1-404f-a207-eda3fbcb1219)


Patch:
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index b4483d3..a77b5da 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -37,6 +37,20 @@
 		}
 	}
 
+	function deleteSessions(): void {
+		if (confirm('Are you sure you want to delete all sessions?')) {
+			localStorage.removeItem('hollama-sessions');
+			location.reload();
+		}
+	}
+
+	function deleteSettings(): void {
+		if (confirm('Are you sure you want to delete server settings?')) {
+			localStorage.removeItem('hollama-settings');
+			location.reload();
+		}
+	}
+
 	onMount(async () => {
 		// Get the current URL
 		ollamaURL = new URL(window.location.href);
@@ -52,9 +66,9 @@
 
 <div class="flex w-full flex-col bg-secondary">
 	<div class="justify-content-center m-auto max-w-[40ch] p-6">
-		<div class="container">
+		<div class="fieldset">
 			<Field name="server">
-				<span slot="title">
+				<span slot="title" class="flex w-full items-center justify-between">
 					Server
 					<Badge variant={serverStatus === 'disconnected' ? 'warning' : 'positive'}>
 						{serverStatus}
@@ -109,13 +123,23 @@
 			{/if}
 		</div>
 
-		<div class="container">
+		<div class="fieldset">
 			<FieldModels models={ollamaTagResponse?.models || []} bind:value={ollamaModel.value} />
 		</div>
 
 		<Separator class="mb-8 mt-8" />
 
-		<div class="container">
+		<div class="fieldset">
+			<Field name="danger-zone">
+				<span slot="title">Danger zone</span>
+				<Button variant="outline" on:click={deleteSessions}>Delete all sessions</Button>
+				<Button variant="outline" on:click={deleteSettings}>Delete server settings</Button>
+			</Field>
+		</div>
+
+		<Separator class="mb-8 mt-8" />
+
+		<div class="fieldset">
 			<p class="p"><strong>About</strong></p>
 			<p class="p">
 				<strong>Hollama</strong> is a minimalistic web interface for
@@ -126,22 +150,27 @@
 					target="_blank">Ollama</Button
 				>
 				servers. Code is available on
-				<Button variant="link" size="link" href="https://github.com/fmaclen/hollama" target="_blank"
-					>Github</Button
-				>.
+				<Button
+					variant="link"
+					size="link"
+					href="https://github.com/fmaclen/hollama"
+					target="_blank"
+				>
+					Github
+				</Button>.
 			</p>
 			<p class="p">
 				Made by
-				<Button variant="link" size="link" href="https://fernando.is" target="_blank"
-					>@fmaclen</Button
-				>
+				<Button variant="link" size="link" href="https://fernando.is" target="_blank">
+					@fmaclen
+				</Button>
 			</p>
 		</div>
 	</div>
 </div>
 
 <style lang="scss">
-	.container {
+	.fieldset {
 		@apply mb-6 mt-6 flex flex-col gap-y-3;
 	}
 
@@ -158,10 +187,10 @@
 	}
 
 	.p {
-		@apply text-sm text-neutral-600;
+		@apply text-sm text-neutral-500;
 
 		strong {
-			@apply font-medium leading-none;
+			@apply font-medium leading-none text-primary;
 		}
 	}
 </style>


Test patch:
diff --git a/tests/session.test.ts b/tests/session.test.ts
index 334febd..ae1c7e1 100644
--- a/tests/session.test.ts
+++ b/tests/session.test.ts
@@ -2,177 +2,222 @@ import { expect, test, type Page } from '@playwright/test';
 import { MOCK_SESSION_1_RESPONSE_1, MOCK_SESSION_1_RESPONSE_2, MOCK_SESSION_2_RESPONSE_1, chooseModelFromSettings, mockCompletionResponse, mockTagsResponse } from './utils';
 
 test.beforeEach(async ({ page }) => {
-  await mockTagsResponse(page);
+	await mockTagsResponse(page);
 });
 
 test('creates new session and chats', async ({ page }) => {
-  const sessionIdLocator = page.getByTestId('session-id');
-  const modelNameLocator = page.getByTestId('model-name');
-  const newSessionButton = page.getByTestId('new-session');
-  const promptTextarea = page.getByPlaceholder('Prompt');
-  const sendButton = page.getByText('Send');
-  const articleLocator = page.locator('article');
-  const newPromptHelp = page.getByText('Write a prompt to start a new session');
-
-  await page.goto('/');
-  await chooseModelFromSettings(page, 'gemma:7b');
-  await expect(sessionIdLocator).not.toBeVisible();
-  await expect(modelNameLocator).not.toBeVisible();
-  await expect(newPromptHelp).not.toBeVisible();
-
-  await newSessionButton.click();
-  await expect(sessionIdLocator).toBeVisible();
-  await expect(sessionIdLocator).toHaveText(/Session #[a-z0-9]{2,8}/);
-  await expect(modelNameLocator).toBeVisible();
-  await expect(modelNameLocator).toHaveText('gemma:7b');
-  await expect(promptTextarea).toBeVisible();
-  await expect(promptTextarea).toHaveValue('');
-  await expect(sendButton).toBeVisible();
-  await expect(sendButton).toBeDisabled();
-  await expect(newPromptHelp).toBeVisible();
-
-  await promptTextarea.fill('Who would win in a fight between Emma Watson and Jessica Alba?');
-  await expect(sendButton).toBeEnabled();
-  await expect(page.locator('article', { hasText: 'I am unable to provide subjective or speculative information, including fight outcomes between individuals.' })).not.toBeVisible();
-
-  await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_1);
-  await sendButton.click();
-  await expect(page.locator('article', { hasText: 'I am unable to provide subjective or speculative information, including fight outcomes between individuals.' })).toBeVisible();
-  await expect(newPromptHelp).not.toBeVisible();
-
-  await promptTextarea.fill("I understand, it's okay");
-  await expect(page.locator('article', { hasText: 'No problem! If you have any other questions or would like to discuss something else, feel free to ask' })).not.toBeVisible();
-  await expect(page.locator('article', { hasText: "I understand, it's okay" })).not.toBeVisible();
-
-  await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_2);
-  await page.keyboard.press('Enter');
-  await expect(page.locator('article', { hasText: "I understand, it's okay" })).toBeVisible();
-  await expect(page.locator('article', { hasText: 'No problem! If you have any other questions or would like to discuss something else, feel free to ask' })).toBeVisible();
-  await expect(page.getByText('AI')).toHaveCount(2);
-  expect(await page.getByText('You').count()).toBeGreaterThan(1);
-
-  // Check the session is saved to localStorage
-  await newSessionButton.click();
-  await expect(articleLocator).toHaveCount(0);
-
-  await page.goBack();
-  await expect(articleLocator).toHaveCount(4);
+	const sessionIdLocator = page.getByTestId('session-id');
+	const modelNameLocator = page.getByTestId('model-name');
+	const newSessionButton = page.getByTestId('new-session');
+	const promptTextarea = page.getByPlaceholder('Prompt');
+	const sendButton = page.getByText('Send');
+	const articleLocator = page.locator('article');
+	const newPromptHelp = page.getByText('Write a prompt to start a new session');
+
+	await page.goto('/');
+	await chooseModelFromSettings(page, 'gemma:7b');
+	await expect(sessionIdLocator).not.toBeVisible();
+	await expect(modelNameLocator).not.toBeVisible();
+	await expect(newPromptHelp).not.toBeVisible();
+
+	await newSessionButton.click();
+	await expect(sessionIdLocator).toBeVisible();
+	await expect(sessionIdLocator).toHaveText(/Session #[a-z0-9]{2,8}/);
+	await expect(modelNameLocator).toBeVisible();
+	await expect(modelNameLocator).toHaveText('gemma:7b');
+	await expect(promptTextarea).toBeVisible();
+	await expect(promptTextarea).toHaveValue('');
+	await expect(sendButton).toBeVisible();
+	await expect(sendButton).toBeDisabled();
+	await expect(newPromptHelp).toBeVisible();
+
+	await promptTextarea.fill('Who would win in a fight between Emma Watson and Jessica Alba?');
+	await expect(sendButton).toBeEnabled();
+	await expect(page.locator('article', { hasText: 'I am unable to provide subjective or speculative information, including fight outcomes between individuals.' })).not.toBeVisible();
+
+	await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_1);
+	await sendButton.click();
+	await expect(page.locator('article', { hasText: 'I am unable to provide subjective or speculative information, including fight outcomes between individuals.' })).toBeVisible();
+	await expect(newPromptHelp).not.toBeVisible();
+
+	await promptTextarea.fill("I understand, it's okay");
+	await expect(page.locator('article', { hasText: 'No problem! If you have any other questions or would like to discuss something else, feel free to ask' })).not.toBeVisible();
+	await expect(page.locator('article', { hasText: "I understand, it's okay" })).not.toBeVisible();
+
+	await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_2);
+	await page.keyboard.press('Enter');
+	await expect(page.locator('article', { hasText: "I understand, it's okay" })).toBeVisible();
+	await expect(page.locator('article', { hasText: 'No problem! If you have any other questions or would like to discuss something else, feel free to ask' })).toBeVisible();
+	await expect(page.getByText('AI')).toHaveCount(2);
+	expect(await page.getByText('You').count()).toBeGreaterThan(1);
+
+	// Check the session is saved to localStorage
+	await newSessionButton.click();
+	await expect(articleLocator).toHaveCount(0);
+
+	await page.goBack();
+	await expect(articleLocator).toHaveCount(4);
 });
 
 test('generates a random session id', async ({ page }) => {
-  await page.goto('/');
+	await page.goto('/');
 
-  const sessionIds = [];
-  const newSessionButton = page.getByText('New session');
+	const sessionIds = [];
+	const newSessionButton = page.getByText('New session');
 
-  // Check it generates a new session id 3 times in a row
-  for (let i = 0; i < 3; i++) {
-    const sessionId = await newSessionButton.getAttribute('href');
-    expect(sessionId).toMatch(/[a-z0-9]{2,8}/);
-    sessionIds.push(sessionId);
-    await newSessionButton.click();
-  }
+	// Check it generates a new session id 3 times in a row
+	for (let i = 0; i < 3; i++) {
+		const sessionId = await newSessionButton.getAttribute('href');
+		expect(sessionId).toMatch(/[a-z0-9]{2,8}/);
+		sessionIds.push(sessionId);
+		await newSessionButton.click();
+	}
 
-  expect(new Set(sessionIds).size).toBe(3);
+	expect(new Set(sessionIds).size).toBe(3);
 });
 
 test('can navigate older sessions from sidebar', async ({ page }) => {
-  await page.goto('/');
-  await chooseModelFromSettings(page, 'gemma:7b');
-  await expect(page.getByText('No sessions in history')).toBeVisible();
-  await expect(page.locator('aside', { hasText: 'Who would win in a fight between Emma Watson and Jessica Alba?' })).not.toBeVisible();
-
-  await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_1);
-  await page.getByText('New session').click();
-  await page.getByPlaceholder('Prompt').fill('Who would win in a fight between Emma Watson and Jessica Alba?');
-  await page.getByText('Send').click();
-  await page.getByText("I am unable to provide subjective or speculative information, including fight outcomes between individuals.").isVisible();
-  await expect(page.getByText('No sessions in history')).not.toBeVisible();
-  expect(await page.getByTestId('session-item').textContent()).toContain("Who would win in a fight between Emma Watson and Jessica Alba?");
-  expect(await page.getByTestId('session-item').textContent()).toContain("gemma:7b");
-  expect(await page.getByTestId('session-item').count()).toBe(1);
-
-  // Leave the conversation by visiting the settings page
-  await page.getByTitle('Settings').click();
-  await expect(page.getByText('Who would win in a fight between Emma Watson and Jessica Alba?')).toBeVisible();
-  await expect(page.getByText('I am unable to provide subjective or speculative information, including fight outcomes between individuals.')).not.toBeVisible();
-
-  // Navigate back to the conversation
-  await page.getByTestId('session-item').click();
-  await expect(page.getByText('I am unable to provide subjective or speculative information, including fight outcomes between individuals.')).toBeVisible();
-
-  // Create a new session
-  await mockCompletionResponse(page, MOCK_SESSION_2_RESPONSE_1);
-  await chooseModelFromSettings(page, 'openhermes2.5-mistral:latest');
-  await page.getByText('New session').click();
-  await page.getByPlaceholder('Prompt').fill('What does the fox say?');
-  await page.getByText('Send').click();
-  await expect(page.getByText('The fox says various things, such as "ring-a-ding-ding," "bada bing-bing" and "higglety-pigglety pop')).toBeVisible();
-  expect(await page.getByTestId('session-item').count()).toBe(2);
-
-  // Check the sessions are listed in the correct order
-  expect(await page.getByTestId('session-item').first().textContent()).toContain("What does the fox say?");
-  expect(await page.getByTestId('session-item').first().textContent()).toContain("openhermes2.5-mistral:latest");
-  expect(await page.getByTestId('session-item').last().textContent()).toContain("Who would win in a fight between Emma Watson and Jessica Alba?");
-  expect(await page.getByTestId('session-item').last().textContent()).toContain("gemma:7b");
-
-  // Check the current session is highlighted in the sidebar
-  await expect(page.getByTestId('session-item').first()).not.toHaveClass(/hover:bg-accent/);
-  await expect(page.getByTestId('session-item').first()).toHaveClass(/ bg-accent/);
-  await expect(page.getByTestId('session-item').last()).not.toHaveClass(/ bg-accent/);
-  await expect(page.getByTestId('session-item').last()).toHaveClass(/hover:bg-accent/);
+	await page.goto('/');
+	await chooseModelFromSettings(page, 'gemma:7b');
+	await expect(page.getByText('No sessions in history')).toBeVisible();
+	await expect(page.locator('aside', { hasText: 'Who would win in a fight between Emma Watson and Jessica Alba?' })).not.toBeVisible();
+
+	await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_1);
+	await page.getByText('New session').click();
+	await page.getByPlaceholder('Prompt').fill('Who would win in a fight between Emma Watson and Jessica Alba?');
+	await page.getByText('Send').click();
+	await page.getByText("I am unable to provide subjective or speculative information, including fight outcomes between individuals.").isVisible();
+	await expect(page.getByText('No sessions in history')).not.toBeVisible();
+	expect(await page.getByTestId('session-item').textContent()).toContain("Who would win in a fight between Emma Watson and Jessica Alba?");
+	expect(await page.getByTestId('session-item').textContent()).toContain("gemma:7b");
+	expect(await page.getByTestId('session-item').count()).toBe(1);
+
+	// Leave the conversation by visiting the settings page
+	await page.getByTitle('Settings').click();
+	await expect(page.getByText('Who would win in a fight between Emma Watson and Jessica Alba?')).toBeVisible();
+	await expect(page.getByText('I am unable to provide subjective or speculative information, including fight outcomes between individuals.')).not.toBeVisible();
+
+	// Navigate back to the conversation
+	await page.getByTestId('session-item').click();
+	await expect(page.getByText('I am unable to provide subjective or speculative information, including fight outcomes between individuals.')).toBeVisible();
+
+	// Create a new session
+	await mockCompletionResponse(page, MOCK_SESSION_2_RESPONSE_1);
+	await chooseModelFromSettings(page, 'openhermes2.5-mistral:latest');
+	await page.getByText('New session').click();
+	await page.getByPlaceholder('Prompt').fill('What does the fox say?');
+	await page.getByText('Send').click();
+	await expect(page.getByText('The fox says various things, such as "ring-a-ding-ding," "bada bing-bing" and "higglety-pigglety pop')).toBeVisible();
+	expect(await page.getByTestId('session-item').count()).toBe(2);
+
+	// Check the sessions are listed in the correct order
+	expect(await page.getByTestId('session-item').first().textContent()).toContain("What does the fox say?");
+	expect(await page.getByTestId('session-item').first().textContent()).toContain("openhermes2.5-mistral:latest");
+	expect(await page.getByTestId('session-item').last().textContent()).toContain("Who would win in a fight between Emma Watson and Jessica Alba?");
+	expect(await page.getByTestId('session-item').last().textContent()).toContain("gemma:7b");
+
+	// Check the current session is highlighted in the sidebar
+	await expect(page.getByTestId('session-item').first()).not.toHaveClass(/hover:bg-accent/);
+	await expect(page.getByTestId('session-item').first()).toHaveClass(/ bg-accent/);
+	await expect(page.getByTestId('session-item').last()).not.toHaveClass(/ bg-accent/);
+	await expect(page.getByTestId('session-item').last()).toHaveClass(/hover:bg-accent/);
 });
 
 test('deletes a session from the sidebar', async ({ page }) => {
-  await page.goto('/');
-  await chooseModelFromSettings(page, 'gemma:7b');
-  await expect(page.getByText('No sessions in history')).toBeVisible();
-
-  await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_1);
-  await page.getByText('New session').click();
-  await page.getByPlaceholder('Prompt').fill('Who would win in a fight between Emma Watson and Jessica Alba?');
-  await page.getByText('Send').click();
-  await expect(page.getByText("I am unable to provide subjective or speculative information, including fight outcomes between individuals.")).toBeVisible();
-  await expect(page.getByText('No sessions in history')).not.toBeVisible();
-  expect(await page.getByTestId('session-item').count()).toBe(1);
-
-  page.on('dialog', dialog => dialog.accept("Are you sure you want to delete this session?"));
-  await page.getByTitle('Delete session').click();
-  await expect(page.getByText('No sessions in history')).toBeVisible();
-  expect(await page.getByTestId('session-item').count()).toBe(0);
+	await page.goto('/');
+	await chooseModelFromSettings(page, 'gemma:7b');
+	await expect(page.getByText('No sessions in history')).toBeVisible();
+
+	await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_1);
+	await page.getByText('New session').click();
+	await page.getByPlaceholder('Prompt').fill('Who would win in a fight between Emma Watson and Jessica Alba?');
+	await page.getByText('Send').click();
+	await expect(page.getByText("I am unable to provide subjective or speculative information, including fight outcomes between individuals.")).toBeVisible();
+	await expect(page.getByText('No sessions in history')).not.toBeVisible();
+	expect(await page.getByTestId('session-item').count()).toBe(1);
+
+	page.on('dialog', dialog => dialog.accept("Are you sure you want to delete this session?"));
+	await page.getByTitle('Delete session').click();
+	await expect(page.getByText('No sessions in history')).toBeVisible();
+	expect(await page.getByTestId('session-item').count()).toBe(0);
 });
 
+test('all sessions can be deleted', async ({ page }) => {
+	await page.goto('/');
+	await expect(page.getByText('No sessions in history')).toBeVisible();
+	await expect(page.getByTestId('session-item')).toHaveCount(0);
+
+	// Stage 2 sessions
+	await page.evaluate(() => window.localStorage.setItem(
+		'hollama-sessions',
+		JSON.stringify(
+			[
+				{
+					id: "qbhc0q",
+					model: "gemma:7b",
+					messages: [
+						{ role: "user", content: "Hello world!" },
+						{ role: "ai", content: "Hello world! 👋 🌎\n\nIt's great to hear from you. What would you like to do today?" }
+					],
+					context: [],
+				},
+				{
+					id: "m2jjac",
+					model: "openhermes2.5-mistral:latest",
+					messages: [
+						{ role: "user", content: "Hello world, again!" },
+						{ role: "ai", content: "Hello! It's always a pleasure to see you back. How can I assist you today?" }
+					],
+					context: [],
+				}
+			]
+		)
+	));
+
+	await page.reload();
+	await expect(page.getByText('No sessions in history')).not.toBeVisible();
+	await expect(page.getByTestId('session-item')).toHaveCount(2);
+
+	// Click the delete button
+	page.on('dialog', dialog => dialog.accept("Are you sure you want to delete all sessions?"));
+	await page.getByText('Delete all sessions').click();
+	expect(await page.evaluate(() => window.localStorage.getItem('hollama-sessions'))).toBe(null);
+	await expect(page.getByText('No sessions in history')).toBeVisible();
+	await expect(page.getByTestId('session-item')).toHaveCount(0);
+});
+
+
 test('copies the raw text of a message to clipboard', async ({ page }) => {
-  await page.goto('/');
-  await chooseModelFromSettings(page, 'gemma:7b');
-  await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_1);
-  await page.getByText('New session').click();
-  await page.getByPlaceholder('Prompt').fill('Who would win in a fight between Emma Watson and Jessica Alba?');
-  await page.getByText('Send').click();
-  await expect(page.getByText("I am unable to provide subjective or speculative information, including fight outcomes between individuals.")).toBeVisible();
-  expect(await page.evaluate(() => navigator.clipboard.readText())).toEqual("");
-
-  await page.getByTitle('Copy').first().click();
-  // HACK: Wait for the clipboard to be updated
-  setTimeout(async () => {
-    expect(await page.evaluate(() => navigator.clipboard.readText())).toEqual("Who would win in a fight between Emma Watson and Jessica Alba?");
-  }, 250);
-  
-  await page.getByTitle('Copy').last().click();
-  // HACK: Wait for the clipboard to be updated
-  setTimeout(async () => {
-    expect(await page.evaluate(() => navigator.clipboard.readText())).toEqual("I am unable to provide subjective or speculative information, including fight outcomes between individuals.");
-  }, 250);
+	await page.goto('/');
+	await chooseModelFromSettings(page, 'gemma:7b');
+	await mockCompletionResponse(page, MOCK_SESSION_1_RESPONSE_1);
+	await page.getByText('New session').click();
+	await page.getByPlaceholder('Prompt').fill('Who would win in a fight between Emma Watson and Jessica Alba?');
+	await page.getByText('Send').click();
+	await expect(page.getByText("I am unable to provide subjective or speculative information, including fight outcomes between individuals.")).toBeVisible();
+	expect(await page.evaluate(() => navigator.clipboard.readText())).toEqual("");
+
+	await page.getByTitle('Copy').first().click();
+	// HACK: Wait for the clipboard to be updated
+	setTimeout(async () => {
+		expect(await page.evaluate(() => navigator.clipboard.readText())).toEqual("Who would win in a fight between Emma Watson and Jessica Alba?");
+	}, 250);
+
+	await page.getByTitle('Copy').last().click();
+	// HACK: Wait for the clipboard to be updated
+	setTimeout(async () => {
+		expect(await page.evaluate(() => navigator.clipboard.readText())).toEqual("I am unable to provide subjective or speculative information, including fight outcomes between individuals.");
+	}, 250);
 });
 
 test.skip('handles API error when generating AI response', async ({ page }) => {
-  // TODO: Implement the test
+	// TODO: Implement the test
 });
 
 test.skip('displays system message when an error occurs', async ({ page }) => {
-  // TODO: Implement the test
+	// TODO: Implement the test
 });
 
 test.skip('auto-scrolls to the bottom when new messages are added', async ({ page }) => {
-  // TODO: Implement the test
+	// TODO: Implement the test
 });
diff --git a/tests/settings.test.ts b/tests/settings.test.ts
index fcf3274..e0e8b55 100644
--- a/tests/settings.test.ts
+++ b/tests/settings.test.ts
@@ -2,45 +2,81 @@ import { expect, test } from '@playwright/test';
 import { mockTagsResponse } from './utils';
 
 test.beforeEach(async ({ page }) => {
-  await mockTagsResponse(page);
+	await mockTagsResponse(page);
 });
 
 test('displays model list and updates settings store', async ({ page }) => {
-  await page.goto('/');
+	await page.goto('/');
 
-  // Check if the model list contains the expected models
-  const modelNames = await page.$$eval('select#model option', options =>
-    options.map(option => option.textContent?.trim())
-  );
-  expect(modelNames).toContain('gemma:7b');
-  expect(modelNames).toContain('openhermes2.5-mistral:latest');
+	// Check if the model list contains the expected models
+	const modelNames = await page.$$eval('select#model option', options =>
+		options.map(option => option.textContent?.trim())
+	);
+	expect(modelNames).toContain('gemma:7b');
+	expect(modelNames).toContain('openhermes2.5-mistral:latest');
 
-  await page.getByLabel('Model').selectOption('openhermes2.5-mistral:latest');
+	await page.getByLabel('Model').selectOption('openhermes2.5-mistral:latest');
 
-  // Check if the settings store is updated with the selected model
-  const localStorageValue = await page.evaluate(() => window.localStorage.getItem('hollama-settings'));
-  expect(localStorageValue).toContain('"ollamaModel":"openhermes2.5-mistral:latest"');
+	// Check if the settings store is updated with the selected model
+	const localStorageValue = await page.evaluate(() => window.localStorage.getItem('hollama-settings'));
+	expect(localStorageValue).toContain('"ollamaModel":"openhermes2.5-mistral:latest"');
 });
 
 test('handles server status updates correctly', async ({ page }) => {
-  await page.goto('/');
-  await expect (page.getByLabel('Server')).toHaveValue('http://localhost:11434');
-
-  // The starting status is "connected"
-  await expect(page.getByText('disconnected')).not.toBeVisible();
-  await expect(page.getByText('connected', { exact: true })).toBeVisible();
-  await expect(page.getByText('connected', { exact: true })).toHaveClass(/badge--positive/);
-
-  // Mock the API to return an error response
-  await page.route('**/api/tags', async (route) => {
-    await route.abort();
-  });
-
-  // Trigger a new API request by typing in the input field
-  await page.getByLabel('Server').clear();
-
-  // Wait for the server status to be updated to "disconnected"
-  await expect(page.getByText('connected', { exact: true })).not.toBeVisible();
-  await expect(page.getByText('disconnected')).toBeVisible();
-  await expect(page.getByText('disconnected')).toHaveClass(/badge--warning/);
+	await page.goto('/');
+	await expect(page.getByLabel('Server')).toHaveValue('http://localhost:11434');
+
+	// The starting status is "connected"
+	await expect(page.getByText('disconnected')).not.toBeVisible();
+	await expect(page.getByText('connected', { exact: true })).toBeVisible();
+	await expect(page.getByText('connected', { exact: true })).toHaveClass(/badge--positive/);
+
+	// Mock the API to return an error response
+	await page.route('**/api/tags', async (route) => {
+		await route.abort();
+	});
+
+	// Trigger a new API request by typing in the input field
+	await page.getByLabel('Server').clear();
+
+	// Wait for the server status to be updated to "disconnected"
+	await expect(page.getByText('connected', { exact: true })).not.toBeVisible();
+	await expect(page.getByText('disconnected')).toBeVisible();
+	await expect(page.getByText('disconnected')).toHaveClass(/badge--warning/);
+});
+
+test('settings and sessions can be deleted', async ({ page }) => {
+	await page.goto('/');
+	await expect(page.getByLabel('Model')).toHaveValue('');
+
+	// Stage the settings store with a model
+	await page.evaluate(() => window.localStorage.setItem(
+		'hollama-settings',
+		JSON.stringify({
+			ollamaServer: 'http://localhost:3000',
+			ollamaModel: 'openhermes2.5-mistral:latest'
+		})));
+
+	await page.reload();
+	await expect(page.getByLabel('Server')).toHaveValue('http://localhost:3000');
+	await expect(page.getByLabel('Model')).toHaveValue('openhermes2.5-mistral:latest');
+
+	// Check if the settings store is updated with the selected model
+	let localStorageValue = await page.evaluate(() => window.localStorage.getItem('hollama-settings'));
+	expect(localStorageValue).toContain('"ollamaServer":"http://localhost:3000"');
+	expect(localStorageValue).toContain('"ollamaModel":"openhermes2.5-mistral:latest"');
+
+	// Click the delete button
+	page.on('dialog', dialog => dialog.accept("Are you sure you want to delete server settings?"));
+	await page.getByText('Delete server settings').click();
+
+	// Wait for page reload
+	await page.waitForFunction(() => {
+		return window.localStorage.getItem('hollama-settings') !== null;
+	});
+
+	// Check if the settings has been reset to defaults
+	localStorageValue = await page.evaluate(() => window.localStorage.getItem('hollama-settings'));
+	expect(localStorageValue).toContain('"ollamaServer":"http://localhost:11434"');
+	expect(localStorageValue).toContain('"ollamaModel":""');
 });