papilot 0.8.1 → 0.8.3
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 +4 -2
- package/bin/doctor.mjs +1 -1
- package/dist/assets/{_virtual_papilot-licenses-CMfCtEYQ.js → _virtual_papilot-licenses-YJbh9c1G.js} +1 -1
- package/dist/assets/index-0siHHtfz.js +267 -0
- package/dist/assets/index-CxdYm81a.css +1 -0
- package/dist/assets/{math-Bu3xEyrC.js → math-ibBpTslk.js} +1 -1
- package/dist/assets/{pdf-view-PbkCuTZ-.js → pdf-view-CD1kNKYH.js} +1 -1
- package/dist/index.html +2 -2
- package/engine/dist/web/assets/FullPaper-DUgLb01O.js +54 -0
- package/engine/dist/web/assets/index-D3_ZFdT8.css +1 -0
- package/engine/dist/web/assets/index-DJywe0wU.js +19 -0
- package/engine/dist/web/assets/pdf.worker.min-CjEcRF4W.mjs +28 -0
- package/{vendor/latex-review → engine}/dist/web/index.html +2 -2
- package/engine/package-lock.json +3390 -0
- package/engine/package.json +50 -0
- package/{vendor/latex-review → engine}/src/core/build.ts +198 -99
- package/{vendor/latex-review → engine}/src/core/git.ts +133 -28
- package/{vendor/latex-review → engine}/src/core/outline.ts +0 -9
- package/engine/src/core/progress.ts +67 -0
- package/{vendor/latex-review → engine}/src/core/review.ts +32 -0
- package/engine/src/locales.ts +47 -0
- package/{vendor/latex-review → engine}/src/render/inline.ts +1 -0
- package/{vendor/latex-review → engine}/src/render/locate.ts +26 -16
- package/engine/src/render/positions.ts +34 -0
- package/engine/src/render/synctex.ts +188 -0
- package/engine/src/render/synctex.wasm +0 -0
- package/engine/src/render/synctex.wasm.LICENSE +50 -0
- package/{vendor/latex-review → engine}/src/server.ts +5 -3
- package/{vendor/latex-review → engine}/src/types.ts +13 -0
- package/engine/src/web/FullPaper.tsx +318 -0
- package/engine/src/web/i18n.ts +36 -0
- package/engine/src/web/locales/de.json +253 -0
- package/engine/src/web/locales/es.json +253 -0
- package/engine/src/web/locales/fr.json +253 -0
- package/engine/src/web/locales/index.ts +16 -0
- package/engine/src/web/locales/it.json +253 -0
- package/engine/src/web/locales/ja.json +253 -0
- package/engine/src/web/locales/ko.json +253 -0
- package/engine/src/web/locales/pt.json +253 -0
- package/engine/src/web/locales/sv.json +253 -0
- package/engine/src/web/locales/zh-Hant.json +253 -0
- package/engine/src/web/paper.ts +13 -0
- package/{vendor/latex-review → engine}/src/web/references.tsx +1 -1
- package/{vendor/latex-review → engine}/src/web/review.tsx +99 -2
- package/{vendor/latex-review → engine}/src/web/style.css +71 -11
- package/engine/src/web/view-preferences.ts +13 -0
- package/{vendor/latex-review → engine}/src/web/zh-CN.ts +3 -0
- package/package.json +18 -9
- package/src/conversation.ts +1 -1
- package/src/server/acp.ts +9 -5
- package/src/server/agents.ts +4 -2
- package/src/server/app.ts +23 -0
- package/src/server/changes.ts +12 -3
- package/src/server/imports.ts +69 -25
- package/src/server/main.ts +3 -2
- package/src/server/manuscript.ts +31 -12
- package/src/server/paths.ts +1 -1
- package/src/server/start.ts +3 -3
- package/src/server/store.ts +0 -8
- package/src/server/update-install.ts +6 -3
- package/src/server/updates.ts +55 -23
- package/src/server/workspace.ts +24 -2
- package/src/shared.ts +7 -1
- package/src/skill-metadata.ts +1 -1
- package/vendor/latex-review/package-lock.json +4 -3106
- package/vendor/latex-review/package.json +3 -46
- package/README.zh-CN.md +0 -100
- package/dist/assets/index-BlLyIqT9.js +0 -265
- package/dist/assets/index-DVZtyZvu.css +0 -1
- package/vendor/latex-review/dist/web/assets/index-CXdnUjct.js +0 -19
- package/vendor/latex-review/dist/web/assets/index-DEz-LsGl.css +0 -1
- package/vendor/latex-review/src/web/i18n.ts +0 -48
- /package/{vendor/latex-review → engine}/LICENSE +0 -0
- /package/{vendor/latex-review → engine}/dist/web/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
- /package/{vendor/latex-review → engine}/dist/web/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
- /package/{vendor/latex-review → engine}/src/cli.ts +0 -0
- /package/{vendor/latex-review → engine}/src/client.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/bib.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/changes.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/config.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/current-commit.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/delivery.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/diff.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/files.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/process.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/publish.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/reconcile.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/store.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/tasks.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/verify.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/view.ts +0 -0
- /package/{vendor/latex-review → engine}/src/render/effects.ts +0 -0
- /package/{vendor/latex-review → engine}/src/render/pdf.ts +0 -0
- /package/{vendor/latex-review → engine}/src/render/pdfdiff.ts +0 -0
- /package/{vendor/latex-review → engine}/src/render/round.ts +0 -0
- /package/{vendor/latex-review → engine}/src/render/worker.ts +0 -0
- /package/{vendor/latex-review → engine}/src/runtime.ts +0 -0
- /package/{vendor/latex-review → engine}/src/web/ChangeMap.tsx +0 -0
- /package/{vendor/latex-review → engine}/src/web/PdfStrip.tsx +0 -0
- /package/{vendor/latex-review → engine}/src/web/effect-preferences.ts +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/GUST-FONT-LICENSE.txt +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/LPPL.txt +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/MANIFEST-newpx.txt +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/README-newpx.txt +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/README.md +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/TeXGyrePagellaX-Bold.otf +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/TeXGyrePagellaX-Regular.otf +0 -0
- /package/{vendor/latex-review → engine}/src/web/index.html +0 -0
- /package/{vendor/latex-review → engine}/src/web/main.tsx +0 -0
- /package/{vendor/latex-review → engine}/src/web/model.ts +0 -0
- /package/{vendor/latex-review → engine}/src/web/overview.tsx +0 -0
- /package/{vendor/latex-review → engine}/src/web/shortcuts.ts +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Fragment,
|
|
3
|
+
lazy,
|
|
4
|
+
Suspense,
|
|
3
5
|
useEffect,
|
|
4
6
|
useMemo,
|
|
5
7
|
useRef,
|
|
@@ -18,7 +20,11 @@ import type {
|
|
|
18
20
|
} from "../types.ts";
|
|
19
21
|
import { PdfStrip } from "./PdfStrip.tsx";
|
|
20
22
|
import { ChangeMap } from "./ChangeMap.tsx";
|
|
23
|
+
import { placeable, type PlacedChange } from "./paper.ts";
|
|
24
|
+
// pdf.js loads only when the full paper is shown.
|
|
25
|
+
const FullPaper = lazy(() => import("./FullPaper.tsx").then((module) => ({ default: module.FullPaper })));
|
|
21
26
|
import { EFFECTS_EVENT, EFFECTS_STORAGE, readExpandedEffects } from "./effect-preferences.ts";
|
|
27
|
+
import { readView, VIEW_EVENT, VIEW_STORAGE, type ReviewView } from "./view-preferences.ts";
|
|
22
28
|
import { STATUS } from "./overview.tsx";
|
|
23
29
|
import { language, locale, t, tn, withLanguage } from "./i18n.ts";
|
|
24
30
|
import {
|
|
@@ -167,6 +173,22 @@ export function ReviewApp({
|
|
|
167
173
|
scrollUntil = useRef(0),
|
|
168
174
|
snapshot = useRef({ state, detail, busy, composer, current });
|
|
169
175
|
const [shortcuts, setShortcuts] = useState(readShortcuts);
|
|
176
|
+
// The whole paper with the changes in place, or the changed sections only.
|
|
177
|
+
// A host may fix the view in the address (Papilot's manuscript chooses its
|
|
178
|
+
// PDF with its own switch); otherwise it follows the stored choice.
|
|
179
|
+
const [pinnedView] = useState<ReviewView | null>(() => {
|
|
180
|
+
const value = new URLSearchParams(location.search).get("view");
|
|
181
|
+
return value === "paper" || value === "changes" ? value : null;
|
|
182
|
+
});
|
|
183
|
+
const [view, setView] = useState<ReviewView>(() => pinnedView ?? readView());
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (pinnedView) return;
|
|
186
|
+
const chosen = (event: Event) => setView((event as CustomEvent).detail === "changes" ? "changes" : "paper");
|
|
187
|
+
const stored = (event: StorageEvent) => { if (event.key === VIEW_STORAGE || event.key === null) setView(readView()); };
|
|
188
|
+
window.addEventListener(VIEW_EVENT, chosen);
|
|
189
|
+
window.addEventListener("storage", stored);
|
|
190
|
+
return () => { window.removeEventListener(VIEW_EVENT, chosen); window.removeEventListener("storage", stored); };
|
|
191
|
+
}, [pinnedView]);
|
|
170
192
|
const [expandedEffects, setExpandedEffects] = useState(readExpandedEffects);
|
|
171
193
|
// Per-edit disclosure is temporary and scoped to its round. Changing the
|
|
172
194
|
// global preference resets those overrides without reloading the review.
|
|
@@ -195,6 +217,16 @@ export function ReviewApp({
|
|
|
195
217
|
[detail],
|
|
196
218
|
),
|
|
197
219
|
stops = useMemo(() => (detail ? buildStops(detail) : []), [detail]),
|
|
220
|
+
// The change blocks in the order and numbering of the changes view.
|
|
221
|
+
placed = useMemo(() => {
|
|
222
|
+
const list: PlacedChange[] = [];
|
|
223
|
+
if (!detail) return list;
|
|
224
|
+
for (const section of detail.sections)
|
|
225
|
+
for (const block of section.blocks)
|
|
226
|
+
if (block.kind === "change") list.push({ block, index: list.length });
|
|
227
|
+
for (const block of detail.loose) list.push({ block, index: list.length });
|
|
228
|
+
return list;
|
|
229
|
+
}, [detail]),
|
|
198
230
|
// An edit's places show under the first change that includes it.
|
|
199
231
|
firstStop = useMemo(() => {
|
|
200
232
|
const found = new Map<string, number>();
|
|
@@ -1002,7 +1034,7 @@ export function ReviewApp({
|
|
|
1002
1034
|
<p className="reference-problem reference-uncited">
|
|
1003
1035
|
{uncited
|
|
1004
1036
|
.map((m) => t("Cites {key}, which no bibliography entry defines.", { key: m.key }))
|
|
1005
|
-
.join(language()
|
|
1037
|
+
.join(language().startsWith("zh") ? "" : " ")}
|
|
1006
1038
|
</p>
|
|
1007
1039
|
)}
|
|
1008
1040
|
{bib.length > 0 && (
|
|
@@ -1094,6 +1126,32 @@ export function ReviewApp({
|
|
|
1094
1126
|
</article>
|
|
1095
1127
|
);
|
|
1096
1128
|
}
|
|
1129
|
+
const fullPaper = !pinnedView && Boolean(detail?.pdf) && placeable(placed),
|
|
1130
|
+
paperView = fullPaper && view === "paper";
|
|
1131
|
+
// A host shows its own switch while the whole paper can be shown.
|
|
1132
|
+
useEffect(() => {
|
|
1133
|
+
const root = document.documentElement;
|
|
1134
|
+
if (fullPaper) root.dataset.fullPaper = "available";
|
|
1135
|
+
else delete root.dataset.fullPaper;
|
|
1136
|
+
}, [fullPaper]);
|
|
1137
|
+
// Keep the selected change in view when the view changes, from here or the host.
|
|
1138
|
+
const shownView = useRef(paperView);
|
|
1139
|
+
useEffect(() => {
|
|
1140
|
+
if (shownView.current === paperView) return;
|
|
1141
|
+
shownView.current = paperView;
|
|
1142
|
+
requestAnimationFrame(() => {
|
|
1143
|
+
const article = paper.current?.querySelector<HTMLElement>(`[data-stop="${snapshot.current.current}"]`);
|
|
1144
|
+
if (article) reveal(article);
|
|
1145
|
+
});
|
|
1146
|
+
}, [paperView]);
|
|
1147
|
+
function chooseView(next: ReviewView) {
|
|
1148
|
+
setView(next);
|
|
1149
|
+
try {
|
|
1150
|
+
localStorage.setItem(VIEW_STORAGE, next);
|
|
1151
|
+
} catch {
|
|
1152
|
+
/* The choice lasts for this page. */
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1097
1155
|
let index = 0;
|
|
1098
1156
|
return (
|
|
1099
1157
|
<>
|
|
@@ -1233,6 +1291,24 @@ export function ReviewApp({
|
|
|
1233
1291
|
{t("Undo revert")}
|
|
1234
1292
|
</button>
|
|
1235
1293
|
)}
|
|
1294
|
+
{fullPaper && (
|
|
1295
|
+
<div className="view-switch" role="group" aria-label={t("Review view")}>
|
|
1296
|
+
<button
|
|
1297
|
+
className={`ghost${view === "paper" ? " active" : ""}`}
|
|
1298
|
+
aria-pressed={view === "paper"}
|
|
1299
|
+
onClick={() => chooseView("paper")}
|
|
1300
|
+
>
|
|
1301
|
+
PDF
|
|
1302
|
+
</button>
|
|
1303
|
+
<button
|
|
1304
|
+
className={`ghost${view === "changes" ? " active" : ""}`}
|
|
1305
|
+
aria-pressed={view === "changes"}
|
|
1306
|
+
onClick={() => chooseView("changes")}
|
|
1307
|
+
>
|
|
1308
|
+
{t("Changes")}
|
|
1309
|
+
</button>
|
|
1310
|
+
</div>
|
|
1311
|
+
)}
|
|
1236
1312
|
{/* History is a toolbar action; its own row appears only while
|
|
1237
1313
|
browsing earlier decisions. */}
|
|
1238
1314
|
{detail && !history && !changes && (
|
|
@@ -1462,7 +1538,28 @@ export function ReviewApp({
|
|
|
1462
1538
|
/>
|
|
1463
1539
|
)}
|
|
1464
1540
|
<div className="document">
|
|
1465
|
-
{detail ? (
|
|
1541
|
+
{detail && paperView ? (
|
|
1542
|
+
<>
|
|
1543
|
+
<Suspense fallback={<div className="paper-status" role="status"><div className="pulse" /></div>}>
|
|
1544
|
+
<FullPaper
|
|
1545
|
+
url={detail.pdf!}
|
|
1546
|
+
changes={placed}
|
|
1547
|
+
render={(block, at) => change(block, at, true)}
|
|
1548
|
+
scrollRoot={paper}
|
|
1549
|
+
onReady={() => {
|
|
1550
|
+
const article = paper.current?.querySelector<HTMLElement>(
|
|
1551
|
+
`[data-stop="${snapshot.current.current}"]`,
|
|
1552
|
+
);
|
|
1553
|
+
if (article) reveal(article);
|
|
1554
|
+
}}
|
|
1555
|
+
onUnavailable={() => chooseView("changes")}
|
|
1556
|
+
/>
|
|
1557
|
+
</Suspense>
|
|
1558
|
+
{!stops.length && !changes && (
|
|
1559
|
+
<p className="empty">{t("No changes to review.")}</p>
|
|
1560
|
+
)}
|
|
1561
|
+
</>
|
|
1562
|
+
) : detail ? (
|
|
1466
1563
|
<>
|
|
1467
1564
|
{detail.sections.map((section) => (
|
|
1468
1565
|
<section className="part" key={section.node}>
|
|
@@ -247,6 +247,33 @@ kbd {
|
|
|
247
247
|
border-color: var(--line);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/* Full paper or changes only, as one segmented control (a host such as
|
|
251
|
+
Papilot shows its own instead). */
|
|
252
|
+
.view-switch {
|
|
253
|
+
display: inline-grid;
|
|
254
|
+
grid-template-columns: repeat(2, 1fr);
|
|
255
|
+
flex: none;
|
|
256
|
+
gap: 2px;
|
|
257
|
+
padding: 2px;
|
|
258
|
+
border: 1px solid var(--line);
|
|
259
|
+
border-radius: 9px;
|
|
260
|
+
background: var(--head);
|
|
261
|
+
}
|
|
262
|
+
.view-switch .ghost {
|
|
263
|
+
padding: 3px 10px;
|
|
264
|
+
border-radius: 6px;
|
|
265
|
+
font-weight: 500;
|
|
266
|
+
}
|
|
267
|
+
.view-switch .ghost:hover {
|
|
268
|
+
text-decoration: none;
|
|
269
|
+
border-color: transparent;
|
|
270
|
+
}
|
|
271
|
+
.view-switch .ghost.active {
|
|
272
|
+
color: var(--ink);
|
|
273
|
+
background: var(--paper);
|
|
274
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 0.06), 0 0 0 1px var(--line);
|
|
275
|
+
}
|
|
276
|
+
|
|
250
277
|
/* Document */
|
|
251
278
|
#paper {
|
|
252
279
|
overflow: auto;
|
|
@@ -399,6 +426,7 @@ kbd {
|
|
|
399
426
|
mix-blend-mode: multiply;
|
|
400
427
|
}
|
|
401
428
|
.side::after {
|
|
429
|
+
content: attr(aria-label);
|
|
402
430
|
position: absolute;
|
|
403
431
|
top: 4px;
|
|
404
432
|
right: 8px;
|
|
@@ -409,20 +437,9 @@ kbd {
|
|
|
409
437
|
opacity: 0.9;
|
|
410
438
|
pointer-events: none;
|
|
411
439
|
}
|
|
412
|
-
.side.before::after {
|
|
413
|
-
content: "before";
|
|
414
|
-
}
|
|
415
440
|
.side.after::after {
|
|
416
|
-
content: "after";
|
|
417
441
|
color: var(--after-edge);
|
|
418
442
|
}
|
|
419
|
-
/* :where keeps the original weight, so later rules still hide the labels. */
|
|
420
|
-
:where(:lang(zh-CN)) .side.before::after {
|
|
421
|
-
content: "修改前";
|
|
422
|
-
}
|
|
423
|
-
:where(:lang(zh-CN)) .side.after::after {
|
|
424
|
-
content: "修改后";
|
|
425
|
-
}
|
|
426
443
|
.side.after.identical::after,
|
|
427
444
|
.change[data-resolved] .side::after {
|
|
428
445
|
content: none;
|
|
@@ -908,6 +925,49 @@ kbd {
|
|
|
908
925
|
text-align: center;
|
|
909
926
|
padding: 90px 16px;
|
|
910
927
|
}
|
|
928
|
+
/* The whole paper: the new PDF's pages, opened where each change is shown
|
|
929
|
+
as in the changes view. A host's narrow-screen crop of the strips
|
|
930
|
+
(--pdf-crop-scale, --pdf-crop-shift) applies to the pages too. */
|
|
931
|
+
.paper-page {
|
|
932
|
+
background: var(--pdf-page, var(--paper));
|
|
933
|
+
box-shadow: var(--shadow);
|
|
934
|
+
border-radius: 3px;
|
|
935
|
+
margin-bottom: 14px;
|
|
936
|
+
}
|
|
937
|
+
.paper-context {
|
|
938
|
+
line-height: 0;
|
|
939
|
+
}
|
|
940
|
+
.paper-slice {
|
|
941
|
+
position: relative;
|
|
942
|
+
width: 100%;
|
|
943
|
+
}
|
|
944
|
+
.paper-slice canvas {
|
|
945
|
+
position: absolute;
|
|
946
|
+
inset: 0;
|
|
947
|
+
width: 100%;
|
|
948
|
+
height: 100%;
|
|
949
|
+
filter: var(--pdf-filter, none);
|
|
950
|
+
}
|
|
951
|
+
@media (max-width: 680px) {
|
|
952
|
+
.paper-slice {
|
|
953
|
+
width: calc(100% * var(--pdf-crop-scale, 1));
|
|
954
|
+
margin-left: calc(-100% * var(--pdf-crop-scale, 1) * var(--pdf-crop-shift, 0));
|
|
955
|
+
}
|
|
956
|
+
.paper-context {
|
|
957
|
+
overflow-x: clip;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
.paper-status {
|
|
961
|
+
text-align: center;
|
|
962
|
+
padding: 90px 16px;
|
|
963
|
+
color: var(--muted);
|
|
964
|
+
}
|
|
965
|
+
.paper-status p {
|
|
966
|
+
margin: 6px 0 14px;
|
|
967
|
+
}
|
|
968
|
+
.paper-clock {
|
|
969
|
+
font-variant-numeric: tabular-nums;
|
|
970
|
+
}
|
|
911
971
|
.waiting p {
|
|
912
972
|
margin: 6px 0;
|
|
913
973
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Whole paper or changed sections only. A host such as Papilot shows its own
|
|
2
|
+
// switch: it stores the choice and tells this frame; storage events also
|
|
3
|
+
// update other tabs.
|
|
4
|
+
export const VIEW_STORAGE = "latex-review:view";
|
|
5
|
+
export const VIEW_EVENT = "latex-review:view-changed";
|
|
6
|
+
export type ReviewView = "paper" | "changes";
|
|
7
|
+
export function readView(): ReviewView {
|
|
8
|
+
try {
|
|
9
|
+
return localStorage.getItem(VIEW_STORAGE) === "changes" ? "changes" : "paper";
|
|
10
|
+
} catch {
|
|
11
|
+
return "paper";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -66,6 +66,9 @@ export const zhCN: Record<string, string> = {
|
|
|
66
66
|
"Review history": "审阅历史",
|
|
67
67
|
"Keyboard shortcuts": "键盘快捷键",
|
|
68
68
|
"Review view": "审阅视图",
|
|
69
|
+
"Changes": "改动",
|
|
70
|
+
"Preparing the full PDF…": "正在准备完整 PDF…",
|
|
71
|
+
"The full PDF could not be loaded.": "无法加载完整 PDF。",
|
|
69
72
|
"Undo {action} · {changes}": "撤销{action} · {changes}",
|
|
70
73
|
"Change {list}": "修改 {list}",
|
|
71
74
|
"Changes {list}": "修改 {list}",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "papilot",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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",
|
|
@@ -48,34 +48,40 @@
|
|
|
48
48
|
"scripts/demo.ts",
|
|
49
49
|
"scripts/service.ts",
|
|
50
50
|
"examples/papilot-paper",
|
|
51
|
-
"
|
|
52
|
-
"
|
|
51
|
+
"engine/src",
|
|
52
|
+
"engine/dist",
|
|
53
|
+
"engine/package.json",
|
|
54
|
+
"engine/package-lock.json",
|
|
55
|
+
"engine/LICENSE",
|
|
53
56
|
"vendor/latex-review/package.json",
|
|
54
|
-
"vendor/latex-review/package-lock.json"
|
|
55
|
-
"vendor/latex-review/LICENSE"
|
|
57
|
+
"vendor/latex-review/package-lock.json"
|
|
56
58
|
],
|
|
57
59
|
"engines": {
|
|
58
60
|
"node": ">=22.22.0"
|
|
59
61
|
},
|
|
62
|
+
"desktopShell": 2,
|
|
60
63
|
"scripts": {
|
|
61
64
|
"dev": "tsx watch src/server/main.ts",
|
|
62
65
|
"start": "tsx src/server/main.ts",
|
|
63
66
|
"build": "tsc --noEmit && vite build",
|
|
64
67
|
"typecheck": "tsc --noEmit && tsc -p desktop/tsconfig.json --noEmit",
|
|
65
68
|
"test": "tsx --test tests/*.test.ts",
|
|
66
|
-
"setup:review": "npm --prefix
|
|
69
|
+
"setup:review": "npm --prefix engine ci && npm --prefix engine run build",
|
|
67
70
|
"demo": "tsx scripts/demo.ts",
|
|
68
71
|
"doctor": "node bin/papilot.mjs doctor",
|
|
69
72
|
"service:install": "tsx scripts/service.ts install",
|
|
70
73
|
"service:stop": "tsx scripts/service.ts stop",
|
|
71
|
-
"test:ui": "npm run build && node scripts/check-ui.mjs && node scripts/check-appearance.mjs && node scripts/check-updates.mjs && node scripts/check-papers.mjs && node scripts/check-imports.mjs && node scripts/check-compilation.mjs && node scripts/check-build-problems.mjs && node scripts/check-review-notes.mjs && node --import tsx scripts/check-note-groups.mjs && node scripts/check-note-selection.mjs && node scripts/check-ai-review.mjs && node --import tsx scripts/check-note-results.mjs && node --import tsx scripts/check-review-flow.mjs && node scripts/check-skills.mjs && node --import tsx scripts/check-skill-library.mjs && node scripts/check-attachments.mjs && node --import tsx scripts/check-accounts.mjs",
|
|
74
|
+
"test:ui": "npm run build && node scripts/check-ui.mjs && node scripts/check-task-model.mjs && node scripts/check-appearance.mjs && node scripts/check-updates.mjs && node scripts/check-papers.mjs && node scripts/check-imports.mjs && node scripts/check-compilation.mjs && node scripts/check-build-problems.mjs && node scripts/check-review-notes.mjs && node --import tsx scripts/check-note-groups.mjs && node scripts/check-note-selection.mjs && node scripts/check-ai-review.mjs && node --import tsx scripts/check-note-results.mjs && node --import tsx scripts/check-review-flow.mjs && node scripts/check-skills.mjs && node --import tsx scripts/check-skill-library.mjs && node scripts/check-attachments.mjs && node --import tsx scripts/check-accounts.mjs",
|
|
72
75
|
"site": "node scripts/build-site.mjs",
|
|
73
76
|
"test:site": "npm run site && node scripts/check-site.mjs",
|
|
74
77
|
"prepack": "npm run build && npm run setup:review",
|
|
75
78
|
"desktop:compile": "tsc -p desktop/tsconfig.json",
|
|
76
79
|
"desktop": "npm run setup:review && npm run build && npm run desktop:compile && node scripts/desktop.mjs dev",
|
|
77
80
|
"desktop:build": "npm run setup:review && npm run build && npm run desktop:compile && node scripts/desktop.mjs package",
|
|
78
|
-
"test:desktop": "node scripts/check-desktop.mjs"
|
|
81
|
+
"test:desktop": "node scripts/check-desktop.mjs",
|
|
82
|
+
"i18n": "tsx scripts/i18n.ts",
|
|
83
|
+
"i18n:check-app": "tsx scripts/i18n/app-locales.ts",
|
|
84
|
+
"test:locales": "npm run build && npm --prefix engine run build && node --import tsx scripts/check-locales.mjs"
|
|
79
85
|
},
|
|
80
86
|
"dependencies": {
|
|
81
87
|
"@fastify/compress": "^9.2.2",
|
|
@@ -109,6 +115,9 @@
|
|
|
109
115
|
"remark-gfm": "^4.0.1",
|
|
110
116
|
"remark-math": "^6.0.0",
|
|
111
117
|
"typescript": "^7.0.2",
|
|
112
|
-
"vite": "^8.3.5"
|
|
118
|
+
"vite": "^8.3.5",
|
|
119
|
+
"parse5": "^8.0.1",
|
|
120
|
+
"@babel/parser": "^8.0.10",
|
|
121
|
+
"@babel/types": "^8.0.10"
|
|
113
122
|
}
|
|
114
123
|
}
|
package/src/conversation.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function skillPrompt(skill: { name: string; body: string; folder?: string
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
interface FeedbackItem { kind?: string; round?: number | null; text?: string; changes?: (string | { file?: string; lines?: [number, number] })[] }
|
|
36
|
-
// The review engine's feedback (see
|
|
36
|
+
// The review engine's feedback (see engine FeedbackItem) as the chat shows it.
|
|
37
37
|
export function summarizeFeedback(feedback: { feedback?: FeedbackItem[]; build_error?: unknown }): FeedbackSummary | undefined {
|
|
38
38
|
const items = feedback.feedback || [];
|
|
39
39
|
if (!items.length && !feedback.build_error) return undefined;
|
package/src/server/acp.ts
CHANGED
|
@@ -218,7 +218,8 @@ export class AcpAgent extends Adapter {
|
|
|
218
218
|
const previous = this.catalog.find(entry => entry.model === option.value);
|
|
219
219
|
const entry = { model: option.value, displayName: option.name, isDefault: option.value === this.defaultModel,
|
|
220
220
|
supportedReasoningEfforts: option.value === current ? levels.map(choice => ({ reasoningEffort: choice.value })) : previous?.supportedReasoningEfforts || [],
|
|
221
|
-
|
|
221
|
+
// A turn's explicit effort must not become the catalog's default.
|
|
222
|
+
defaultReasoningEffort: previous?.supportedReasoningEfforts.length ? previous.defaultReasoningEffort : option.value === current ? level : null };
|
|
222
223
|
if (previous) Object.assign(previous, entry); else this.catalog.push(entry);
|
|
223
224
|
}
|
|
224
225
|
}
|
|
@@ -239,10 +240,13 @@ export class AcpAgent extends Adapter {
|
|
|
239
240
|
};
|
|
240
241
|
try {
|
|
241
242
|
const session = await this.session(turn.threadId, thread.cwd, thread.model);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
const option = session.configOptions?.find(option => option.category === 'thought_level' || option.id === 'thinking');
|
|
244
|
+
const defaultEffort = choices(option).some(choice => choice.value === 'default') ? 'default' : this.catalog.find(model => model.model === thread.model)?.defaultReasoningEffort;
|
|
245
|
+
const chosenEffort = effort || defaultEffort;
|
|
246
|
+
if (chosenEffort) {
|
|
247
|
+
if (!option || !choices(option).some(choice => choice.value === chosenEffort)) throw new Error(`${this.name} does not support reasoning effort ${chosenEffort} for this model.`);
|
|
248
|
+
const result = await this.rpc('session/set_config_option', { sessionId: turn.threadId, configId: option.id, value: chosenEffort });
|
|
249
|
+
if (result?.configOptions) session.configOptions = result.configOptions;
|
|
246
250
|
}
|
|
247
251
|
const prompt: any[] = [{ type: 'text', text: thread.started ? text : `${thread.instructions}\n\n---\n\n${text}` }];
|
|
248
252
|
if (images.length && !this.capabilities.promptCapabilities?.image) throw new Error(`This ${this.name} version does not support image input. Update it, or send the images to another agent.`);
|
package/src/server/agents.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { createRequire } from 'node:module';
|
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { pathToFileURL } from 'node:url';
|
|
11
|
-
import type { AccountInfo, ModelInfo, Options, PermissionResult,
|
|
11
|
+
import type { AccountInfo, ModelInfo, Options, PermissionResult, SDKMessage, SDKRateLimitInfo, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
12
12
|
import { AGENT_NAMES } from '../shared.ts';
|
|
13
13
|
import { DATA, ROOT } from './paths.ts';
|
|
14
14
|
import { imageType } from './attachments.ts';
|
|
@@ -79,13 +79,15 @@ export abstract class Adapter extends EventEmitter implements AgentRuntime {
|
|
|
79
79
|
if (method === 'turn/start') {
|
|
80
80
|
const thread = this.threads.get(String(params.threadId));
|
|
81
81
|
if (!thread) throw new Error('Start or resume the thread before starting a turn.');
|
|
82
|
+
if (params.model !== undefined) thread.model = params.model || null;
|
|
82
83
|
const text = (params.input || []).filter((part: { text?: unknown }) => typeof part.text === 'string').map((part: { text: string }) => part.text).join('\n');
|
|
83
84
|
const images = (params.input || []).filter((part: { type?: string; path?: string }) => part.type === 'localImage' && typeof part.path === 'string').map((part: { path: string }) => part.path);
|
|
84
85
|
const turn: Turn = { id: randomUUID(), threadId: String(params.threadId), clientId: params.clientUserMessageId, status: 'inProgress', stop: () => {} };
|
|
85
86
|
turn.stop = () => this.finish(turn, 'interrupted');
|
|
86
87
|
this.turns.set(turn.id, turn);
|
|
87
88
|
// Papilot records the turn ID before the first event arrives.
|
|
88
|
-
|
|
89
|
+
const settings = { ...thread, cwd: params.cwd || thread.cwd };
|
|
90
|
+
setImmediate(() => { if (turn.status === 'inProgress') this.run(turn, settings, text, params.effort || null, images); });
|
|
89
91
|
return { turn: { id: turn.id } };
|
|
90
92
|
}
|
|
91
93
|
if (method === 'turn/interrupt') { this.turns.get(String(params.turnId))?.stop(); return {}; }
|
package/src/server/app.ts
CHANGED
|
@@ -263,6 +263,13 @@ export async function buildApp(options: AppOptions) {
|
|
|
263
263
|
const task = store.task(request.params.id);
|
|
264
264
|
return workspace.commitMessage(task.projectId, task.review?.files || [], { taskId: task.id, agent: task.agent, fresh: (request.body as any)?.fresh === true });
|
|
265
265
|
});
|
|
266
|
+
app.post<{ Params: { id: string } }>('/api/tasks/:id/model', task, request => {
|
|
267
|
+
const b = (request.body || {}) as Record<string, unknown>;
|
|
268
|
+
if (Object.keys(b).some(key => !['model', 'effort', 'serviceTier'].includes(key))) throw new Error('Only the model and its settings can be changed.');
|
|
269
|
+
if (b.model !== undefined && (typeof b.model !== 'string' || b.model.length > 200)) throw new Error('Invalid model.');
|
|
270
|
+
if (b.effort !== undefined && b.effort !== '' && !effort(b.effort)) throw new Error('Invalid reasoning effort.');
|
|
271
|
+
return workspace.updateTaskModel(request.params.id, { ...(b.model !== undefined ? { model: b.model as string } : {}), ...(b.effort !== undefined ? { effort: b.effort as string } : {}), ...(b.serviceTier !== undefined ? { serviceTier: string(b.serviceTier, 'Service tier', 64) } : {}) });
|
|
272
|
+
});
|
|
266
273
|
app.post<{ Params: { id: string } }>('/api/tasks/:id/rename', task, request => workspace.renameTask(request.params.id, string((request.body as any).title, 'Task title', 120)));
|
|
267
274
|
app.post<{ Params: { id: string } }>('/api/tasks/:id/title', task, request => workspace.requestTaskTitle(request.params.id));
|
|
268
275
|
app.post<{ Params: { id: string } }>('/api/tasks/:id/archive', task, request => workspace.archiveTask(request.params.id, (request.body as any)?.archived, (request.body as any)?.finished === true));
|
|
@@ -466,6 +473,10 @@ export async function buildApp(options: AppOptions) {
|
|
|
466
473
|
if (typeof b?.id !== 'string' || typeof b?.confirmed !== 'boolean') throw new Error('Choose an entry to confirm.');
|
|
467
474
|
return changes.confirmReference(project.id, project.path, Number(request.params.number), b.id, b.confirmed);
|
|
468
475
|
});
|
|
476
|
+
app.get<{ Params: { projectId: string; number: string } }>('/review/:projectId/changes/pdf/:number', changesAccess, (request, reply) => {
|
|
477
|
+
const pdf = changes.pdf(store.project(request.params.projectId).id, Number(request.params.number));
|
|
478
|
+
return reply.type('application/pdf').sendFile(path.basename(pdf), path.dirname(pdf), { cacheControl: false });
|
|
479
|
+
});
|
|
469
480
|
app.get<{ Params: { projectId: string; number: string; file: string } }>('/review/:projectId/changes/files/:number/:file', changesAccess, (request, reply) => {
|
|
470
481
|
const file = changes.file(store.project(request.params.projectId).id, Number(request.params.number), request.params.file);
|
|
471
482
|
return reply.header('Cache-Control', 'private, max-age=31536000, immutable').type(file.endsWith('.svg') ? 'image/svg+xml' : 'image/png').send(fs.createReadStream(file));
|
|
@@ -484,6 +495,14 @@ export async function buildApp(options: AppOptions) {
|
|
|
484
495
|
return serialGit(project.id, () => workspace.exclusiveCommit(project.id, () => changes.undo(project.id, project.path, csrfFor(request.viewer!), id)));
|
|
485
496
|
});
|
|
486
497
|
|
|
498
|
+
// The complete PDF of a round's new version, for the review's full paper.
|
|
499
|
+
// A round may compile only the changed chapters; this compiles them all once.
|
|
500
|
+
app.get<{ Params: { projectId: string; name: string; round: string } }>('/review/:projectId/pdf/:name/:round', { config: { access: { project: 'projectId' } } }, async (request, reply) => {
|
|
501
|
+
const project = store.project(request.params.projectId), name = request.params.name, round = Number(request.params.round);
|
|
502
|
+
if (!/^[a-z0-9-]+$/.test(name) || !Number.isSafeInteger(round) || round < 1) return reply.code(404).send({ error: 'Review resource not found.' });
|
|
503
|
+
const pdf = await fullPaperPdf(project.path, name, round, 'after');
|
|
504
|
+
return reply.type('application/pdf').sendFile(path.basename(pdf), path.dirname(pdf), { cacheControl: false });
|
|
505
|
+
});
|
|
487
506
|
app.route<{ Params: { projectId: string; '*': string } }>({
|
|
488
507
|
// The review page posts with the review engine's token rather than Papilot's.
|
|
489
508
|
method: ['GET', 'POST'], url: '/review/:projectId/*', config: { access: { project: 'projectId' }, csrf: false }, handler: async (request, reply) => {
|
|
@@ -521,6 +540,10 @@ export async function buildApp(options: AppOptions) {
|
|
|
521
540
|
await workspace.refresh(task.id);
|
|
522
541
|
}
|
|
523
542
|
}
|
|
543
|
+
// A round's new version as one complete PDF, which Papilot compiles,
|
|
544
|
+
// lets the review show the changes in the whole paper.
|
|
545
|
+
const round = request.method === 'GET' && response.ok ? /^\/api\/tasks\/([a-z0-9-]+)\/rounds\/(\d+)$/.exec(route) : null;
|
|
546
|
+
if (round) (value as { pdf?: string }).pdf = `${prefix}/pdf/${round[1]}/${round[2]}`;
|
|
524
547
|
return rewriteReviewJson(value, prefix);
|
|
525
548
|
}
|
|
526
549
|
if (reviewTask && writesFiles) throw new Error('The review engine returned an unexpected response. Select Check status before continuing.');
|
package/src/server/changes.ts
CHANGED
|
@@ -17,7 +17,7 @@ type Op = ['equal' | 'replace' | 'insert' | 'delete', number, number, number, nu
|
|
|
17
17
|
export interface Unit { id: string; file: string; status: 'added' | 'deleted' | 'modified'; kind: string; whole_file: boolean; ops: Op[]; old_start: number; old_end: number; new_start: number; new_end: number; old_text: string; new_text: string; truncated: boolean }
|
|
18
18
|
type Decision = 'pending' | 'accepted' | 'rejected' | 'commented';
|
|
19
19
|
export type ChangeAction = 'stage' | 'unstage' | 'revert';
|
|
20
|
-
interface Round { number: number; before: string; after: string; units: Unit[]; folder: string; warnings: string[]; decisions: Record<string, Decision
|
|
20
|
+
interface Round { number: number; before: string; after: string; units: Unit[]; folder: string; warnings: string[]; decisions: Record<string, Decision>; pdf?: string }
|
|
21
21
|
interface Job { key: string; phase: 'build' | 'render'; status: 'running' | 'failed'; created: number; error?: string; details?: string[]; log?: string }
|
|
22
22
|
interface Snapshot { before: string | null; after: string }
|
|
23
23
|
interface Entry { mode: string; sha: string }
|
|
@@ -139,7 +139,7 @@ export class Changes {
|
|
|
139
139
|
const { Builder, BuildError, Repo, loadProject, withUnits, render } = await engine();
|
|
140
140
|
const project = loadProject(root), repo = new Repo(root), number = comparison.next++;
|
|
141
141
|
const units = await this.units(projectId, root, snapshot), folder = path.join(this.dir, projectId, String(number));
|
|
142
|
-
let warnings: string[] = [];
|
|
142
|
+
let warnings: string[] = [], pdf: string | undefined;
|
|
143
143
|
fs.rmSync(folder, { recursive: true, force: true }); fs.mkdirSync(folder, { recursive: true });
|
|
144
144
|
if (units.length) {
|
|
145
145
|
const builder = new Builder(project, repo);
|
|
@@ -154,8 +154,9 @@ export class Changes {
|
|
|
154
154
|
await render(folder, { main: project.main, source_dir: project.source_dir, figures: project.figures, old, new: next, units },
|
|
155
155
|
(subset: Unit[]) => builder.build(withUnits(repo, snapshot.before!, snapshot.after, subset), ['full']));
|
|
156
156
|
warnings = next.warnings.filter((warning: string) => !old.warnings.includes(warning));
|
|
157
|
+
pdf = next.pdf;
|
|
157
158
|
} else fs.writeFileSync(path.join(folder, 'round.json'), JSON.stringify({ units: {}, sections: [], loose: [] }));
|
|
158
|
-
comparison.rounds.push({ number, before: snapshot.before!, after: snapshot.after, units, folder, warnings, decisions: {} });
|
|
159
|
+
comparison.rounds.push({ number, before: snapshot.before!, after: snapshot.after, units, folder, warnings, decisions: {}, pdf });
|
|
159
160
|
// The viewer may still show the round before; older ones go.
|
|
160
161
|
for (const old of comparison.rounds.splice(0, Math.max(0, comparison.rounds.length - 3))) fs.rmSync(old.folder, { recursive: true, force: true });
|
|
161
162
|
if (comparison.job === job) comparison.job = null;
|
|
@@ -203,6 +204,8 @@ export class Changes {
|
|
|
203
204
|
task: 'changes', round: round.number, before: round.before, after: round.after,
|
|
204
205
|
units: round.units.map(unit => ({ ...unit, ...render.units[unit.id] })), sections: render.sections, loose: render.loose,
|
|
205
206
|
warnings: round.warnings, files: `/review/${projectId}/changes/files/${round.number}/`,
|
|
207
|
+
// The working tree compiles in full, so its PDF shows the whole paper.
|
|
208
|
+
...(round.pdf ? { pdf: `/review/${projectId}/changes/pdf/${round.number}` } : {}),
|
|
206
209
|
...(render.references ? { references: render.references } : {}),
|
|
207
210
|
};
|
|
208
211
|
}
|
|
@@ -238,6 +241,12 @@ export class Changes {
|
|
|
238
241
|
return { places: await more(round.folder, unit) };
|
|
239
242
|
}
|
|
240
243
|
|
|
244
|
+
pdf(projectId: string, number: number) {
|
|
245
|
+
const file = this.comparison(projectId).rounds.find(item => item.number === number)?.pdf;
|
|
246
|
+
if (!file || !fs.existsSync(file)) throw Object.assign(new Error('This comparison is out of date. Refresh the page.'), { statusCode: 404 });
|
|
247
|
+
return file;
|
|
248
|
+
}
|
|
249
|
+
|
|
241
250
|
file(projectId: string, number: number, name: string) {
|
|
242
251
|
const round = this.comparison(projectId).rounds.find(item => item.number === number);
|
|
243
252
|
const file = round && /^[\w.-]+\.(svg|png)$/.test(name) ? path.join(round.folder, name) : '';
|