feat: enable scratchpad tooling frontend and instruction update

- Remove !isScratchpad gate on tooling/approval pills in ChatPane
- Update scratchpad agent guidance to permit tool/file usage
- Add EryxAppService scratchpad tooling integration tests
- Update ARCHITECTURE.md to reflect scratchpad tooling support

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-25 21:20:43 +01:00
co-authored by Copilot
parent 4ecfa5bff6
commit cc3c04b841
5 changed files with 215 additions and 6 deletions
+1 -1
View File
@@ -263,7 +263,7 @@ This lets the application treat tooling as reusable workspace capability while s
### Project awareness
Project-backed sessions can carry repository context such as branch and dirty state, while scratchpad sessions stay lightweight. This keeps the architecture grounded in real codebases without forcing every conversation to be project-heavy.
Project-backed sessions can carry repository context such as branch and dirty state, while scratchpad sessions omit git context but still support MCP, LSP, and runtime tooling. Both session kinds share the same tooling selection and approval model. This keeps the architecture grounded in real codebases without forcing every conversation to be project-heavy, while still letting scratchpad sessions leverage configured tools when useful.
### Execution observability
@@ -14,8 +14,9 @@ internal static class AgentInstructionComposer
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.
Treat this session as ad-hoc work inside the scratchpad workspace rather than repository automation against a connected user project.
You may use the available tools and files inside the scratchpad workspace when they help answer the request.
Do not assume there is a connected repository, checked-out branch, or project-specific context unless the user provides it in the conversation.
Answer conversationally and focus on the user's question directly.
"""
: string.Empty;
@@ -119,8 +119,9 @@ public sealed class AgentInstructionComposerTests
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);
Assert.Contains("ad-hoc work inside the scratchpad workspace", instructions, StringComparison.OrdinalIgnoreCase);
Assert.Contains("use the available tools and files inside the scratchpad workspace", instructions, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("Do not inspect, modify, create, or delete files", instructions, StringComparison.OrdinalIgnoreCase);
}
private static PatternAgentDefinitionDto CreateAgent(string id, string name, string instructions)
+1 -1
View File
@@ -369,7 +369,7 @@ export function ChatPane({
)}
{/* Session config pills — tool & approval controls */}
{!isScratchpad && (hasConfigurableTools || hasToolCallApproval) && (
{(hasConfigurableTools || hasToolCallApproval) && (
<div className="mb-2 flex items-center gap-2">
{hasConfigurableTools && onUpdateSessionTooling && (
<InlineToolsPill
+207
View File
@@ -0,0 +1,207 @@
import { describe, expect, mock, test } from 'bun:test';
import type { RunTurnCommand } from '@shared/contracts/sidecar';
import type { PatternDefinition } from '@shared/domain/pattern';
import { createScratchpadProject } from '@shared/domain/project';
import type { SessionRecord } from '@shared/domain/session';
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
const TIMESTAMP = '2026-03-25T00:00:00.000Z';
const SCRATCHPAD_PATH = 'C:\\workspace\\personal\\repositories\\eryx\\scratchpad';
mock.module('electron', () => ({
app: {
isPackaged: false,
getAppPath: () => 'C:\\workspace\\personal\\repositories\\eryx',
getPath: () => 'C:\\workspace\\personal\\repositories\\eryx\\tests\\fixtures',
},
dialog: {
showOpenDialog: async () => ({ canceled: true, filePaths: [] }),
},
}));
mock.module('keytar', () => ({
default: {
getPassword: async () => null,
setPassword: async () => undefined,
deletePassword: async () => false,
},
}));
const { EryxAppService } = await import('@main/EryxAppService');
function createWorkspaceFixture(): {
workspace: WorkspaceState;
pattern: PatternDefinition;
session: SessionRecord;
} {
const workspace = createWorkspaceSeed();
const pattern = workspace.patterns.find((candidate) => candidate.mode === 'single');
if (!pattern) {
throw new Error('Expected the workspace seed to include a single-agent pattern.');
}
const project = createScratchpadProject(SCRATCHPAD_PATH, TIMESTAMP);
const session: SessionRecord = {
id: 'session-scratchpad',
projectId: project.id,
patternId: pattern.id,
title: 'Scratchpad',
createdAt: TIMESTAMP,
updatedAt: TIMESTAMP,
status: 'idle',
messages: [],
runs: [],
};
workspace.projects = [project];
workspace.sessions = [session];
workspace.selectedProjectId = project.id;
workspace.selectedPatternId = pattern.id;
workspace.selectedSessionId = session.id;
workspace.settings.tooling = {
mcpServers: [
{
id: 'mcp-git',
name: 'Git MCP',
transport: 'local',
command: 'node',
args: ['server.js', '--stdio'],
cwd: 'C:\\workspace\\personal\\repositories\\eryx',
tools: ['git.status'],
timeoutMs: 1500,
createdAt: TIMESTAMP,
updatedAt: TIMESTAMP,
},
],
lspProfiles: [
{
id: 'lsp-ts',
name: 'TypeScript',
command: 'typescript-language-server',
args: ['--stdio'],
languageId: 'typescript',
fileExtensions: ['.ts', '.tsx'],
createdAt: TIMESTAMP,
updatedAt: TIMESTAMP,
},
],
};
return { workspace, pattern, session };
}
function createService(
workspace: WorkspaceState,
pattern: PatternDefinition,
options?: {
captureRunTurn?: (command: RunTurnCommand) => void;
knownApprovalToolNames?: string[];
},
): InstanceType<typeof EryxAppService> {
const service = new EryxAppService();
const internals = service as unknown as Record<string, unknown>;
internals.loadWorkspace = async () => workspace;
internals.persistAndBroadcast = async (nextWorkspace: WorkspaceState) => nextWorkspace;
internals.buildEffectivePattern = async () => pattern;
internals.awaitFinalResponseApproval = async () => undefined;
internals.finalizeTurn = () => undefined;
internals.emitSessionEvent = () => undefined;
internals.listKnownApprovalToolNames = async () =>
options?.knownApprovalToolNames ?? ['glob', 'git.status', 'lsp_lsp_ts_definition'];
(
service as unknown as {
sidecar: {
runTurn: (
command: RunTurnCommand,
onDelta: unknown,
onActivity: unknown,
onApproval: unknown,
) => Promise<[]>;
resolveApproval: () => Promise<void>;
};
}
).sidecar = {
runTurn: async (command) => {
options?.captureRunTurn?.(command);
return [];
},
resolveApproval: async () => undefined,
};
return service;
}
describe('EryxAppService scratchpad tooling support', () => {
test('allows scratchpad sessions to save MCP and LSP selections', async () => {
const { workspace, pattern, session } = createWorkspaceFixture();
const service = createService(workspace, pattern);
await service.updateSessionTooling(session.id, ['mcp-git'], ['lsp-ts']);
expect(session.tooling).toEqual({
enabledMcpServerIds: ['mcp-git'],
enabledLspProfileIds: ['lsp-ts'],
});
});
test('allows scratchpad sessions to save auto-approved tool overrides', async () => {
const { workspace, pattern, session } = createWorkspaceFixture();
const service = createService(workspace, pattern, {
knownApprovalToolNames: ['glob', 'git.status', 'lsp_lsp_ts_definition'],
});
await service.updateSessionApprovalSettings(session.id, ['glob', 'git.status']);
expect(session.approvalSettings).toEqual({
autoApprovedToolNames: ['glob', 'git.status'],
});
});
test('sends scratchpad-selected tooling to the sidecar run-turn command', async () => {
const { workspace, pattern, session } = createWorkspaceFixture();
session.tooling = {
enabledMcpServerIds: ['mcp-git'],
enabledLspProfileIds: ['lsp-ts'],
};
let command: RunTurnCommand | undefined;
const service = createService(workspace, pattern, {
captureRunTurn: (capturedCommand) => {
command = capturedCommand;
},
});
await service.sendSessionMessage(session.id, 'Inspect the scratchpad tooling wiring.');
expect(command).toMatchObject({
sessionId: session.id,
projectPath: SCRATCHPAD_PATH,
workspaceKind: 'scratchpad',
tooling: {
mcpServers: [
{
id: 'mcp-git',
name: 'Git MCP',
transport: 'local',
command: 'node',
args: ['server.js', '--stdio'],
cwd: 'C:\\workspace\\personal\\repositories\\eryx',
tools: ['git.status'],
timeoutMs: 1500,
},
],
lspProfiles: [
{
id: 'lsp-ts',
name: 'TypeScript',
command: 'typescript-language-server',
args: ['--stdio'],
languageId: 'typescript',
fileExtensions: ['.ts', '.tsx'],
},
],
},
});
});
});