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
+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>
)}