feat: add scratchpad chat sessions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-22 10:11:42 +01:00
co-authored by Copilot
parent 2f90a19736
commit 1b41cbd1e0
15 changed files with 300 additions and 73 deletions
@@ -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<ChatMessageDto> Messages { get; init; } = [];
}
@@ -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()));
}
}
@@ -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<ChatMessage> inputMessages = command.Messages.Select(ToChatMessage).ToList();
@@ -436,15 +436,15 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
public IReadOnlyList<AIAgent> Agents { get; }
public static async Task<AgentBundle> CreateAsync(
PatternDefinitionDto pattern,
string projectPath,
RunTurnCommandDto command,
CancellationToken cancellationToken)
{
List<IAsyncDisposable> disposables = [];
List<AIAgent> 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,
@@ -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
+7 -1
View File
@@ -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<AppServiceEvents> {
}
async removeProject(projectId: string): Promise<WorkspaceState> {
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<AppServiceEvents> {
});
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<AppServiceEvents> {
requestId,
sessionId: session.id,
projectPath: project.path,
workspaceKind,
pattern,
messages: session.messages,
},
+4
View File
@@ -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');
}
+20 -3
View File
@@ -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<WorkspaceState> {
await mkdir(this.scratchpadPath, { recursive: true });
const stored = await readJsonFile<WorkspaceState>(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(),
};
+6 -1
View File
@@ -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 = (
<WelcomePane
hasProjects={workspace.projects.length > 0}
hasProjects={hasUserProjects}
onAddProject={() => void api.addProject()}
onNewSession={() => setShowNewSession(true)}
onOpenSettings={() => setShowSettings(true)}
+14 -4
View File
@@ -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<HTMLTextAreaElement>(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) {
<div className="min-w-0">
<h2 className="truncate text-sm font-semibold text-zinc-100">{session.title}</h2>
<p className="mt-0.5 truncate text-[12px] text-zinc-500">
{project.name} · {pattern.name} · {pattern.mode}
{isScratchpad ? `Scratchpad · ${pattern.name}` : `${project.name} · ${pattern.name} · ${pattern.mode}`}
</p>
</div>
<div className="flex items-center gap-2">
@@ -90,8 +91,17 @@ export function ChatPane({ project, pattern, session, onSend }: ChatPaneProps) {
Send a message to start the conversation
</p>
<p className="text-[12px] text-zinc-700">
Using <span className="text-zinc-500">{pattern.name}</span> in{' '}
<span className="text-zinc-500">{project.name}</span>
{isScratchpad ? (
<>
Scratchpad is ready for ad-hoc questions using{' '}
<span className="text-zinc-500">{pattern.name}</span>
</>
) : (
<>
Using <span className="text-zinc-500">{pattern.name}</span> in{' '}
<span className="text-zinc-500">{project.name}</span>
</>
)}
</p>
</div>
) : (
+28 -3
View File
@@ -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({
</option>
))}
</select>
{isScratchpadProject(projectId) && (
<p className="text-[12px] leading-relaxed text-zinc-600">
Scratchpad is a projectless 1-on-1 chat for ad-hoc questions. It should behave like
pure Q&A rather than repo automation.
</p>
)}
</label>
<label className="block space-y-1.5">
@@ -75,6 +95,11 @@ export function NewSessionModal({
{availablePatterns.find((p) => p.id === patternId)?.description}
</p>
)}
{isScratchpadProject(projectId) && (
<p className="text-[12px] leading-relaxed text-zinc-700">
Scratchpad supports single-agent chat patterns only.
</p>
)}
</label>
</div>
+65 -36
View File
@@ -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<string, PatternDefinition>();
@@ -182,7 +183,11 @@ function ProjectGroup({
) : (
<ChevronRight className="size-3 shrink-0 text-zinc-500" />
)}
<FolderOpen className="size-3.5 shrink-0 text-zinc-500 transition group-hover:text-indigo-400" />
{isScratchpad ? (
<MessageSquare className="size-3.5 shrink-0 text-zinc-500 transition group-hover:text-indigo-400" />
) : (
<FolderOpen className="size-3.5 shrink-0 text-zinc-500 transition group-hover:text-indigo-400" />
)}
<span className="truncate">{project.name}</span>
<div className="ml-auto flex items-center gap-1.5">
@@ -202,7 +207,7 @@ function ProjectGroup({
<div className="ml-2 mt-0.5 space-y-0.5 border-l border-zinc-800/60 pl-2">
{sessions.length === 0 ? (
<div className="px-3 py-3 text-center text-[12px] text-zinc-600">
No sessions yet
{isScratchpad ? 'No scratchpad chats yet' : 'No sessions yet'}
</div>
) : (
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 (
<div className="flex h-full flex-col">
{/* Header — extra top padding clears the title bar overlay zone */}
@@ -272,48 +280,69 @@ export function Sidebar({
{/* Project + Session Tree */}
<div className="flex-1 overflow-y-auto px-2 py-2">
{workspace.projects.length === 0 ? (
<div className="flex flex-col items-center gap-4 px-4 py-10 text-center">
<div className="relative">
<div className="flex size-14 items-center justify-center rounded-2xl bg-zinc-800/50 ring-1 ring-zinc-700/50">
<FolderOpen className="size-7 text-zinc-600" />
<div className="space-y-3">
{scratchpadProject && (
<div className="space-y-1">
<div className="px-2 text-[10px] font-semibold uppercase tracking-[0.12em] text-zinc-600">
Scratchpad
</div>
<div className="absolute -bottom-1 -right-1 flex size-6 items-center justify-center rounded-full bg-indigo-600 ring-2 ring-[var(--color-surface-1)]">
<Plus className="size-3 text-white" />
</div>
</div>
<div>
<p className="text-[13px] font-medium text-zinc-300">No projects yet</p>
<p className="mt-1 text-[12px] leading-relaxed text-zinc-500">
Add a project folder to start<br />orchestrating AI agents
</p>
</div>
<button
className="rounded-lg bg-indigo-600 px-4 py-2 text-[13px] font-medium text-white transition hover:bg-indigo-500"
onClick={onAddProject}
type="button"
>
Add Project
</button>
</div>
) : (
<div className="space-y-1">
{workspace.projects.map((project) => (
<ProjectGroup
key={project.id}
key={scratchpadProject.id}
onSessionSelect={onSessionSelect}
patterns={workspace.patterns}
project={project}
project={scratchpadProject}
selectedSessionId={workspace.selectedSessionId}
sessions={workspace.sessions.filter((s) => s.projectId === project.id)}
sessions={workspace.sessions.filter((session) => session.projectId === scratchpadProject.id)}
/>
))}
</div>
)}
</div>
)}
{userProjects.length === 0 ? (
<div className="flex flex-col items-center gap-4 px-4 py-8 text-center">
<div className="relative">
<div className="flex size-14 items-center justify-center rounded-2xl bg-zinc-800/50 ring-1 ring-zinc-700/50">
<FolderOpen className="size-7 text-zinc-600" />
</div>
<div className="absolute -bottom-1 -right-1 flex size-6 items-center justify-center rounded-full bg-indigo-600 ring-2 ring-[var(--color-surface-1)]">
<Plus className="size-3 text-white" />
</div>
</div>
<div>
<p className="text-[13px] font-medium text-zinc-300">No projects yet</p>
<p className="mt-1 text-[12px] leading-relaxed text-zinc-500">
Use Scratchpad for ad-hoc chat or add a repo<br />to work against project files
</p>
</div>
<button
className="rounded-lg bg-indigo-600 px-4 py-2 text-[13px] font-medium text-white transition hover:bg-indigo-500"
onClick={onAddProject}
type="button"
>
Add Project
</button>
</div>
) : (
<div className="space-y-1">
<div className="px-2 text-[10px] font-semibold uppercase tracking-[0.12em] text-zinc-600">
Projects
</div>
{userProjects.map((project) => (
<ProjectGroup
key={project.id}
onSessionSelect={onSessionSelect}
patterns={workspace.patterns}
project={project}
selectedSessionId={workspace.selectedSessionId}
sessions={workspace.sessions.filter((session) => session.projectId === project.id)}
/>
))}
</div>
)}
</div>
</div>
{/* Footer */}
{workspace.projects.length > 0 && (
{userProjects.length > 0 && (
<div className="border-t border-[var(--color-border)] px-3 py-2">
<button
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-[12px] text-zinc-500 transition hover:bg-zinc-800/60 hover:text-zinc-300"
+13 -14
View File
@@ -23,28 +23,27 @@ export function WelcomePane({
<div>
<h1 className="text-xl font-semibold text-zinc-100">Welcome to kopaya</h1>
<p className="mt-2 max-w-md text-sm leading-relaxed text-zinc-500">
Orchestrate AI agents across your projects. Start a session to begin a conversation
with one or more Copilot-backed agents.
Start a scratchpad conversation for ad-hoc questions or connect a project to work with
repo-aware Copilot agents.
</p>
</div>
<div className="flex flex-col items-center gap-2">
{hasProjects ? (
<button
className="flex items-center gap-2 rounded-lg bg-indigo-600 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-indigo-500"
onClick={onNewSession}
type="button"
>
<Plus className="size-4" />
New Session
</button>
{!hasProjects && (
<button
className="flex items-center gap-2 rounded-lg bg-indigo-600 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-indigo-500"
onClick={onNewSession}
type="button"
>
<Plus className="size-4" />
New Session
</button>
) : (
<button
className="flex items-center gap-2 rounded-lg bg-indigo-600 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-indigo-500"
className="flex items-center gap-2 rounded-lg px-4 py-2 text-sm text-zinc-500 transition hover:bg-zinc-900 hover:text-zinc-300"
onClick={onAddProject}
type="button"
>
<Plus className="size-4" />
<Plus className="size-3.5" />
Add Your First Project
</button>
)}
+1
View File
@@ -27,6 +27,7 @@ export interface RunTurnCommand {
requestId: string;
sessionId: string;
projectPath: string;
workspaceKind?: 'project' | 'scratchpad';
pattern: PatternDefinition;
messages: ChatMessageRecord[];
}
+38
View File
@@ -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<ProjectRecord, 'id'>): 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)),
];
}
+46
View File
@@ -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');
});
});