From 1b41cbd1e00b695c70ab5676fc1e4500d28fc282 Mon Sep 17 00:00:00 2001
From: David Kaya
Date: Sun, 22 Mar 2026 10:11:42 +0100
Subject: [PATCH] feat: add scratchpad chat sessions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Contracts/ProtocolModels.cs | 1 +
.../Services/AgentInstructionComposer.cs | 25 ++++-
.../Services/CopilotWorkflowRunner.cs | 17 +--
.../AgentInstructionComposerTests.cs | 26 +++++
src/main/KopayaAppService.ts | 8 +-
src/main/persistence/appPaths.ts | 4 +
src/main/persistence/workspaceRepository.ts | 23 +++-
src/renderer/App.tsx | 7 +-
src/renderer/components/ChatPane.tsx | 18 +++-
src/renderer/components/NewSessionModal.tsx | 31 +++++-
src/renderer/components/Sidebar.tsx | 101 +++++++++++-------
src/renderer/components/WelcomePane.tsx | 27 +++--
src/shared/contracts/sidecar.ts | 1 +
src/shared/domain/project.ts | 38 +++++++
tests/shared/project.test.ts | 46 ++++++++
15 files changed, 300 insertions(+), 73 deletions(-)
create mode 100644 tests/shared/project.test.ts
diff --git a/sidecar/src/Kopaya.AgentHost/Contracts/ProtocolModels.cs b/sidecar/src/Kopaya.AgentHost/Contracts/ProtocolModels.cs
index 53ad860..5622642 100644
--- a/sidecar/src/Kopaya.AgentHost/Contracts/ProtocolModels.cs
+++ b/sidecar/src/Kopaya.AgentHost/Contracts/ProtocolModels.cs
@@ -71,6 +71,7 @@ public sealed class RunTurnCommandDto : SidecarCommandEnvelope
{
public string SessionId { get; init; } = string.Empty;
public string ProjectPath { get; init; } = string.Empty;
+ public string WorkspaceKind { get; init; } = "project";
public PatternDefinitionDto Pattern { get; init; } = new();
public IReadOnlyList Messages { get; init; } = [];
}
diff --git a/sidecar/src/Kopaya.AgentHost/Services/AgentInstructionComposer.cs b/sidecar/src/Kopaya.AgentHost/Services/AgentInstructionComposer.cs
index cade8ce..82256a6 100644
--- a/sidecar/src/Kopaya.AgentHost/Services/AgentInstructionComposer.cs
+++ b/sidecar/src/Kopaya.AgentHost/Services/AgentInstructionComposer.cs
@@ -7,12 +7,22 @@ internal static class AgentInstructionComposer
public static string Compose(
PatternDefinitionDto pattern,
PatternAgentDefinitionDto agent,
- int agentIndex)
+ int agentIndex,
+ string workspaceKind = "project")
{
string baseInstructions = agent.Instructions.Trim();
+ string workspaceGuidance = string.Equals(workspaceKind, "scratchpad", StringComparison.OrdinalIgnoreCase)
+ ? """
+ You are operating in scratchpad mode.
+ Treat this session as pure ad-hoc Q&A rather than repository automation.
+ Do not inspect, modify, create, or delete files, and do not behave as though you are working inside a user project.
+ Answer conversationally and focus on the user's question directly.
+ """
+ : string.Empty;
+
if (!string.Equals(pattern.Mode, "handoff", StringComparison.OrdinalIgnoreCase))
{
- return baseInstructions;
+ return JoinInstructionBlocks(baseInstructions, workspaceGuidance);
}
string runtimeGuidance = agentIndex == 0
@@ -28,8 +38,13 @@ internal static class AgentInstructionComposer
Do not push the actual work back to triage unless you are blocked or the request is clearly outside your specialty.
""";
- return string.IsNullOrWhiteSpace(baseInstructions)
- ? runtimeGuidance
- : $"{baseInstructions}\n\n{runtimeGuidance}";
+ return JoinInstructionBlocks(baseInstructions, workspaceGuidance, runtimeGuidance);
+ }
+
+ private static string JoinInstructionBlocks(params string[] blocks)
+ {
+ return string.Join(
+ "\n\n",
+ blocks.Where(block => !string.IsNullOrWhiteSpace(block)).Select(block => block.Trim()));
}
}
diff --git a/sidecar/src/Kopaya.AgentHost/Services/CopilotWorkflowRunner.cs b/sidecar/src/Kopaya.AgentHost/Services/CopilotWorkflowRunner.cs
index c4353af..c67b40f 100644
--- a/sidecar/src/Kopaya.AgentHost/Services/CopilotWorkflowRunner.cs
+++ b/sidecar/src/Kopaya.AgentHost/Services/CopilotWorkflowRunner.cs
@@ -40,7 +40,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
throw new InvalidOperationException(validationError.Message);
}
- await using AgentBundle bundle = await AgentBundle.CreateAsync(command.Pattern, command.ProjectPath, cancellationToken);
+ await using AgentBundle bundle = await AgentBundle.CreateAsync(command, cancellationToken);
Workflow workflow = bundle.BuildWorkflow(command.Pattern);
List inputMessages = command.Messages.Select(ToChatMessage).ToList();
@@ -436,15 +436,15 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
public IReadOnlyList Agents { get; }
public static async Task CreateAsync(
- PatternDefinitionDto pattern,
- string projectPath,
+ RunTurnCommandDto command,
CancellationToken cancellationToken)
{
List disposables = [];
List agents = [];
CopilotClientOptions clientOptions = CopilotCliPathResolver.CreateClientOptions();
+ bool isScratchpad = string.Equals(command.WorkspaceKind, "scratchpad", StringComparison.OrdinalIgnoreCase);
- foreach ((PatternAgentDefinitionDto definition, int agentIndex) in pattern.Agents.Select((definition, index) => (definition, index)))
+ foreach ((PatternAgentDefinitionDto definition, int agentIndex) in command.Pattern.Agents.Select((definition, index) => (definition, index)))
{
CopilotClient client = new(clientOptions);
await client.StartAsync(cancellationToken).ConfigureAwait(false);
@@ -455,13 +455,18 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
ReasoningEffort = definition.ReasoningEffort,
SystemMessage = new SystemMessageConfig
{
- Content = AgentInstructionComposer.Compose(pattern, definition, agentIndex),
+ Content = AgentInstructionComposer.Compose(command.Pattern, definition, agentIndex, command.WorkspaceKind),
},
- WorkingDirectory = projectPath,
+ WorkingDirectory = command.ProjectPath,
OnPermissionRequest = ApprovePermissionAsync,
Streaming = true,
};
+ if (isScratchpad)
+ {
+ sessionConfig.AvailableTools = [];
+ }
+
GitHubCopilotAgent agent = new(
client,
sessionConfig,
diff --git a/sidecar/tests/Kopaya.AgentHost.Tests/AgentInstructionComposerTests.cs b/sidecar/tests/Kopaya.AgentHost.Tests/AgentInstructionComposerTests.cs
index 6c721fc..ec7e9f9 100644
--- a/sidecar/tests/Kopaya.AgentHost.Tests/AgentInstructionComposerTests.cs
+++ b/sidecar/tests/Kopaya.AgentHost.Tests/AgentInstructionComposerTests.cs
@@ -67,6 +67,32 @@ public sealed class AgentInstructionComposerTests
Assert.Contains("own the substantive answer", instructions, StringComparison.OrdinalIgnoreCase);
}
+ [Fact]
+ public void Compose_AddsScratchpadGuidanceForProjectlessQaSessions()
+ {
+ PatternDefinitionDto pattern = new()
+ {
+ Id = "pattern-single",
+ Name = "Single",
+ Mode = "single",
+ Availability = "available",
+ };
+ PatternAgentDefinitionDto agent = CreateAgent(
+ id: "agent-primary",
+ name: "Primary Agent",
+ instructions: "You are a helpful assistant.");
+
+ string instructions = AgentInstructionComposer.Compose(
+ pattern,
+ agent,
+ agentIndex: 0,
+ workspaceKind: "scratchpad");
+
+ Assert.Contains("scratchpad mode", instructions, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("pure ad-hoc Q&A", instructions, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("Do not inspect, modify, create, or delete files", instructions, StringComparison.OrdinalIgnoreCase);
+ }
+
private static PatternAgentDefinitionDto CreateAgent(string id, string name, string instructions)
{
return new PatternAgentDefinitionDto
diff --git a/src/main/KopayaAppService.ts b/src/main/KopayaAppService.ts
index 5e1295e..e3380d9 100644
--- a/src/main/KopayaAppService.ts
+++ b/src/main/KopayaAppService.ts
@@ -5,7 +5,7 @@ import { dialog } from 'electron';
import type { AgentActivityEvent, TurnDeltaEvent } from '@shared/contracts/sidecar';
import { buildSessionTitle, validatePatternDefinition, type PatternDefinition } from '@shared/domain/pattern';
-import type { ProjectRecord } from '@shared/domain/project';
+import { isScratchpadProject, type ProjectRecord } from '@shared/domain/project';
import type { SessionEventRecord } from '@shared/domain/event';
import type { ChatMessageRecord, SessionRecord } from '@shared/domain/session';
import type { WorkspaceState } from '@shared/domain/workspace';
@@ -75,6 +75,10 @@ export class KopayaAppService extends EventEmitter {
}
async removeProject(projectId: string): Promise {
+ if (isScratchpadProject(projectId)) {
+ throw new Error('Scratchpad cannot be removed.');
+ }
+
const workspace = await this.loadWorkspace();
workspace.projects = workspace.projects.filter((project) => project.id !== projectId);
workspace.sessions = workspace.sessions.filter((session) => session.projectId !== projectId);
@@ -187,6 +191,7 @@ export class KopayaAppService extends EventEmitter {
});
const requestId = createId('turn');
+ const workspaceKind = isScratchpadProject(project) ? 'scratchpad' : 'project';
try {
const responseMessages = await this.sidecar.runTurn(
{
@@ -194,6 +199,7 @@ export class KopayaAppService extends EventEmitter {
requestId,
sessionId: session.id,
projectPath: project.path,
+ workspaceKind,
pattern,
messages: session.messages,
},
diff --git a/src/main/persistence/appPaths.ts b/src/main/persistence/appPaths.ts
index 2928966..1d4cf3f 100644
--- a/src/main/persistence/appPaths.ts
+++ b/src/main/persistence/appPaths.ts
@@ -4,3 +4,7 @@ import { join } from 'node:path';
export function getWorkspaceFilePath(): string {
return join(app.getPath('userData'), 'workspace.json');
}
+
+export function getScratchpadDirectoryPath(): string {
+ return join(app.getPath('userData'), 'scratchpad');
+}
diff --git a/src/main/persistence/workspaceRepository.ts b/src/main/persistence/workspaceRepository.ts
index 2ff320c..e60db58 100644
--- a/src/main/persistence/workspaceRepository.ts
+++ b/src/main/persistence/workspaceRepository.ts
@@ -1,9 +1,12 @@
+import { mkdir } from 'node:fs/promises';
+
import { createBuiltinPatterns } from '@shared/domain/pattern';
import type { PatternDefinition } from '@shared/domain/pattern';
+import { mergeScratchpadProject } from '@shared/domain/project';
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
import { nowIso } from '@shared/utils/ids';
-import { getWorkspaceFilePath } from '@main/persistence/appPaths';
+import { getScratchpadDirectoryPath, getWorkspaceFilePath } from '@main/persistence/appPaths';
import { readJsonFile, writeJsonFile } from '@main/persistence/jsonStore';
function mergePatterns(existingPatterns: PatternDefinition[]): PatternDefinition[] {
@@ -32,20 +35,34 @@ function mergePatterns(existingPatterns: PatternDefinition[]): PatternDefinition
export class WorkspaceRepository {
readonly filePath = getWorkspaceFilePath();
+ readonly scratchpadPath = getScratchpadDirectoryPath();
async load(): Promise {
+ await mkdir(this.scratchpadPath, { recursive: true });
+
const stored = await readJsonFile(this.filePath);
if (!stored) {
- const seeded = createWorkspaceSeed();
+ const seededBase = createWorkspaceSeed();
+ const projects = mergeScratchpadProject([], this.scratchpadPath);
+ const seeded: WorkspaceState = {
+ ...seededBase,
+ projects,
+ selectedProjectId: projects[0]?.id,
+ };
await this.save(seeded);
return seeded;
}
+ const projects = mergeScratchpadProject(stored.projects ?? [], this.scratchpadPath);
+
const workspace: WorkspaceState = {
...stored,
patterns: mergePatterns(stored.patterns ?? []),
- projects: stored.projects ?? [],
+ projects,
sessions: stored.sessions ?? [],
+ selectedProjectId: projects.some((project) => project.id === stored.selectedProjectId)
+ ? stored.selectedProjectId
+ : projects[0]?.id,
lastUpdatedAt: stored.lastUpdatedAt ?? nowIso(),
};
diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index ba48f69..d4e74f8 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -15,6 +15,7 @@ import { applySessionEventWorkspace } from '@renderer/lib/sessionWorkspace';
import { WelcomePane } from '@renderer/components/WelcomePane';
import { getElectronApi } from '@renderer/lib/electronApi';
import type { PatternDefinition } from '@shared/domain/pattern';
+import { isScratchpadProject } from '@shared/domain/project';
import type { WorkspaceState } from '@shared/domain/workspace';
import { createId, nowIso } from '@shared/utils/ids';
@@ -106,6 +107,10 @@ export default function App() {
() => (selectedSession ? sessionActivities[selectedSession.id] : undefined),
[selectedSession, sessionActivities],
);
+ const hasUserProjects = useMemo(
+ () => (workspace?.projects.some((project) => !isScratchpadProject(project)) ?? false),
+ [workspace?.projects],
+ );
// Loading state
if (!workspace) {
@@ -147,7 +152,7 @@ export default function App() {
} else {
content = (
0}
+ hasProjects={hasUserProjects}
onAddProject={() => void api.addProject()}
onNewSession={() => setShowNewSession(true)}
onOpenSettings={() => setShowSettings(true)}
diff --git a/src/renderer/components/ChatPane.tsx b/src/renderer/components/ChatPane.tsx
index ff12b39..ee47772 100644
--- a/src/renderer/components/ChatPane.tsx
+++ b/src/renderer/components/ChatPane.tsx
@@ -5,7 +5,7 @@ import { MarkdownContent } from '@renderer/components/MarkdownContent';
import { getAssistantMessagePhase } from '@renderer/lib/messagePhase';
import type { PatternDefinition } from '@shared/domain/pattern';
-import type { ProjectRecord } from '@shared/domain/project';
+import { isScratchpadProject, type ProjectRecord } from '@shared/domain/project';
import type { SessionRecord } from '@shared/domain/session';
function ThinkingDots() {
@@ -31,6 +31,7 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
const textareaRef = useRef(null);
const isBusy = session.status === 'running';
+ const isScratchpad = isScratchpadProject(project);
useEffect(() => {
transcriptRef.current?.scrollTo({
@@ -60,7 +61,7 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
{session.title}
- {project.name} · {pattern.name} · {pattern.mode}
+ {isScratchpad ? `Scratchpad · ${pattern.name}` : `${project.name} · ${pattern.name} · ${pattern.mode}`}
@@ -90,8 +91,17 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
Send a message to start the conversation
- Using {pattern.name} in{' '}
- {project.name}
+ {isScratchpad ? (
+ <>
+ Scratchpad is ready for ad-hoc questions using{' '}
+ {pattern.name}
+ >
+ ) : (
+ <>
+ Using {pattern.name} in{' '}
+ {project.name}
+ >
+ )}
) : (
diff --git a/src/renderer/components/NewSessionModal.tsx b/src/renderer/components/NewSessionModal.tsx
index 3c16e3e..80f6194 100644
--- a/src/renderer/components/NewSessionModal.tsx
+++ b/src/renderer/components/NewSessionModal.tsx
@@ -1,8 +1,8 @@
-import { useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { X } from 'lucide-react';
import type { PatternDefinition } from '@shared/domain/pattern';
-import type { ProjectRecord } from '@shared/domain/project';
+import { isScratchpadProject, type ProjectRecord } from '@shared/domain/project';
interface NewSessionModalProps {
projects: ProjectRecord[];
@@ -19,10 +19,24 @@ export function NewSessionModal({
onClose,
onCreate,
}: NewSessionModalProps) {
- const availablePatterns = patterns.filter((p) => p.availability !== 'unavailable');
const [projectId, setProjectId] = useState(defaultProjectId ?? projects[0]?.id ?? '');
+ const availablePatterns = useMemo(
+ () =>
+ patterns.filter(
+ (pattern) =>
+ pattern.availability !== 'unavailable'
+ && (!isScratchpadProject(projectId) || pattern.mode === 'single'),
+ ),
+ [patterns, projectId],
+ );
const [patternId, setPatternId] = useState(availablePatterns[0]?.id ?? '');
+ useEffect(() => {
+ if (!availablePatterns.some((pattern) => pattern.id === patternId)) {
+ setPatternId(availablePatterns[0]?.id ?? '');
+ }
+ }, [availablePatterns, patternId]);
+
const canCreate = projectId && patternId;
return (
@@ -55,6 +69,12 @@ export function NewSessionModal({
))}
+ {isScratchpadProject(projectId) && (
+
+ Scratchpad is a projectless 1-on-1 chat for ad-hoc questions. It should behave like
+ pure Q&A rather than repo automation.
+
+ )}
)}
+ {isScratchpadProject(projectId) && (
+
+ Scratchpad supports single-agent chat patterns only.
+
+ )}
diff --git a/src/renderer/components/Sidebar.tsx b/src/renderer/components/Sidebar.tsx
index 0802efa..e5cb5e3 100644
--- a/src/renderer/components/Sidebar.tsx
+++ b/src/renderer/components/Sidebar.tsx
@@ -16,7 +16,7 @@ import {
} from 'lucide-react';
import type { OrchestrationMode, PatternDefinition } from '@shared/domain/pattern';
-import type { ProjectRecord } from '@shared/domain/project';
+import { isScratchpadProject, type ProjectRecord } from '@shared/domain/project';
import type { SessionRecord } from '@shared/domain/session';
import type { WorkspaceState } from '@shared/domain/workspace';
@@ -161,6 +161,7 @@ function ProjectGroup({
onSessionSelect: (sessionId: string) => void;
}) {
const [expanded, setExpanded] = useState(true);
+ const isScratchpad = isScratchpadProject(project);
const patternMap = useMemo(() => {
const map = new Map();
@@ -182,7 +183,11 @@ function ProjectGroup({
) : (
)}
-
+ {isScratchpad ? (
+
+ ) : (
+
+ )}
{project.name}
@@ -202,7 +207,7 @@ function ProjectGroup({
{sessions.length === 0 ? (
- No sessions yet
+ {isScratchpad ? 'No scratchpad chats yet' : 'No sessions yet'}
) : (
sessions.map((session) => (
@@ -231,6 +236,9 @@ export function Sidebar({
onSessionSelect,
onOpenSettings,
}: SidebarProps) {
+ const scratchpadProject = workspace.projects.find((project) => isScratchpadProject(project));
+ const userProjects = workspace.projects.filter((project) => !isScratchpadProject(project));
+
return (
{/* Header — extra top padding clears the title bar overlay zone */}
@@ -272,48 +280,69 @@ export function Sidebar({
{/* Project + Session Tree */}
- {workspace.projects.length === 0 ? (
-
-
-
-
+
+ {scratchpadProject && (
+
-
-
No projects yet
-
- Add a project folder to start
orchestrating AI agents
-
-
-
-
- ) : (
-
- {workspace.projects.map((project) => (
s.projectId === project.id)}
+ sessions={workspace.sessions.filter((session) => session.projectId === scratchpadProject.id)}
/>
- ))}
-
- )}
+
+ )}
+
+ {userProjects.length === 0 ? (
+
+
+
+
No projects yet
+
+ Use Scratchpad for ad-hoc chat or add a repo
to work against project files
+
+
+
+
+ ) : (
+
+
+ Projects
+
+ {userProjects.map((project) => (
+
session.projectId === project.id)}
+ />
+ ))}
+
+ )}
+
{/* Footer */}
- {workspace.projects.length > 0 && (
+ {userProjects.length > 0 && (
- {hasProjects ? (
+
+ {!hasProjects && (
- ) : (
-
)}
diff --git a/src/shared/contracts/sidecar.ts b/src/shared/contracts/sidecar.ts
index ff7f270..3de40c1 100644
--- a/src/shared/contracts/sidecar.ts
+++ b/src/shared/contracts/sidecar.ts
@@ -27,6 +27,7 @@ export interface RunTurnCommand {
requestId: string;
sessionId: string;
projectPath: string;
+ workspaceKind?: 'project' | 'scratchpad';
pattern: PatternDefinition;
messages: ChatMessageRecord[];
}
diff --git a/src/shared/domain/project.ts b/src/shared/domain/project.ts
index b22ddf6..b8fbe88 100644
--- a/src/shared/domain/project.ts
+++ b/src/shared/domain/project.ts
@@ -1,6 +1,44 @@
+import { nowIso } from '@shared/utils/ids';
+
export interface ProjectRecord {
id: string;
name: string;
path: string;
addedAt: string;
}
+
+export const SCRATCHPAD_PROJECT_ID = 'project-scratchpad';
+export const SCRATCHPAD_PROJECT_NAME = 'Scratchpad';
+
+export function createScratchpadProject(path: string, addedAt = nowIso()): ProjectRecord {
+ return {
+ id: SCRATCHPAD_PROJECT_ID,
+ name: SCRATCHPAD_PROJECT_NAME,
+ path,
+ addedAt,
+ };
+}
+
+export function isScratchpadProject(projectIdOrProject?: string | Pick
): boolean {
+ if (!projectIdOrProject) {
+ return false;
+ }
+
+ return (
+ (typeof projectIdOrProject === 'string' ? projectIdOrProject : projectIdOrProject.id)
+ === SCRATCHPAD_PROJECT_ID
+ );
+}
+
+export function mergeScratchpadProject(existingProjects: ProjectRecord[], scratchpadPath: string): ProjectRecord[] {
+ const existingScratchpad = existingProjects.find((project) => isScratchpadProject(project));
+ const scratchpadProject = createScratchpadProject(
+ scratchpadPath,
+ existingScratchpad?.addedAt ?? nowIso(),
+ );
+
+ return [
+ scratchpadProject,
+ ...existingProjects.filter((project) => !isScratchpadProject(project.id)),
+ ];
+}
diff --git a/tests/shared/project.test.ts b/tests/shared/project.test.ts
new file mode 100644
index 0000000..90b5a70
--- /dev/null
+++ b/tests/shared/project.test.ts
@@ -0,0 +1,46 @@
+import { describe, expect, test } from 'bun:test';
+
+import {
+ SCRATCHPAD_PROJECT_ID,
+ SCRATCHPAD_PROJECT_NAME,
+ createScratchpadProject,
+ isScratchpadProject,
+ mergeScratchpadProject,
+} from '@shared/domain/project';
+
+describe('scratchpad project helpers', () => {
+ test('creates a stable built-in scratchpad project', () => {
+ const project = createScratchpadProject('C:\\Users\\me\\AppData\\Roaming\\kopaya\\scratchpad');
+
+ expect(project.id).toBe(SCRATCHPAD_PROJECT_ID);
+ expect(project.name).toBe(SCRATCHPAD_PROJECT_NAME);
+ expect(project.path).toContain('scratchpad');
+ });
+
+ test('recognizes scratchpad project ids and records', () => {
+ expect(isScratchpadProject(SCRATCHPAD_PROJECT_ID)).toBe(true);
+ expect(
+ isScratchpadProject({
+ id: SCRATCHPAD_PROJECT_ID,
+ }),
+ ).toBe(true);
+ expect(isScratchpadProject('project-normal')).toBe(false);
+ });
+
+ test('merges scratchpad project ahead of normal user projects and preserves user projects', () => {
+ const merged = mergeScratchpadProject(
+ [
+ {
+ id: 'project-a',
+ name: 'Repo A',
+ path: 'C:\\repo-a',
+ addedAt: '2026-03-23T00:00:00.000Z',
+ },
+ ],
+ 'C:\\Users\\me\\AppData\\Roaming\\kopaya\\scratchpad',
+ );
+
+ expect(merged[0].id).toBe(SCRATCHPAD_PROJECT_ID);
+ expect(merged[1].id).toBe('project-a');
+ });
+});