Repository navigation
feat(scratchpad): add per-session scratchpad viewer #242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| /** | ||
| * HTTP route handlers for the per-session Scratchpad viewer. | ||
| * | ||
| * Mirrors the read paths of the IPC surface. Opening files in the OS file | ||
| * manager has no meaning for a browser client, so it is not exposed here. | ||
| */ | ||
|
|
||
| import { scratchpadReader } from '@main/services/discovery/ScratchpadReader'; | ||
| import { createLogger } from '@shared/utils/logger'; | ||
|
|
||
| import { validateScratchpadArgs } from '../ipc/guards'; | ||
|
|
||
| import type { ScratchpadListResult, ScratchpadReadFileResult } from '@shared/types'; | ||
| import type { FastifyInstance } from 'fastify'; | ||
|
|
||
| const logger = createLogger('HTTP:scratchpad'); | ||
|
|
||
| interface ScratchpadQuery { | ||
| projectId?: string; | ||
| sessionId?: string; | ||
| path?: string; | ||
| } | ||
|
|
||
| export function registerScratchpadRoutes(app: FastifyInstance): void { | ||
| app.get<{ Querystring: ScratchpadQuery }>( | ||
| '/api/scratchpad/list', | ||
| async (request): Promise<ScratchpadListResult> => { | ||
| const { projectId, sessionId, path } = request.query; | ||
| const args = validateScratchpadArgs(projectId, sessionId, path); | ||
| if (!args.valid) return { success: false, error: args.error }; | ||
| try { | ||
| const listing = await scratchpadReader.list( | ||
| args.projectId, | ||
| args.sessionId, | ||
| args.relativePath | ||
| ); | ||
| return { success: true, ...listing }; | ||
| } catch (error) { | ||
| logger.error('Error in GET /api/scratchpad/list:', error); | ||
| return { success: false, error: error instanceof Error ? error.message : String(error) }; | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| app.get<{ Querystring: ScratchpadQuery }>( | ||
| '/api/scratchpad/file', | ||
| async (request): Promise<ScratchpadReadFileResult> => { | ||
| const { projectId, sessionId, path } = request.query; | ||
| const args = validateScratchpadArgs(projectId, sessionId, path); | ||
| if (!args.valid) return { success: false, error: args.error }; | ||
| try { | ||
| const result = await scratchpadReader.readFile( | ||
| args.projectId, | ||
| args.sessionId, | ||
| args.relativePath | ||
| ); | ||
| return { success: true, ...result }; | ||
| } catch (error) { | ||
| logger.error('Error in GET /api/scratchpad/file:', error); | ||
| return { success: false, error: error instanceof Error ? error.message : String(error) }; | ||
| } | ||
| } | ||
| ); | ||
| } | ||
|
Comment on lines
+45
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: sed -n '170,205p' README.md
sed -n '1,90p' SECURITY.md
rg -n 'projectId|sessionId|/api/projects|/api/sessions|auth|reverse.proxy' src/main/http README.md SECURITY.mdRepository: matt1398/claude-devtools Length of output: 15202 🏁 Script executed: set -eu
printf '%s\n' '--- projects ---'
cat -n src/main/http/projects.ts
printf '%s\n' '--- sessions listing ---'
sed -n '1,125p' src/main/http/sessions.ts | cat -n
printf '%s\n' '--- session detail and ID validation ---'
sed -n '135,180p' src/main/http/sessions.ts | cat -n
rg -n -C 4 'function validate(Project|Session)Id|const validate(Project|Session)Id|export .*validate(Project|Session)Id' src
printf '%s\n' '--- standalone/server binding ---'
sed -n '35,180p' src/main/standalone.ts | cat -n
sed -n '50,140p' src/main/services/infrastructure/HttpServer.ts | cat -n
printf '%s\n' '--- deployment/security guidance ---'
sed -n '175,205p' README.md | cat -n
sed -n '1,45p' SECURITY.md | cat -nRepository: matt1398/claude-devtools Length of output: 24233 🏁 Script executed: set -eu
printf '%s\n' '--- scratchpad routes ---'
cat -n src/main/http/scratchpad.ts
printf '%s\n' '--- scratchpad reader definitions ---'
rg -n -C 5 'class ScratchpadReader|listFiles|readFile|Scratchpad' src/main/services/discovery src/main/services src/main/types src/shared 2>/dev/null | head -240
printf '%s\n' '--- project scanner definitions and result shapes ---'
rg -n -C 6 'class ProjectScanner|async scan\\(|scanWithWorktreeGrouping|listSessions\\(|ProjectSummary|projectId.*string|sessionId.*string' src/main/services src/main/types src/shared 2>/dev/null | head -320
printf '%s\n' '--- identifier guard bodies ---'
sed -n '1,85p' src/main/ipc/guards.ts | cat -nRepository: matt1398/claude-devtools Length of output: 24979 🏁 Script executed: set -eu
printf '%s\n' '--- ProjectScanner outline ---'
ast-grep outline src/main/services/discovery/ProjectScanner.ts
printf '%s\n' '--- ProjectScanner scan/list methods ---'
rg -n -C 12 'scan\\(|listSessions\\(|interface .*Project|type .*Project|projectId:' src/main/services/discovery/ProjectScanner.ts src/main/types src/shared
printf '%s\n' '--- ScratchpadReader root and read body ---'
sed -n '70,190p' src/main/services/discovery/ScratchpadReader.ts | cat -n
printf '%s\n' '--- path decoder ---'
cat -n src/main/utils/pathDecoder.tsRepository: matt1398/claude-devtools Length of output: 1489 🏁 Script executed: printf '%s\n' '--- ProjectScanner method locations ---'
grep -nE 'scan\(|listSessions\(|getRootPath|resolveProjectPathForId' src/main/services/discovery/ProjectScanner.ts src/main/services/discovery/ScratchpadReader.ts || true
printf '%s\n' '--- ProjectScanner scan/list implementations ---'
sed -n '100,230p' src/main/services/discovery/ProjectScanner.ts
sed -n '300,430p' src/main/services/discovery/ProjectScanner.ts
printf '%s\n' '--- ScratchpadReader root and path handling ---'
sed -n '70,190p' src/main/services/discovery/ScratchpadReader.ts
printf '%s\n' '--- path decoder ---'
cat src/main/utils/pathDecoder.tsRepository: matt1398/claude-devtools Length of output: 25261 Require authentication before exposing scratchpad contents. Standalone mode binds to 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| /** | ||
| * IPC Handlers for the per-session Scratchpad viewer. | ||
| * | ||
| * Surface: | ||
| * - scratchpad:list → ScratchpadListResult (one directory level) | ||
| * - scratchpad:readFile → ScratchpadReadFileResult (text, image or binary stub) | ||
| * - scratchpad:openPath → reveal a file in the OS file manager, or open a directory | ||
| * | ||
| * Read-only. Scratchpads live in the local temp directory, so SSH contexts are | ||
| * rejected rather than resolved against the wrong machine. | ||
| */ | ||
|
|
||
| import { scratchpadReader } from '@main/services/discovery/ScratchpadReader'; | ||
| import { createLogger } from '@shared/utils/logger'; | ||
| import { type IpcMain, type IpcMainInvokeEvent, shell } from 'electron'; | ||
| import * as fs from 'fs'; | ||
|
|
||
| import { validateScratchpadArgs } from './guards'; | ||
|
|
||
| import type { ServiceContextRegistry } from '../services'; | ||
| import type { | ||
| ScratchpadListResult, | ||
| ScratchpadOpenResult, | ||
| ScratchpadReadFileResult, | ||
| } from '@shared/types'; | ||
|
|
||
| const SCRATCHPAD_LIST = 'scratchpad:list'; | ||
| const SCRATCHPAD_READ_FILE = 'scratchpad:readFile'; | ||
| const SCRATCHPAD_OPEN_PATH = 'scratchpad:openPath'; | ||
|
|
||
| const logger = createLogger('IPC:scratchpad'); | ||
|
|
||
| let registry: ServiceContextRegistry; | ||
|
|
||
| export function initializeScratchpadHandlers(contextRegistry: ServiceContextRegistry): void { | ||
| registry = contextRegistry; | ||
| } | ||
|
|
||
| export function registerScratchpadHandlers(ipcMain: IpcMain): void { | ||
| ipcMain.handle(SCRATCHPAD_LIST, handleList); | ||
| ipcMain.handle(SCRATCHPAD_READ_FILE, handleReadFile); | ||
| ipcMain.handle(SCRATCHPAD_OPEN_PATH, handleOpenPath); | ||
| logger.info('Scratchpad handlers registered'); | ||
| } | ||
|
|
||
| export function removeScratchpadHandlers(ipcMain: IpcMain): void { | ||
| ipcMain.removeHandler(SCRATCHPAD_LIST); | ||
| ipcMain.removeHandler(SCRATCHPAD_READ_FILE); | ||
| ipcMain.removeHandler(SCRATCHPAD_OPEN_PATH); | ||
| } | ||
|
|
||
| function sshUnsupported(): { success: false; error: string } | null { | ||
| return registry?.getActive().type === 'ssh' | ||
| ? { success: false, error: 'Scratchpads are only available for local sessions' } | ||
| : null; | ||
| } | ||
|
|
||
| async function handleList( | ||
| _event: IpcMainInvokeEvent, | ||
| projectId: unknown, | ||
| sessionId: unknown, | ||
| relativeDir: unknown | ||
| ): Promise<ScratchpadListResult> { | ||
| const args = validateScratchpadArgs(projectId, sessionId, relativeDir); | ||
| if (!args.valid) return { success: false, error: args.error }; | ||
| const unsupported = sshUnsupported(); | ||
| if (unsupported) return unsupported; | ||
| try { | ||
| const listing = await scratchpadReader.list(args.projectId, args.sessionId, args.relativePath); | ||
| return { success: true, ...listing }; | ||
| } catch (error) { | ||
| logger.error('Error in scratchpad:list:', error); | ||
| return { success: false, error: error instanceof Error ? error.message : String(error) }; | ||
| } | ||
| } | ||
|
|
||
| async function handleReadFile( | ||
| _event: IpcMainInvokeEvent, | ||
| projectId: unknown, | ||
| sessionId: unknown, | ||
| relativePath: unknown | ||
| ): Promise<ScratchpadReadFileResult> { | ||
| const args = validateScratchpadArgs(projectId, sessionId, relativePath); | ||
| if (!args.valid) return { success: false, error: args.error }; | ||
| const unsupported = sshUnsupported(); | ||
| if (unsupported) return unsupported; | ||
| try { | ||
| const result = await scratchpadReader.readFile( | ||
| args.projectId, | ||
| args.sessionId, | ||
| args.relativePath | ||
| ); | ||
| return { success: true, ...result }; | ||
| } catch (error) { | ||
| logger.error('Error in scratchpad:readFile:', error); | ||
| return { success: false, error: error instanceof Error ? error.message : String(error) }; | ||
| } | ||
| } | ||
|
|
||
| async function handleOpenPath( | ||
| _event: IpcMainInvokeEvent, | ||
| projectId: unknown, | ||
| sessionId: unknown, | ||
| relativePath: unknown | ||
| ): Promise<ScratchpadOpenResult> { | ||
| const args = validateScratchpadArgs(projectId, sessionId, relativePath); | ||
| if (!args.valid) return { success: false, error: args.error }; | ||
| const unsupported = sshUnsupported(); | ||
| if (unsupported) return unsupported; | ||
| try { | ||
| const rootPath = scratchpadReader.getRootPath(args.projectId, args.sessionId); | ||
| const target = await scratchpadReader.resolveInside(rootPath, args.relativePath); | ||
| if ((await fs.promises.stat(target)).isDirectory()) { | ||
| const error = await shell.openPath(target); | ||
| return error ? { success: false, error } : { success: true }; | ||
| } | ||
| shell.showItemInFolder(target); | ||
| return { success: true }; | ||
| } catch (error) { | ||
| logger.error('Error in scratchpad:openPath:', error); | ||
| return { success: false, error: error instanceof Error ? error.message : String(error) }; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: matt1398/claude-devtools
Length of output: 6014
🏁 Script executed:
Repository: matt1398/claude-devtools
Length of output: 42419
🏁 Script executed:
Repository: matt1398/claude-devtools
Length of output: 42201
Gate scratchpad HTTP routes using the captured HTTP context.
startHttpServerpasses the active context's services toHttpServeronce. Later context switches do not replace those services. AgetActiveTypecallback would inspect the registry's current context instead of the context serving the HTTP session IDs. This can allow local scratchpad reads for remote session IDs, or reject local scratchpads after a later SSH switch.Pass the captured context type through
HttpServicesand use it for both scratchpad routes.Suggested fix
export interface HttpServices { + contextType: 'local' | 'ssh'; projectScanner: ProjectScanner; ... - registerScratchpadRoutes(app); + registerScratchpadRoutes(app, services.contextType);Add
contextType: activeContext.typeto the desktopHttpServicesobject andcontextType: localContext.typeto the standalone object.📝 Committable suggestion
🤖 Prompt for AI Agents