papilot 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/README.zh-CN.md +14 -2
- package/bin/doctor.mjs +239 -0
- package/bin/papilot.mjs +95 -86
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/index-CqhlgnOu.css +1 -0
- package/dist/assets/index-Czf1eCno.js +185 -0
- package/dist/assets/math-DEcVZfaU.css +1 -0
- package/dist/assets/math-Ebyrobf9.js +263 -0
- package/dist/assets/{pdf-view-DJo0X38w.js → pdf-view-BhJZr4qw.js} +18 -18
- package/dist/index.html +11 -2
- package/examples/papilot-paper/preamble.tex +17 -11
- package/package.json +12 -2
- package/scripts/service.ts +8 -2
- package/skills/check-math.md +12 -0
- package/skills/consistency.md +11 -0
- package/skills/latex-warnings.md +12 -0
- package/skills/polish.md +13 -0
- package/skills/proofread.md +12 -0
- package/skills/reviewer-response.md +11 -0
- package/skills/shorten.md +12 -0
- package/src/conversation.ts +37 -0
- package/src/notes.ts +5 -0
- package/src/server/agents.ts +4 -3
- package/src/server/main.ts +19 -5
- package/src/server/review.ts +1 -1
- package/src/server/service-environment.ts +22 -0
- package/src/server/skills.ts +57 -0
- package/src/server/workspace.ts +41 -18
- package/src/shared.ts +10 -2
- package/vendor/latex-review/dist/web/assets/index-Db7pq4Cj.css +1 -0
- package/vendor/latex-review/dist/web/assets/index-DqkrTw0t.js +19 -0
- package/vendor/latex-review/dist/web/index.html +2 -2
- package/vendor/latex-review/src/client.ts +10 -3
- package/vendor/latex-review/src/core/build.ts +53 -7
- package/vendor/latex-review/src/core/outline.ts +6 -3
- package/vendor/latex-review/src/core/publish.ts +8 -6
- package/vendor/latex-review/src/runtime.ts +14 -0
- package/vendor/latex-review/src/server.ts +2 -1
- package/vendor/latex-review/src/types.ts +16 -1
- package/vendor/latex-review/src/web/ChangeMap.tsx +21 -9
- package/vendor/latex-review/src/web/PdfStrip.tsx +5 -1
- package/vendor/latex-review/src/web/i18n.ts +48 -0
- package/vendor/latex-review/src/web/model.ts +23 -4
- package/vendor/latex-review/src/web/overview.tsx +25 -20
- package/vendor/latex-review/src/web/review.tsx +387 -143
- package/vendor/latex-review/src/web/style.css +106 -0
- package/vendor/latex-review/src/web/zh-CN.ts +186 -0
- package/dist/assets/index-BhQFCMTx.js +0 -131
- package/dist/assets/index-CAV3-87U.css +0 -1
- package/vendor/latex-review/dist/web/assets/index-DyzYnIGk.css +0 -1
- package/vendor/latex-review/dist/web/assets/index-HxBK-dls.js +0 -19
package/dist/index.html
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"/><meta name="theme-color" content="#ffffff"/><meta name="description" content="Revise LaTeX papers with an agent and review the PDF changes."/><link rel="icon" type="image/png" href="/favicon.png"/><link rel="apple-touch-icon" href="/apple-touch-icon.png"/><title>Papilot</title
|
|
3
|
-
|
|
2
|
+
<html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"/><meta name="theme-color" content="#ffffff"/><meta name="description" content="Revise LaTeX papers with an agent and review the PDF changes."/><link rel="icon" type="image/png" href="/favicon.png"/><link rel="apple-touch-icon" href="/apple-touch-icon.png"/><title>Papilot</title><script>
|
|
3
|
+
// Resolve before the first paint, even while the application bundle is loading.
|
|
4
|
+
(() => {
|
|
5
|
+
let theme;
|
|
6
|
+
try { theme = localStorage.getItem('papilot:theme'); } catch {}
|
|
7
|
+
const dark = theme === 'dark' || (theme !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches);
|
|
8
|
+
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
|
|
9
|
+
document.querySelector('meta[name="theme-color"]').content = dark ? '#26272b' : '#ffffff';
|
|
10
|
+
})();
|
|
11
|
+
</script><style>html{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;background:#fff}html[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;background:#26272b}</style> <script type="module" crossorigin src="/assets/index-Czf1eCno.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CqhlgnOu.css">
|
|
4
13
|
</head><body><div id="root"></div></body></html>
|
|
@@ -18,17 +18,23 @@
|
|
|
18
18
|
\usepackage[colorlinks=true,linkcolor=blue!50!black,citecolor=blue!50!black,urlcolor=blue!50!black]{hyperref}
|
|
19
19
|
\usepackage[nameinlink]{cleveref}
|
|
20
20
|
|
|
21
|
-
%
|
|
22
|
-
%
|
|
23
|
-
|
|
24
|
-
\
|
|
25
|
-
\
|
|
26
|
-
\
|
|
27
|
-
\
|
|
28
|
-
\declaretheorem[style=
|
|
29
|
-
\declaretheorem[style
|
|
30
|
-
\
|
|
31
|
-
\
|
|
21
|
+
% Every kind of statement shares the theorem counter, and cleveref still names
|
|
22
|
+
% each kind correctly. LaTeX from June 2026 does this itself and clashes with
|
|
23
|
+
% thmtools, which older LaTeX needs for it.
|
|
24
|
+
\IfFormatAtLeastTF{2026-06-01}
|
|
25
|
+
{\newtheorem{theorem}{Theorem}[section]
|
|
26
|
+
\NewDocumentCommand\statement{mmm}{\theoremstyle{#1}\newtheorem{#2}[theorem]{#3}}}
|
|
27
|
+
{\usepackage{thmtools}
|
|
28
|
+
\declaretheorem[style=plain, numberwithin=section]{theorem}
|
|
29
|
+
\NewDocumentCommand\statement{mmm}{\declaretheorem[style=#1, sibling=theorem, name=#3]{#2}}}
|
|
30
|
+
\statement{plain}{proposition}{Proposition}
|
|
31
|
+
\statement{plain}{lemma}{Lemma}
|
|
32
|
+
\statement{plain}{corollary}{Corollary}
|
|
33
|
+
\statement{definition}{definition}{Definition}
|
|
34
|
+
\statement{definition}{example}{Example}
|
|
35
|
+
\statement{definition}{assumption}{Assumption}
|
|
36
|
+
\statement{remark}{remark}{Remark}
|
|
37
|
+
\crefname{assumption}{assumption}{assumptions}
|
|
32
38
|
\numberwithin{equation}{section}
|
|
33
39
|
|
|
34
40
|
\newcommand{\R}{\mathbb{R}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "papilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A Cursor-like workspace for your papers: annotate and review PDFs directly without ever touching LaTeX.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"latex",
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"src/server",
|
|
36
36
|
"src/shared.ts",
|
|
37
37
|
"src/notes.ts",
|
|
38
|
+
"src/conversation.ts",
|
|
39
|
+
"skills",
|
|
38
40
|
"dist",
|
|
39
41
|
"scripts/demo.ts",
|
|
40
42
|
"scripts/service.ts",
|
|
@@ -56,9 +58,12 @@
|
|
|
56
58
|
"test": "tsx --test tests/*.test.ts",
|
|
57
59
|
"setup:review": "npm --prefix vendor/latex-review ci && npm --prefix vendor/latex-review run build",
|
|
58
60
|
"demo": "tsx scripts/demo.ts",
|
|
61
|
+
"doctor": "node bin/papilot.mjs doctor",
|
|
59
62
|
"service:install": "tsx scripts/service.ts install",
|
|
60
63
|
"service:stop": "tsx scripts/service.ts stop",
|
|
61
|
-
"test:ui": "npm run build && node scripts/check-ui.mjs",
|
|
64
|
+
"test:ui": "npm run build && node scripts/check-ui.mjs && node scripts/check-review-notes.mjs && node --import tsx scripts/check-review-flow.mjs && node scripts/check-skills.mjs",
|
|
65
|
+
"site": "node scripts/build-site.mjs",
|
|
66
|
+
"site:video": "node scripts/build-site.mjs --video",
|
|
62
67
|
"prepack": "npm run build && npm run setup:review"
|
|
63
68
|
},
|
|
64
69
|
"dependencies": {
|
|
@@ -78,9 +83,14 @@
|
|
|
78
83
|
"@types/react": "^19.3.0",
|
|
79
84
|
"@types/react-dom": "^19.3.0",
|
|
80
85
|
"@vitejs/plugin-react": "^6.1.2",
|
|
86
|
+
"katex": "^0.16.47",
|
|
81
87
|
"pdfjs-dist": "^6.4.299",
|
|
82
88
|
"react": "^19.3.1",
|
|
83
89
|
"react-dom": "^19.3.1",
|
|
90
|
+
"react-markdown": "^10.1.0",
|
|
91
|
+
"rehype-katex": "^7.0.1",
|
|
92
|
+
"remark-gfm": "^4.0.1",
|
|
93
|
+
"remark-math": "^6.0.0",
|
|
84
94
|
"typescript": "^7.0.2",
|
|
85
95
|
"vite": "^8.3.5"
|
|
86
96
|
}
|
package/scripts/service.ts
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { ROOT, DATA } from '../src/server/paths.ts';
|
|
6
|
+
import { serviceEnvironment } from '../src/server/service-environment.ts';
|
|
6
7
|
|
|
7
8
|
if (process.platform !== 'darwin') throw new Error('This service installer is for macOS. On other systems run npm start with your process manager.');
|
|
8
9
|
const label = 'local.papilot';
|
|
@@ -14,8 +15,13 @@ if (process.argv[2] === 'stop') {
|
|
|
14
15
|
const xml = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
15
16
|
fs.mkdirSync(DATA, { recursive: true, mode: 0o700 });
|
|
16
17
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
let previous: NodeJS.ProcessEnv = {};
|
|
19
|
+
if (fs.existsSync(file)) {
|
|
20
|
+
try {
|
|
21
|
+
previous = JSON.parse(execFileSync('/usr/bin/plutil', ['-extract', 'EnvironmentVariables', 'json', '-o', '-', file], { encoding: 'utf8' }));
|
|
22
|
+
} catch { /* An older or incomplete plist may not contain environment settings. */ }
|
|
23
|
+
}
|
|
24
|
+
const env = serviceEnvironment(ROOT, os.homedir(), process.execPath, process.env, previous);
|
|
19
25
|
const args = ['/usr/bin/caffeinate', '-i', process.execPath, '--import', 'tsx', path.join(ROOT, 'src/server/main.ts')];
|
|
20
26
|
const plist = `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict>
|
|
21
27
|
<key>Label</key><string>${label}</string><key>ProgramArguments</key><array>${args.map(a => `<string>${xml(a)}</string>`).join('')}</array>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Check math
|
|
3
|
+
description: Find errors in notation, equations and proofs
|
|
4
|
+
hint: Which part? e.g. "Section 3 and Appendix A". Leave empty for the whole paper
|
|
5
|
+
order: 4
|
|
6
|
+
---
|
|
7
|
+
Check the mathematics for errors and inconsistencies.
|
|
8
|
+
|
|
9
|
+
- Every symbol is defined before it is used and means one thing throughout. Indices, signs, dimensions and quantifiers agree. Theorem statements match their proofs and the places that cite them. References to equations, theorems and figures point to the right ones.
|
|
10
|
+
- Fix clear slips directly: a typo in a formula, an inconsistent symbol, a wrong reference.
|
|
11
|
+
- Do not change what a result says. If you suspect a substantive error, leave the text and explain the problem in your summary, with its location and your reasoning.
|
|
12
|
+
- Follow the paper's own macros and environments.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Consistency
|
|
3
|
+
description: Unify terms, notation, spelling and references
|
|
4
|
+
hint: Which part? Leave empty for the whole paper
|
|
5
|
+
order: 5
|
|
6
|
+
---
|
|
7
|
+
Make the paper consistent with itself.
|
|
8
|
+
|
|
9
|
+
- Terminology, capitalization and hyphenation; American or British spelling; acronyms defined once at first use and used after that; notation and macros; reference style (for example `\cref` or `Section~\ref`); numbers, units, list and caption punctuation.
|
|
10
|
+
- For each difference, use the variant the paper uses most, and name your choices in the summary.
|
|
11
|
+
- Do not change meaning, and leave quoted material and bibliography entries as they are.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Fix LaTeX warnings
|
|
3
|
+
description: Resolve undefined references, overfull boxes and other build warnings
|
|
4
|
+
hint: Anything to leave alone? Leave empty to fix every warning you can
|
|
5
|
+
order: 6
|
|
6
|
+
---
|
|
7
|
+
Fix the problems LaTeX reports when it builds the paper.
|
|
8
|
+
|
|
9
|
+
- Compile in your worktree (for example with `latexmk`) and read the log. If you cannot run commands, look for the same problems in the source instead.
|
|
10
|
+
- Fix undefined or duplicate labels, references and citations; overfull boxes, by rewording slightly or allowing a break where the line overflows; and package warnings that point to real mistakes.
|
|
11
|
+
- Do not silence warnings globally: no document-wide `\sloppy`, badness or tolerance changes, and no new packages, unless the request asks for them.
|
|
12
|
+
- In your summary, list the warnings you fixed and any you left, with the reason.
|
package/skills/polish.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Polish
|
|
3
|
+
description: Improve clarity and flow, keeping the meaning
|
|
4
|
+
hint: Which part? e.g. "the introduction". Leave empty for the whole paper
|
|
5
|
+
order: 1
|
|
6
|
+
---
|
|
7
|
+
Improve the clarity, concision and flow of the prose, sentence by sentence and paragraph by paragraph.
|
|
8
|
+
|
|
9
|
+
- Keep every claim, number, citation, label, definition and technical term. Do not add claims, results or citations.
|
|
10
|
+
- Prefer concrete subjects and active verbs. Cut filler such as "it is worth noting that" or "in order to".
|
|
11
|
+
- Keep the author's voice and the paper's terminology; never replace a defined term with a synonym.
|
|
12
|
+
- Make the smallest edit that fixes a real problem, and leave sentences that already read well.
|
|
13
|
+
- Do not touch mathematics, code, labels or the document's structure unless the request asks for it.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Proofread
|
|
3
|
+
description: Fix typos, grammar and punctuation without rewording
|
|
4
|
+
hint: Which part? Leave empty for the whole paper
|
|
5
|
+
order: 2
|
|
6
|
+
---
|
|
7
|
+
Proofread the prose for typos, spelling, grammar, punctuation, agreement, articles and repeated words.
|
|
8
|
+
|
|
9
|
+
- Change only what is wrong. Do not rephrase correct sentences, reorder content, or change terminology, notation, citations or labels.
|
|
10
|
+
- Keep the paper's spelling convention (American or British) as the majority of the text uses it.
|
|
11
|
+
- Leave mathematics, code, citation keys, labels and URLs as they are.
|
|
12
|
+
- In your summary, list anything you noticed but left because it needs the author's judgment.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Address reviewers
|
|
3
|
+
description: Revise for reviewer comments and draft a point-by-point reply
|
|
4
|
+
hint: Paste the reviewer comments
|
|
5
|
+
order: 7
|
|
6
|
+
---
|
|
7
|
+
Revise the paper in response to the reviewer comments in the request.
|
|
8
|
+
|
|
9
|
+
- For each comment, make a targeted change where one is warranted. Keep the rest of the text as it is.
|
|
10
|
+
- Do not invent experiments, results or citations. When a comment needs new work, say so in the reply instead of editing.
|
|
11
|
+
- End with a point-by-point reply in your summary: quote each comment briefly, then say what changed and where (section, and the new wording if it is short), or explain politely why nothing changed.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Shorten
|
|
3
|
+
description: Cut length without losing content
|
|
4
|
+
hint: What and by how much? e.g. "Section 4 by a third" or "fit in 8 pages"
|
|
5
|
+
order: 3
|
|
6
|
+
---
|
|
7
|
+
Make the text shorter while keeping its content.
|
|
8
|
+
|
|
9
|
+
- Cut redundancy, repeated motivation, hedging and wordy phrasing first; merge sentences that say the same thing.
|
|
10
|
+
- Keep every result, definition, theorem, figure, table and citation unless the request says otherwise.
|
|
11
|
+
- With a page target, compile in your worktree to check the page count if you can run commands.
|
|
12
|
+
- In your summary, say roughly how much you cut, what kind of text went, and what you considered cutting but kept.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { FeedbackSummary } from './shared.ts';
|
|
2
|
+
|
|
3
|
+
// What the agent receives when the author sends review feedback without a
|
|
4
|
+
// message of their own. The chat shows the feedback, never this prompt.
|
|
5
|
+
export const reviewInstructions = 'Continue this task using the decisions and comments from this review round. Preserve accepted changes and do not reintroduce rejected edits. Address any remaining revision requests; if no revision is needed, summarize the result.';
|
|
6
|
+
// Earlier versions stored the prompt itself as the author's message.
|
|
7
|
+
export const isFeedbackPrompt = (text: string) => text === reviewInstructions || text === 'Address the comments from this review round. Preserve accepted changes and do not reintroduce rejected edits.';
|
|
8
|
+
|
|
9
|
+
// A skill is a named set of instructions; the author's own words, if any, say where and how.
|
|
10
|
+
export function skillPrompt(skill: { name: string; body: string; folder?: string }, request: string) {
|
|
11
|
+
return [
|
|
12
|
+
`Use the "${skill.name}" skill below for this request.${skill.folder ? ` Files the skill mentions are in ${skill.folder}.` : ''}`,
|
|
13
|
+
`<skill name="${skill.name}">\n${skill.body.trim()}\n</skill>`,
|
|
14
|
+
request.trim() ? `Request: ${request.trim()}` : 'No further details were given: apply the skill to the whole paper.',
|
|
15
|
+
].join('\n\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface FeedbackItem { kind?: string; round?: number | null; text?: string; changes?: (string | { file?: string; lines?: [number, number] })[] }
|
|
19
|
+
// The review engine's feedback (see vendor/latex-review FeedbackItem) as the chat shows it.
|
|
20
|
+
export function summarizeFeedback(feedback: { feedback?: FeedbackItem[]; build_error?: unknown }): FeedbackSummary | undefined {
|
|
21
|
+
const items = feedback.feedback || [];
|
|
22
|
+
if (!items.length && !feedback.build_error) return undefined;
|
|
23
|
+
const summary: FeedbackSummary = { round: null, accepted: 0, rejected: 0, undone: 0, comments: [], ...(feedback.build_error ? { buildError: true } : {}) };
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
if (item.round) summary.round = Math.max(summary.round || 0, item.round);
|
|
26
|
+
const changes = item.changes || [];
|
|
27
|
+
if (item.kind === 'accept') summary.accepted += changes.length || 1;
|
|
28
|
+
else if (item.kind === 'reject') summary.rejected += changes.length || 1;
|
|
29
|
+
else if (item.kind === 'undo') summary.undone++;
|
|
30
|
+
else if ((item.kind === 'comment' || item.kind === 'note') && item.text?.trim()) {
|
|
31
|
+
const first = changes.find(change => typeof change === 'object' && change.file) as { file: string; lines?: [number, number] } | undefined;
|
|
32
|
+
const where = first ? `${first.file}${first.lines ? `:${first.lines[0]}` : ''}${changes.length > 1 ? ` +${changes.length - 1}` : ''}` : item.kind === 'note' ? 'Note on the review' : `Round ${item.round ?? ''}`.trim();
|
|
33
|
+
summary.comments.push({ where, text: item.text.trim().slice(0, 2000) });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return summary;
|
|
37
|
+
}
|
package/src/notes.ts
CHANGED
|
@@ -7,6 +7,11 @@ const QUOTE = ' PDF text: "', REGION = ' Marked region: a figure, equation o
|
|
|
7
7
|
|
|
8
8
|
export interface NoteSummary { where: string; quote: string; comment: string }
|
|
9
9
|
|
|
10
|
+
export function reviewNotesInstructions(notes: NoteSummary[]) {
|
|
11
|
+
const items = notes.map((note, index) => `${index + 1}. ${note.where}\n${note.quote ? `${QUOTE}${note.quote}"` : REGION}${NOTE}${note.comment.trim().replace(/\n/g, '\n ')}`);
|
|
12
|
+
return `${PREAMBLE} These notes refer to the displayed review round and its Before, After or Unchanged text. Region coordinates are percentages of the displayed PDF strip (left, top, right, bottom); any quoted text for a region is nearby context. Read the current source before editing. Preserve accepted changes and do not reintroduce rejected edits unless a note explicitly asks for it.\n\n${items.join('\n\n')}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export function notesInstructions(notes: Annotation[]) {
|
|
11
16
|
const items = notes.map((note, index) => {
|
|
12
17
|
const where = [`Page ${note.page + 1}`, note.source ? `${note.source.file} ${note.source.lines[0] === note.source.lines[1] ? `line ${note.source.lines[0]}` : `lines ${note.source.lines[0]}–${note.source.lines[1]}`}` : ''].filter(Boolean).join(' · ');
|
package/src/server/agents.ts
CHANGED
|
@@ -139,10 +139,11 @@ export class ClaudeAgent extends Adapter {
|
|
|
139
139
|
this.ready = true;
|
|
140
140
|
} finally { finish(); probe.close(); }
|
|
141
141
|
}
|
|
142
|
-
// Claude tasks need an API key (
|
|
143
|
-
// Anthropic
|
|
142
|
+
// Claude tasks need an API key, an ANTHROPIC_AUTH_TOKEN (for a gateway or an
|
|
143
|
+
// Anthropic-compatible ANTHROPIC_BASE_URL), or a cloud provider's own
|
|
144
|
+
// credentials: Anthropic does not accept a Claude subscription login through the SDK.
|
|
144
145
|
protected account() {
|
|
145
|
-
const account = this.info.account, keyed = Boolean(account?.apiKeySource && !['none', 'oauth'].includes(account.apiKeySource));
|
|
146
|
+
const account = this.info.account, keyed = Boolean(account?.apiKeySource && !['none', 'oauth'].includes(account.apiKeySource)) || account?.tokenSource === 'ANTHROPIC_AUTH_TOKEN';
|
|
146
147
|
const external = Boolean(account?.apiProvider && account.apiProvider !== 'firstParty');
|
|
147
148
|
if (keyed || external) return { account: { email: account?.email, type: external ? account!.apiProvider : 'API key' }, requiresOpenaiAuth: false };
|
|
148
149
|
return { account: null, requiresOpenaiAuth: true, reason: CLAUDE_KEY };
|
package/src/server/main.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { reviewClient, allowedReviewRoute, rewriteReviewHtml, rewriteReviewJson,
|
|
|
11
11
|
import { authorize } from './security.ts';
|
|
12
12
|
import { fullPaperPdf } from './pdf.ts';
|
|
13
13
|
import { Manuscripts, annotationTask, gitCommit, gitDiff, gitRemote, gitStatus } from './manuscript.ts';
|
|
14
|
+
import { findSkill, listedSkill, loadSkills } from './skills.ts';
|
|
14
15
|
import type { AgentId } from '../shared.ts';
|
|
15
16
|
|
|
16
17
|
const store = new Store(path.join(DATA, 'papilot.sqlite'));
|
|
@@ -46,9 +47,14 @@ const effort = (value: unknown) => typeof value === 'string' && /^[a-z]{1,16}$/.
|
|
|
46
47
|
const string = (value: unknown, name: string, max = 20000) => { if (typeof value !== 'string' || !value.trim() || value.length > max) throw new Error(`${name} is required and must be no longer than ${max} characters.`); return value.trim(); };
|
|
47
48
|
app.post('/api/projects', async request => { const b = request.body as any; return workspace.addProject(string(b.path, 'Repository path', 4000), b.name ? string(b.name, 'Paper name', 120) : undefined); });
|
|
48
49
|
app.post<{ Params: { id: string } }>('/api/projects/:id/rename', request => workspace.renameProject(request.params.id, string((request.body as any).name, 'Paper name', 120)));
|
|
50
|
+
// A skill's name: what the author typed after "/".
|
|
51
|
+
const skillName = (value: unknown) => typeof value === 'string' && /^[a-z0-9][a-z0-9-]{0,47}$/.test(value) ? value : undefined;
|
|
52
|
+
// Text the author may leave empty when a skill or review feedback says enough.
|
|
53
|
+
const optional = (value: unknown, name: string) => value === undefined || value === '' ? '' : string(value, name);
|
|
54
|
+
app.get<{ Params: { id: string } }>('/api/projects/:id/skills', request => ({ skills: loadSkills(store.project(request.params.id).path).map(listedSkill) }));
|
|
49
55
|
app.post('/api/tasks', async request => {
|
|
50
|
-
const b = request.body as any;
|
|
51
|
-
return workspace.createTask({ id: string(b.id, 'Task ID', 64), projectId: string(b.projectId, 'Paper ID', 64), title: string(b.title, 'Task title', 120), instructions: string(b.instructions, 'Instructions'), agent: agent(b.agent), model: typeof b.model === 'string' ? b.model : undefined, effort: effort(b.effort) });
|
|
56
|
+
const b = request.body as any, skill = skillName(b.skill);
|
|
57
|
+
return workspace.createTask({ id: string(b.id, 'Task ID', 64), projectId: string(b.projectId, 'Paper ID', 64), title: string(b.title, 'Task title', 120), instructions: skill ? optional(b.instructions, 'Instructions') : string(b.instructions, 'Instructions'), skill, agent: agent(b.agent), model: typeof b.model === 'string' ? b.model : undefined, effort: effort(b.effort) });
|
|
52
58
|
});
|
|
53
59
|
app.get<{ Params: { id: string } }>('/api/tasks/:id/items', request => { store.task(request.params.id); return { items: store.items(request.params.id) }; });
|
|
54
60
|
app.get<{ Params: { id: string }; Querystring: { round?: string; version?: string; download?: string } }>('/api/tasks/:id/pdf', async (request, reply) => {
|
|
@@ -61,8 +67,10 @@ app.get<{ Params: { id: string }; Querystring: { round?: string; version?: strin
|
|
|
61
67
|
return reply.type('application/pdf').header('Content-Disposition', `${request.query.download === '1' ? 'attachment' : 'inline'}; filename="${filename}"`).sendFile(path.basename(pdf), path.dirname(pdf), { cacheControl: false });
|
|
62
68
|
});
|
|
63
69
|
app.post<{ Params: { id: string } }>('/api/tasks/:id/messages', async request => {
|
|
64
|
-
const b = request.body as any;
|
|
65
|
-
|
|
70
|
+
const b = request.body as any, task = store.task(request.params.id), feedback = b.feedback === true, name = skillName(b.skill);
|
|
71
|
+
const skill = name ? findSkill(store.project(task.projectId).path, name) : undefined;
|
|
72
|
+
const text = feedback || skill ? optional(b.text, 'Revision instructions') : string(b.text, 'Revision instructions');
|
|
73
|
+
return workspace.enqueue(task.id, text, string(b.requestKey, 'Request ID', 100), true, { skill, feedback });
|
|
66
74
|
});
|
|
67
75
|
app.post<{ Params: { id: string } }>('/api/tasks/:id/rename', request => workspace.renameTask(request.params.id, string((request.body as any).title, 'Task title', 120)));
|
|
68
76
|
app.post<{ Params: { id: string } }>('/api/tasks/:id/interrupt', request => workspace.interrupt(request.params.id).then(() => ({ ok: true })));
|
|
@@ -193,7 +201,13 @@ await app.listen({ host: '127.0.0.1', port: PORT });
|
|
|
193
201
|
store.recover();
|
|
194
202
|
console.log(`Papilot is running at http://127.0.0.1:${PORT}${PUBLIC_ORIGIN ? `\nRemote: ${PUBLIC_ORIGIN}` : ''}`);
|
|
195
203
|
void workspace.connect();
|
|
196
|
-
|
|
204
|
+
// Recompute cached feedback counts, including decisions from before this restart.
|
|
205
|
+
for (const task of store.tasks()) {
|
|
206
|
+
if (task.status === 'building') {
|
|
207
|
+
store.updateTask(task.id, { status: 'interrupted', error: 'The server restarted during the PDF build. Check the review and rebuild it if needed.' });
|
|
208
|
+
void workspace.refresh(task.id);
|
|
209
|
+
} else if (task.review?.round && task.status !== 'deleting') void workspace.refresh(task.id);
|
|
210
|
+
}
|
|
197
211
|
let shuttingDown = false;
|
|
198
212
|
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => {
|
|
199
213
|
if (shuttingDown) return; shuttingDown = true; workspace.close();
|
package/src/server/review.ts
CHANGED
|
@@ -34,7 +34,7 @@ export function annotateReviewState(value: unknown, tasks: readonly Task[]): unk
|
|
|
34
34
|
else if (task.status === 'building') decisionLock = 'Wait for the PDF build before accepting, rejecting, or undoing changes. You can still add comments.';
|
|
35
35
|
else if (['running', 'waiting_input', 'queued'].includes(task.status)) decisionLock = 'Pause this task in Papilot before accepting, rejecting, or undoing changes. You can still add comments.';
|
|
36
36
|
else if (task.pendingReview) decisionLock = 'Wait for the current review action before accepting, rejecting, or undoing changes. You can still add comments.';
|
|
37
|
-
return { ...state, decisionLock, commentLock };
|
|
37
|
+
return { ...state, decisionLock, commentLock, ...(task ? { workspace: { status: task.status, unsent: task.review?.unsent ?? 0, comments: task.review?.comments ?? 0, accepted: task.review?.files?.length ?? 0 } } : {}) };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function allowedReviewRoute(route: string, method: string): boolean {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
/** launchd has no login shell. Keep the tool choices made by the installer. */
|
|
4
|
+
export function serviceEnvironment(
|
|
5
|
+
root: string, home: string, executable: string,
|
|
6
|
+
inherited: NodeJS.ProcessEnv, previous: NodeJS.ProcessEnv = {},
|
|
7
|
+
): Record<string, string> {
|
|
8
|
+
// An explicit empty value clears a previously saved override.
|
|
9
|
+
const toolPath = inherited.PAPILOT_SERVICE_PATH ?? previous.PAPILOT_SERVICE_PATH ?? '';
|
|
10
|
+
const directories = [
|
|
11
|
+
...toolPath.split(path.delimiter),
|
|
12
|
+
path.dirname(executable), path.join(root, 'node_modules/.bin'),
|
|
13
|
+
...(inherited.PATH || '').split(path.delimiter),
|
|
14
|
+
'/Library/TeX/texbin', '/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin',
|
|
15
|
+
].filter(directory => path.isAbsolute(directory));
|
|
16
|
+
const env: Record<string, string> = {
|
|
17
|
+
PATH: [...new Set(directories)].join(path.delimiter), HOME: home, NODE_ENV: 'production',
|
|
18
|
+
};
|
|
19
|
+
for (const [key, value] of Object.entries(inherited)) if (key.startsWith('PAPILOT_') && value) env[key] = value;
|
|
20
|
+
if (toolPath) env.PAPILOT_SERVICE_PATH = toolPath;
|
|
21
|
+
return env;
|
|
22
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DATA, ROOT } from './paths.ts';
|
|
4
|
+
import type { Skill } from '../shared.ts';
|
|
5
|
+
|
|
6
|
+
// Skills are Markdown instructions an author runs with "/" in the composer.
|
|
7
|
+
// Papilot ships some in skills/; the data folder's skills/ adds the author's
|
|
8
|
+
// own for every paper, and a paper's .papilot/skills/ its own. A later source
|
|
9
|
+
// replaces an earlier skill with the same name. Each skill is <name>.md, or a
|
|
10
|
+
// folder with a SKILL.md (the Agent Skills layout) whose other files the
|
|
11
|
+
// agent may read. Front matter gives its name, description, composer hint
|
|
12
|
+
// and order.
|
|
13
|
+
export interface SkillFile extends Skill { body: string; folder?: string; order: number }
|
|
14
|
+
const NAME = /^[a-z0-9][a-z0-9-]{0,47}$/;
|
|
15
|
+
|
|
16
|
+
export function parseSkill(text: string, id: string, source: Skill['source'], folder?: string): SkillFile | null {
|
|
17
|
+
text = text.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
|
|
18
|
+
const header = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(text), fields: Record<string, string> = {};
|
|
19
|
+
for (const line of header?.[1].split('\n') || []) {
|
|
20
|
+
const field = /^([A-Za-z-]+):\s*(.*)$/.exec(line);
|
|
21
|
+
if (field) fields[field[1].toLowerCase()] = field[2].trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
22
|
+
}
|
|
23
|
+
const body = text.slice(header ? header[0].length : 0).trim().slice(0, 20000);
|
|
24
|
+
if (!body) return null;
|
|
25
|
+
return { id, name: (fields.name || id).slice(0, 60), description: (fields.description || '').slice(0, 300), hint: (fields.hint || '').slice(0, 200), source, body, ...(folder ? { folder } : {}), order: fields.order && Number.isFinite(Number(fields.order)) ? Number(fields.order) : 1000 };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function read(dir: string, source: Skill['source']): SkillFile[] {
|
|
29
|
+
let entries: fs.Dirent[];
|
|
30
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
|
31
|
+
const found: SkillFile[] = [];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const id = entry.name.replace(/\.md$/i, '').toLowerCase();
|
|
34
|
+
if (!NAME.test(id)) continue;
|
|
35
|
+
try {
|
|
36
|
+
const skill = entry.isDirectory() ? parseSkill(fs.readFileSync(path.join(dir, entry.name, 'SKILL.md'), 'utf8'), id, source, path.join(dir, entry.name))
|
|
37
|
+
: entry.isFile() && /\.md$/i.test(entry.name) ? parseSkill(fs.readFileSync(path.join(dir, entry.name), 'utf8'), id, source) : null;
|
|
38
|
+
if (skill) found.push(skill);
|
|
39
|
+
} catch { /* A folder without a readable SKILL.md is not a skill. */ }
|
|
40
|
+
}
|
|
41
|
+
return found;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadSkills(paper?: string, roots = { builtin: path.join(ROOT, 'skills'), user: path.join(DATA, 'skills') }): SkillFile[] {
|
|
45
|
+
const skills = new Map<string, SkillFile>();
|
|
46
|
+
const sources: [string, Skill['source']][] = [[roots.builtin, 'builtin'], [roots.user, 'user'], ...(paper ? [[path.join(paper, '.papilot', 'skills'), 'paper'] as [string, Skill['source']]] : [])];
|
|
47
|
+
for (const [dir, source] of sources) for (const skill of read(dir, source)) skills.set(skill.id, skill);
|
|
48
|
+
return [...skills.values()].sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function findSkill(paper: string, id: string) {
|
|
52
|
+
const skill = loadSkills(paper).find(item => item.id === id);
|
|
53
|
+
if (!skill) throw new Error(`The skill "${id}" is not available for this paper.`);
|
|
54
|
+
return skill;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const listedSkill = ({ id, name, description, hint, source }: SkillFile): Skill => ({ id, name, description, hint, source });
|
package/src/server/workspace.ts
CHANGED
|
@@ -9,7 +9,9 @@ import { Codex, RpcResponseError } from './codex.ts';
|
|
|
9
9
|
import { AGENT_NAMES, AntigravityAgent, ClaudeAgent, type AgentRuntime } from './agents.ts';
|
|
10
10
|
import { reviewClient, taskRoute, confirmedReviewResponse, type ReviewClient } from './review.ts';
|
|
11
11
|
import { VERSION } from './paths.ts';
|
|
12
|
-
import
|
|
12
|
+
import { findSkill, type SkillFile } from './skills.ts';
|
|
13
|
+
import { reviewInstructions, skillPrompt, summarizeFeedback } from '../conversation.ts';
|
|
14
|
+
import type { Task, Run, TimelineItem, PendingRequest, RuntimeStatus, Snapshot, PendingReview, AgentModel, Usage, AgentId, MessageMeta, FeedbackSummary } from '../shared.ts';
|
|
13
15
|
|
|
14
16
|
const execute = promisify(execFile);
|
|
15
17
|
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -122,25 +124,30 @@ export class Workspace extends EventEmitter {
|
|
|
122
124
|
const project = { ...this.store.project(projectId), name };
|
|
123
125
|
this.store.saveProject(project); return project;
|
|
124
126
|
}
|
|
125
|
-
|
|
127
|
+
// A task can start from a skill; its instructions are then the author's own words, possibly none.
|
|
128
|
+
async createTask(input: { projectId: string; title: string; instructions: string; skill?: string; agent?: AgentId; model?: string; effort?: string; id: string }) {
|
|
126
129
|
if (!/^[\w-]{16,64}$/.test(input.id)) throw new Error('Invalid task ID.');
|
|
127
130
|
if (this.store.taskDeleted(input.id)) throw new Error('This task was deleted. Create a new task instead.');
|
|
128
131
|
const existing = this.store.tasks().find(t => t.id === input.id); if (existing) return existing;
|
|
129
132
|
if (this.preparing.has(input.id)) throw new Error('This task is being created. Refresh in a moment.');
|
|
130
133
|
this.preparing.add(input.id);
|
|
131
134
|
try {
|
|
132
|
-
const project = this.store.project(input.projectId),
|
|
135
|
+
const project = this.store.project(input.projectId), skill = input.skill ? findSkill(project.path, input.skill) : undefined;
|
|
136
|
+
if (!skill && !input.instructions.trim()) throw new Error('Describe the revision, or choose a skill.');
|
|
137
|
+
const client = await this.connectReview(project.path);
|
|
133
138
|
const name = 'p-' + input.id.toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 48);
|
|
134
139
|
const info = await client.post('/api/tasks', { name, title: input.title });
|
|
135
|
-
const task: Task = { id: input.id, projectId: project.id, name, title: input.title, instructions: input.instructions,
|
|
140
|
+
const task: Task = { id: input.id, projectId: project.id, name, title: input.title, instructions: skill ? skillPrompt(skill, input.instructions) : input.instructions,
|
|
136
141
|
worktree: info.worktree, threadId: null, turnId: null, agent: input.agent || 'codex', model: input.model || null, effort: input.effort || null, status: 'idle',
|
|
137
142
|
createdAt: Date.now(), updatedAt: Date.now(), feedbackCursor: 0, error: null, review: null };
|
|
138
143
|
this.store.saveTask(task);
|
|
139
|
-
await this.enqueue(task.id, input.instructions, 'initial-' + task.id, false);
|
|
144
|
+
await this.enqueue(task.id, input.instructions, 'initial-' + task.id, false, { skill });
|
|
140
145
|
return this.store.task(task.id);
|
|
141
146
|
} finally { this.preparing.delete(input.id); }
|
|
142
147
|
}
|
|
143
|
-
|
|
148
|
+
// The author's message, a skill run with or without one, or the review
|
|
149
|
+
// feedback alone. The chat keeps what the author did; the agent gets the prompt.
|
|
150
|
+
async enqueue(taskId: string, text: string, requestKey: string, includeFeedback = true, options: { skill?: SkillFile; feedback?: boolean } = {}) {
|
|
144
151
|
const task = this.store.task(taskId);
|
|
145
152
|
if (task.status === 'deleting') throw new Error('This task is being deleted.');
|
|
146
153
|
const existing = this.store.runByKey(requestKey);
|
|
@@ -148,15 +155,18 @@ export class Workspace extends EventEmitter {
|
|
|
148
155
|
if (task.status === 'uncertain') throw new Error('Check the task status before sending more instructions.');
|
|
149
156
|
const pauseVersion = task.pauseVersion || 0;
|
|
150
157
|
if (this.store.runs().some(r => r.taskId === taskId && r.status === 'queued')) throw new Error('This task already has queued instructions. Wait or pause it first.');
|
|
151
|
-
let prompt = text.trim(), cursor = task.feedbackCursor;
|
|
158
|
+
let prompt = options.feedback ? reviewInstructions : options.skill ? skillPrompt(options.skill, text) : text.trim(), cursor = task.feedbackCursor;
|
|
159
|
+
let summary: FeedbackSummary | undefined;
|
|
152
160
|
if (includeFeedback) {
|
|
153
161
|
const client = await this.connectReview(this.store.project(task.projectId).path);
|
|
154
162
|
const feedback = await client.get(`${taskRoute(task.name)}/feedback?after=${task.feedbackCursor}`);
|
|
155
|
-
cursor = feedback.cursor;
|
|
163
|
+
cursor = feedback.cursor; summary = summarizeFeedback(feedback);
|
|
164
|
+
if (options.feedback && !summary) throw new Error('There is no new review feedback to send.');
|
|
156
165
|
if (feedback.feedback?.length || feedback.build_error) prompt += '\n\nPapilot review feedback (bound to the specified review round and source changes):\n' + JSON.stringify(feedback, null, 2);
|
|
157
166
|
if (task.error) prompt += '\n\nLatest task/review error:\n' + task.error + '\nThe latest accepted manuscript is at ' + this.store.project(task.projectId).path + '. You may read it to understand accepted changes, but must only edit your isolated task worktree. Prepare a revised proposal for another review; never write to or accept changes in the manuscript checkout.';
|
|
158
167
|
}
|
|
159
168
|
const run: Run = { id: randomUUID(), taskId, requestKey, prompt, feedbackCursor: cursor, status: 'queued', createdAt: Date.now(), finishedAt: null };
|
|
169
|
+
const meta: MessageMeta = { ...(options.skill ? { skill: { id: options.skill.id, name: options.skill.name } } : {}), ...(summary ? { feedback: summary } : {}), ...(options.feedback ? { sendsFeedback: true } : {}) };
|
|
160
170
|
const saved = this.store.transaction(() => {
|
|
161
171
|
// Feedback loading yields to other requests. Recheck identity, queue, and
|
|
162
172
|
// task status together before committing; none of the earlier reads own a lock.
|
|
@@ -172,7 +182,7 @@ export class Workspace extends EventEmitter {
|
|
|
172
182
|
if (current.status === 'uncertain') throw new Error('Check the task status before sending more instructions.');
|
|
173
183
|
this.store.saveRun(run);
|
|
174
184
|
if (!BUSY.has(current.status)) this.store.updateTask(taskId, { status: 'queued', error: null });
|
|
175
|
-
this.store.saveItem({ id: run.id, taskId, type: 'user', text: text.trim(), createdAt: run.createdAt });
|
|
185
|
+
this.store.saveItem({ id: run.id, taskId, type: 'user', text: options.feedback ? '' : text.trim(), createdAt: run.createdAt, ...(Object.keys(meta).length ? { meta } : {}) });
|
|
176
186
|
return run;
|
|
177
187
|
});
|
|
178
188
|
this.schedule();
|
|
@@ -200,7 +210,7 @@ export class Workspace extends EventEmitter {
|
|
|
200
210
|
Your task is already created. Your editable working directory is ${task.worktree}.
|
|
201
211
|
Original task: ${task.instructions}
|
|
202
212
|
Only edit this isolated task worktree. Never edit the manuscript checkout, other task worktrees, or Papilot's own source/state. Never commit, push, stash, or accept/reject/undo review decisions.
|
|
203
|
-
Papilot owns the LaTeX review lifecycle: it compiles and publishes automatically AFTER your turn finishes, and delivers review feedback in a new turn. Do not create another review task, invoke publish, or wait on feedback --wait. This application workflow replaces the standalone latex-review skill's publish-and-wait loop. End your turn after completing the requested edits, with a concise summary.
|
|
213
|
+
Papilot owns the LaTeX review lifecycle: it compiles and publishes automatically AFTER your turn finishes, and delivers review feedback in a new turn. Do not create another review task, invoke publish, or wait on feedback --wait. This application workflow replaces the standalone latex-review skill's publish-and-wait loop. End your turn after completing the requested edits, with a concise summary for the author of what you changed and why; do not describe Papilot's review workflow.
|
|
204
214
|
Preserve meaning and mathematical correctness. Read relevant project instructions. Treat reviewer comments as instructions tied to their round, file and changes. Rejected edits must not be reintroduced unless explicitly requested. If there is a conflict with accepted text, explain it and propose a revision for a new review round; never overwrite the accepted manuscript.
|
|
205
215
|
${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to you here and any attempt is refused, which ends your turn: read, search and edit files in the task worktree with your file tools only.' : 'You may run tools in the task worktree to inspect and validate your changes.'} If a decision requires the user, ask through the available user-input tool or explain it in your final response. Use English for your progress updates, questions, and final summaries unless the user explicitly requests another language. Preserve the manuscript language unless asked to translate it.`;
|
|
206
216
|
}
|
|
@@ -460,7 +470,7 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
460
470
|
if (this.closing) return;
|
|
461
471
|
const current = await client.get(`/api/jobs/${job.job}`);
|
|
462
472
|
const summary = await client.get(taskRoute(task.name));
|
|
463
|
-
this.store.updateTask(taskId, { review: { state: summary.state, round: summary.round, counts: summary.counts, job: current
|
|
473
|
+
this.store.updateTask(taskId, { review: { ...this.store.task(taskId).review, state: summary.state, round: summary.round, counts: summary.counts, job: current } });
|
|
464
474
|
if (current.status === 'done' || current.status === 'completed' || current.status === 'succeeded') break;
|
|
465
475
|
if (current.status === 'failed') throw new Error([current.error, ...(current.details || [])].filter(Boolean).join('\n') || 'PDF build failed.');
|
|
466
476
|
await delay(1500);
|
|
@@ -473,19 +483,22 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
473
483
|
try {
|
|
474
484
|
const task = this.store.task(taskId);
|
|
475
485
|
if (task.status === 'deleting') return;
|
|
476
|
-
const
|
|
486
|
+
const project = this.store.project(task.projectId), client = await this.connectReview(project.path);
|
|
477
487
|
const summary = await client.get(taskRoute(task.name));
|
|
478
|
-
const unsent = await this.
|
|
488
|
+
const { unsent, comments } = await this.unsentReviewFeedback(client, this.store.task(taskId));
|
|
489
|
+
const files = await acceptedFiles(project.path, task.name);
|
|
479
490
|
const current = this.store.task(taskId);
|
|
480
|
-
if (current.status !== 'deleting') this.store.updateTask(taskId, { review: { state: summary.state, round: summary.round, counts: summary.counts, job: current.review?.job, unsent } });
|
|
491
|
+
if (current.status !== 'deleting') this.store.updateTask(taskId, { review: { state: summary.state, round: summary.round, counts: summary.counts, job: current.review?.job, unsent, comments, files } });
|
|
481
492
|
} catch { /* The task remains visible even if its project is temporarily unavailable. */ }
|
|
482
493
|
}
|
|
483
|
-
// Review comments
|
|
484
|
-
//
|
|
485
|
-
|
|
494
|
+
// Review decisions, comments and undo updates the agent has not received yet.
|
|
495
|
+
// The engine excludes decisions undone before delivery. A queued or in-flight message
|
|
496
|
+
// already carries the feedback up to its own cursor. Comments and notes ask for a revision.
|
|
497
|
+
private async unsentReviewFeedback(client: ReviewClient, task: Task) {
|
|
486
498
|
const after = Math.max(task.feedbackCursor, ...this.store.runs().filter(r => r.taskId === task.id && ['queued', 'running', 'uncertain'].includes(r.status)).map(r => r.feedbackCursor || 0));
|
|
487
499
|
const feedback = await client.get<{ feedback?: { kind?: string }[] }>(`${taskRoute(task.name)}/feedback?after=${after}`);
|
|
488
|
-
|
|
500
|
+
const items = feedback.feedback || [];
|
|
501
|
+
return { unsent: items.length, comments: items.filter(item => item.kind === 'comment' || item.kind === 'note').length };
|
|
489
502
|
}
|
|
490
503
|
renameTask(taskId: string, title: string) {
|
|
491
504
|
if (this.store.task(taskId).status === 'deleting') throw new Error('This task is being deleted.');
|
|
@@ -528,3 +541,13 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
528
541
|
}
|
|
529
542
|
close() { this.closing = true; for (const timer of this.usageTimers.values()) clearTimeout(timer); this.flush(); for (const id of AGENTS) this.agents[id].close(); }
|
|
530
543
|
}
|
|
544
|
+
|
|
545
|
+
// The review engine records each task's accepted text as a commit beside the
|
|
546
|
+
// one it started from; their difference is what the task put in the
|
|
547
|
+
// manuscript. A rename counts as both of its paths.
|
|
548
|
+
export async function acceptedFiles(root: string, name: string) {
|
|
549
|
+
try {
|
|
550
|
+
const { stdout } = await execute('git', ['-C', root, 'diff', '--name-only', '--no-renames', '-z', `refs/latex-review/${name}/base`, `refs/latex-review/${name}/accepted`, '--']);
|
|
551
|
+
return stdout.split('\0').filter(Boolean);
|
|
552
|
+
} catch { return []; }
|
|
553
|
+
}
|
package/src/shared.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export type TaskStatus = 'idle' | 'queued' | 'running' | 'waiting_input' | 'building' | 'reviewing' | 'interrupted' | 'failed' | 'uncertain' | 'deleting';
|
|
2
2
|
export interface Project { id: string; name: string; path: string; createdAt: number }
|
|
3
|
-
|
|
3
|
+
// unsent counts review updates the agent has not received; comments are the
|
|
4
|
+
// ones that ask for a revision. files are the manuscript files this task's
|
|
5
|
+
// accepted changes touched, relative to the repository root.
|
|
6
|
+
export interface ReviewSummary { state: string; round: number | null; counts: Record<string, number>; unsent?: number; comments?: number; files?: string[]; next?: string; job?: { phase: string; status: string; error?: string; details?: string[] } | null }
|
|
4
7
|
export interface PendingReview { route: string; body: Record<string, unknown>; previousStatus: TaskStatus }
|
|
5
8
|
export interface Task {
|
|
6
9
|
id: string; projectId: string; name: string; title: string; instructions: string;
|
|
@@ -11,7 +14,12 @@ export interface Task {
|
|
|
11
14
|
pendingReview?: PendingReview | null;
|
|
12
15
|
}
|
|
13
16
|
export interface Run { id: string; taskId: string; requestKey: string; prompt: string; feedbackCursor?: number; status: 'queued' | 'running' | 'completed' | 'failed' | 'interrupted' | 'uncertain'; createdAt: number; finishedAt: number | null }
|
|
14
|
-
export interface TimelineItem { id: string; taskId: string; type: string; text: string; detail?: string; status?: string; createdAt: number }
|
|
17
|
+
export interface TimelineItem { id: string; taskId: string; type: string; text: string; detail?: string; status?: string; createdAt: number; meta?: MessageMeta }
|
|
18
|
+
// A user message can run a skill or carry review feedback; the chat shows
|
|
19
|
+
// those as what they are rather than as the prompt the agent receives.
|
|
20
|
+
export interface MessageMeta { skill?: { id: string; name: string }; feedback?: FeedbackSummary; sendsFeedback?: boolean }
|
|
21
|
+
export interface FeedbackSummary { round: number | null; accepted: number; rejected: number; undone: number; comments: { where: string; text: string }[]; buildError?: boolean }
|
|
22
|
+
export interface Skill { id: string; name: string; description: string; hint: string; source: 'builtin' | 'user' | 'paper' }
|
|
15
23
|
export interface PendingRequest { id: string; taskId: string; method: string; params: Record<string, any>; createdAt: number }
|
|
16
24
|
export type AgentId = 'codex' | 'claude' | 'antigravity';
|
|
17
25
|
export interface AgentModel { id: string; name: string; isDefault: boolean; efforts: string[]; defaultEffort: string | null }
|