From c9cd5119077c69088eb8ba768ee1e1602baec751 Mon Sep 17 00:00:00 2001 From: haoyuren <13851610112@163.com> Date: Thu, 30 Jul 2026 08:53:27 +0800 Subject: Add workspace file browser and OpenAlex citation search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspace browser: - The left panel gains a Project/Workspace toggle; the Workspace view browses the agent scratch space (claude-workspace/, never synced) - Open/edit workspace text files in the editor with debounced auto-save (flushed on tab switch); binaries open with the system default app - Import to Project copies a workspace file/folder into the project root where the sync bridge picks it up and creates it on Overleaf (collision-safe: falls back to a -imported suffix) - New file/folder, rename, delete, reveal in Finder, drag-drop into the workspace, 5s auto-refresh while visible OpenAlex search (MCP): - New search_openalex tool: broad OpenAlex coverage with an explicit citation-authority policy — BibTeX is cross-checked against Semantic Scholar (by DOI, then by exact title, since OpenAlex DOIs can point at mirrors); entries not found on S2 are flagged "OpenAlex only" with instructions to verify via web search before citing - Tool description, agent guide, permission allowlist, and README updated Co-Authored-By: Claude Fable 5 --- src/main/index.ts | 100 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 8 deletions(-) (limited to 'src/main/index.ts') diff --git a/src/main/index.ts b/src/main/index.ts index 32be5b3..4f23762 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2,8 +2,9 @@ // Licensed under AGPL-3.0 - see LICENSE file import { app, BrowserWindow, ipcMain, dialog, shell, net } from 'electron' -import { join, basename, relative, extname } from 'path' -import { copyFile, readFile, writeFile, mkdir as mkdirAsync, unlink, readdir, stat } from 'fs/promises' +import { join, basename, dirname, relative, extname } from 'path' +import { copyFile, readFile, writeFile, mkdir as mkdirAsync, unlink, readdir, stat, rename as fsRename, rm, cp } from 'fs/promises' +import { existsSync } from 'fs' import { spawn } from 'child_process' import * as pty from 'node-pty' import { OverleafSocket, type RootFolder, type SubFolder, type JoinDocResult } from './overleafSocket' @@ -160,6 +161,82 @@ ipcMain.handle('fs:readBinary', async (_e, filePath: string) => { return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) }) +// ── Workspace file operations (agent scratch space browser) ───── + +ipcMain.handle('fs:writeFile', async (_e, filePath: string, content: string) => { + await mkdirAsync(dirname(filePath), { recursive: true }) + await writeFile(filePath, content, 'utf-8') +}) + +interface DiskNode { + name: string + path: string + isDir: boolean + children?: DiskNode[] +} + +// List a directory tree from disk. Paths in the result are `pathPrefix` + +// path relative to rootPath (so the renderer can key tabs consistently). +ipcMain.handle('fs:listDirTree', async (_e, rootPath: string, pathPrefix: string) => { + const MAX_ENTRIES_PER_DIR = 500 + const MAX_DEPTH = 10 + + async function walk(dir: string, rel: string, depth: number): Promise { + if (depth > MAX_DEPTH) return [] + let entries + try { + entries = await readdir(dir, { withFileTypes: true }) + } catch { + return [] + } + entries = entries + .filter((e) => !e.name.startsWith('.')) + .sort((a, b) => + (b.isDirectory() ? 1 : 0) - (a.isDirectory() ? 1 : 0) || a.name.localeCompare(b.name) + ) + .slice(0, MAX_ENTRIES_PER_DIR) + + const nodes: DiskNode[] = [] + for (const entry of entries) { + const relPath = rel ? `${rel}/${entry.name}` : entry.name + if (entry.isDirectory()) { + nodes.push({ + name: entry.name, + path: pathPrefix + relPath, + isDir: true, + children: await walk(join(dir, entry.name), relPath, depth + 1) + }) + } else if (entry.isFile()) { + nodes.push({ name: entry.name, path: pathPrefix + relPath, isDir: false }) + } + } + return nodes + } + + return walk(rootPath, '', 0) +}) + +ipcMain.handle('fs:mkdirp', async (_e, dirPath: string) => { + await mkdirAsync(dirPath, { recursive: true }) +}) + +ipcMain.handle('fs:rename', async (_e, oldPath: string, newPath: string) => { + await fsRename(oldPath, newPath) +}) + +ipcMain.handle('fs:deletePath', async (_e, targetPath: string) => { + await rm(targetPath, { recursive: true, force: true }) +}) + +ipcMain.handle('fs:copyPath', async (_e, src: string, dest: string) => { + await mkdirAsync(dirname(dest), { recursive: true }) + await cp(src, dest, { recursive: true }) +}) + +ipcMain.handle('fs:exists', async (_e, targetPath: string) => { + return existsSync(targetPath) +}) + // ── API Key Storage ───────────────────────────────────────────── const apiKeysPath = join(app.getPath('userData'), 'api-keys.json') @@ -1031,7 +1108,8 @@ You have MCP tools to interact with Overleaf. Use them proactively. - **read_compiled_pdf**: Get the path to the compiled PDF. After calling this, use your **Read** tool on the returned path to visually inspect the PDF. Use the \`pages\` parameter (e.g. \`"1-3"\`) to read specific pages. This lets you verify formatting, figures, tables, and layout. ### Bibliography -- **search_citation**: Search academic papers by title, topic, or author. Returns matching papers with ready-to-use BibTeX entries that can be pasted directly into a \`.bib\` file. **Note:** Without a Semantic Scholar API key configured in LatteX settings, requests will likely be rate-limited (HTTP 429). With a key, the rate limit is 1 request/second. +- **search_citation**: Search academic papers by title, topic, or author (Semantic Scholar). Returns matching papers with ready-to-use BibTeX entries that can be pasted directly into a \`.bib\` file. **Note:** Without a Semantic Scholar API key configured in LatteX settings, requests will likely be rate-limited (HTTP 429). With a key, the rate limit is 1 request/second. +- **search_openalex**: Search scholarly works via OpenAlex (broader/faster-moving coverage, citation counts, venues). **Citation policy:** OpenAlex metadata lags, so BibTeX is cross-checked against Semantic Scholar — only entries marked "Semantic Scholar ✓" are authoritative. Entries marked "OpenAlex only ⚠" must be verified with a web search (publisher page / arXiv) before citing; very recent papers may be missing from both indexes. ### Workflows @@ -1050,10 +1128,11 @@ You have MCP tools to interact with Overleaf. Use them proactively. 5. To check visual output: use \`read_compiled_pdf\`, then Read the returned path with \`pages: "1-3"\` #### Bibliography Workflow -1. Use \`search_citation\` with a topic or paper title to find references -2. Copy the BibTeX entry into the \`.bib\` file -3. Use \`\\cite{key}\` in the \`.tex\` file -4. Compile to verify the citation renders correctly +1. Use \`search_citation\` (Semantic Scholar) or \`search_openalex\` (broader coverage) to find references +2. If the entry is marked "OpenAlex only ⚠", verify it with a web search before using it +3. Copy the BibTeX entry into the \`.bib\` file +4. Use \`\\cite{key}\` in the \`.tex\` file +5. Compile to verify the citation renders correctly ## Workspace @@ -1095,7 +1174,8 @@ The tools above come from LatteX's MCP server (standard stdio MCP — works with 'mcp__lattex__get_compile_warnings', 'mcp__lattex__get_compile_log', 'mcp__lattex__read_compiled_pdf', - 'mcp__lattex__search_citation' + 'mcp__lattex__search_citation', + 'mcp__lattex__search_openalex' ] } }, null, 2)) @@ -2026,6 +2106,10 @@ ipcMain.handle('shell:openExternal', async (_e, url: string) => { await shell.openExternal(url) }) +ipcMain.handle('shell:openPath', async (_e, targetPath: string) => { + return shell.openPath(targetPath) +}) + ipcMain.handle('shell:showInFinder', async (_e, path: string) => { shell.showItemInFolder(path) }) -- cgit v1.2.3