papilot 0.7.0 → 0.8.1

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.
Files changed (75) hide show
  1. package/README.md +53 -100
  2. package/README.zh-CN.md +52 -97
  3. package/bin/doctor.mjs +55 -6
  4. package/bin/installation.d.mts +2 -0
  5. package/bin/installation.mjs +7 -1
  6. package/dist/assets/{_virtual_papilot-licenses-BWIsfrQh.js → _virtual_papilot-licenses-CMfCtEYQ.js} +54 -3
  7. package/dist/assets/index-BlLyIqT9.js +265 -0
  8. package/dist/assets/index-DVZtyZvu.css +1 -0
  9. package/dist/assets/latex-BarqJFtQ.svg +1 -0
  10. package/dist/assets/{math-BsEl3u6P.js → math-Bu3xEyrC.js} +1 -1
  11. package/dist/assets/pdf-view-PbkCuTZ-.js +598 -0
  12. package/dist/index.html +2 -2
  13. package/examples/papilot-paper/main.tex +1 -1
  14. package/examples/papilot-paper/preview/main.pdf +0 -0
  15. package/examples/papilot-paper/preview/main.synctex.gz +0 -0
  16. package/examples/papilot-paper/sections/02-using.tex +3 -1
  17. package/examples/papilot-paper/sections/09-agents.tex +8 -5
  18. package/package.json +18 -8
  19. package/skills/ai-review.md +18 -0
  20. package/skills/latex-warnings.md +1 -1
  21. package/src/conversation.ts +6 -0
  22. package/src/server/acp.ts +387 -0
  23. package/src/server/agents.ts +220 -43
  24. package/src/server/ai-review-routes.ts +72 -0
  25. package/src/server/ai-review.ts +38 -0
  26. package/src/server/app.ts +56 -14
  27. package/src/server/build-problems.ts +329 -0
  28. package/src/server/changes.ts +25 -2
  29. package/src/server/codex.ts +2 -1
  30. package/src/server/cursor.ts +153 -0
  31. package/src/server/desktop-demo.ts +38 -0
  32. package/src/server/latex-installation.ts +43 -0
  33. package/src/server/manuscript.ts +183 -23
  34. package/src/server/paths.ts +1 -1
  35. package/src/server/reconcile.ts +20 -0
  36. package/src/server/references.ts +70 -0
  37. package/src/server/review.ts +6 -5
  38. package/src/server/skills.ts +5 -8
  39. package/src/server/start.ts +19 -4
  40. package/src/server/store.ts +20 -2
  41. package/src/server/update-install.ts +53 -2
  42. package/src/server/update-worker.ts +6 -6
  43. package/src/server/updates.ts +49 -14
  44. package/src/server/workspace.ts +236 -41
  45. package/src/shared.ts +88 -9
  46. package/src/skill-icons.ts +18 -0
  47. package/src/skill-metadata.ts +45 -0
  48. package/vendor/latex-review/dist/web/assets/index-CXdnUjct.js +19 -0
  49. package/vendor/latex-review/dist/web/assets/index-DEz-LsGl.css +1 -0
  50. package/vendor/latex-review/dist/web/index.html +2 -2
  51. package/vendor/latex-review/src/core/bib.ts +692 -0
  52. package/vendor/latex-review/src/core/changes.ts +39 -0
  53. package/vendor/latex-review/src/core/current-commit.ts +104 -0
  54. package/vendor/latex-review/src/core/delivery.ts +62 -16
  55. package/vendor/latex-review/src/core/git.ts +2 -1
  56. package/vendor/latex-review/src/core/publish.ts +7 -1
  57. package/vendor/latex-review/src/core/reconcile.ts +101 -0
  58. package/vendor/latex-review/src/core/review.ts +6 -2
  59. package/vendor/latex-review/src/core/store.ts +1 -0
  60. package/vendor/latex-review/src/core/tasks.ts +1 -1
  61. package/vendor/latex-review/src/core/verify.ts +784 -0
  62. package/vendor/latex-review/src/core/view.ts +204 -0
  63. package/vendor/latex-review/src/render/round.ts +11 -0
  64. package/vendor/latex-review/src/server.ts +77 -1
  65. package/vendor/latex-review/src/types.ts +130 -2
  66. package/vendor/latex-review/src/web/effect-preferences.ts +10 -0
  67. package/vendor/latex-review/src/web/references.tsx +499 -0
  68. package/vendor/latex-review/src/web/review.tsx +263 -133
  69. package/vendor/latex-review/src/web/style.css +285 -0
  70. package/vendor/latex-review/src/web/zh-CN.ts +79 -2
  71. package/dist/assets/index-CIJEYlP7.js +0 -252
  72. package/dist/assets/index-hOJEruFc.css +0 -1
  73. package/dist/assets/pdf-view-Cz9_vK5U.js +0 -598
  74. package/vendor/latex-review/dist/web/assets/index-Be9CtgcZ.css +0 -1
  75. package/vendor/latex-review/dist/web/assets/index-TvMWVSaw.js +0 -19
@@ -18,8 +18,16 @@ import type {
18
18
  } from "../types.ts";
19
19
  import { PdfStrip } from "./PdfStrip.tsx";
20
20
  import { ChangeMap } from "./ChangeMap.tsx";
21
+ import { EFFECTS_EVENT, EFFECTS_STORAGE, readExpandedEffects } from "./effect-preferences.ts";
21
22
  import { STATUS } from "./overview.tsx";
22
- import { locale, t, tn, withLanguage } from "./i18n.ts";
23
+ import { language, locale, t, tn, withLanguage } from "./i18n.ts";
24
+ import {
25
+ ConfirmReferences,
26
+ ReferenceBanner,
27
+ ReferenceEntries,
28
+ risks,
29
+ useReferenceChecks,
30
+ } from "./references.tsx";
23
31
  import { bindingLabel, readShortcuts, resolveShortcuts, shortcutAction, SHORTCUT_EVENT, SHORTCUT_STORAGE } from "./shortcuts.ts";
24
32
  import {
25
33
  STATE,
@@ -137,6 +145,13 @@ export function ReviewApp({
137
145
  Record<string, PlaceView[] | "loading" | { error: string }>
138
146
  >({}),
139
147
  [composer, setComposer] = useState<Composer | null>(null),
148
+ // Accepting bibliography entries that are not verified waits for this.
149
+ [confirm, setConfirm] = useState<{
150
+ kind: Kind;
151
+ chosen: string[];
152
+ body: string;
153
+ items: { label: string; reasons: string[] }[];
154
+ } | null>(null),
140
155
  [draft, setDraft] = useState(""),
141
156
  [help, setHelp] = useState(false),
142
157
  // A note can reveal a decided change without opening the history controls.
@@ -152,6 +167,18 @@ export function ReviewApp({
152
167
  scrollUntil = useRef(0),
153
168
  snapshot = useRef({ state, detail, busy, composer, current });
154
169
  const [shortcuts, setShortcuts] = useState(readShortcuts);
170
+ const [expandedEffects, setExpandedEffects] = useState(readExpandedEffects);
171
+ // Per-edit disclosure is temporary and scoped to its round. Changing the
172
+ // global preference resets those overrides without reloading the review.
173
+ const [effectOverrides, setEffectOverrides] = useState<Record<string, boolean>>({});
174
+ useEffect(() => {
175
+ const changed = (event: Event) => setExpandedEffects((event as CustomEvent).detail === true);
176
+ const stored = (event: StorageEvent) => { if (event.key === EFFECTS_STORAGE || event.key === null) setExpandedEffects(readExpandedEffects()); };
177
+ window.addEventListener(EFFECTS_EVENT, changed);
178
+ window.addEventListener('storage', stored);
179
+ return () => { window.removeEventListener(EFFECTS_EVENT, changed); window.removeEventListener('storage', stored); };
180
+ }, []);
181
+ useEffect(() => { setEffectOverrides({}); }, [expandedEffects]);
155
182
  useEffect(() => {
156
183
  const changed = (event: Event) => setShortcuts(resolveShortcuts((event as CustomEvent).detail));
157
184
  const stored = (event: StorageEvent) => { if (event.key === SHORTCUT_STORAGE || event.key === null) setShortcuts(readShortcuts()); };
@@ -191,6 +218,13 @@ export function ReviewApp({
191
218
  undecided = Object.keys(decisions).filter(
192
219
  (id) => decisions[id] === "pending",
193
220
  ),
221
+ references = useReferenceChecks(detail, api),
222
+ hazards = useMemo(
223
+ () => risks(detail?.references, references.checks, references.pending, references.confirmed),
224
+ [detail, references.checks, references.pending, references.confirmed],
225
+ ),
226
+ // Batch acceptance leaves the changes whose entries need a look.
227
+ batch = undecided.filter((id) => !hazards.has(id)),
194
228
  latest = Boolean(
195
229
  detail && state && detail.round === state.latest && !state.closed,
196
230
  ),
@@ -198,6 +232,9 @@ export function ReviewApp({
198
232
  label = (kind: string) => t((changes ? CHANGE_ACTION : ACTION)[kind] ?? kind),
199
233
  stateLabel = (value: string) =>
200
234
  t((changes ? CHANGE_STATE : STATE)[value] ?? value);
235
+ // Keyboard handlers read the newest checks, not those of their render.
236
+ const hazardsNow = useRef(hazards);
237
+ hazardsNow.current = hazards;
201
238
  // What each button acts on: undecided changes, or in changes mode the
202
239
  // staged ones for Unstage and every change still on disk for Revert.
203
240
  const targets = (units: string[], kind: Kind) =>
@@ -281,11 +318,20 @@ export function ReviewApp({
281
318
  const article = paper.current?.querySelector<HTMLElement>(
282
319
  `[data-stop="${index}"]`,
283
320
  );
284
- article?.scrollIntoView({ block: "start", behavior: "smooth" });
321
+ if (article) reveal(article, "smooth");
285
322
  article?.focus({ preventScroll: true });
286
323
  });
287
324
  }
288
325
  }
326
+ function reveal(article: HTMLElement, behavior: ScrollBehavior = "instant") {
327
+ const root = paper.current;
328
+ if (!root) return;
329
+ // scrollIntoView also scrolls ancestors across the embedding iframe.
330
+ // Navigation belongs to the paper, independently of the host's Chat.
331
+ const padding = parseFloat(getComputedStyle(root).scrollPaddingTop) || 0;
332
+ const margin = parseFloat(getComputedStyle(article).scrollMarginTop) || 0;
333
+ root.scrollTo({ top: root.scrollTop + article.getBoundingClientRect().top - root.getBoundingClientRect().top - padding - margin, behavior });
334
+ }
289
335
  useEffect(() => {
290
336
  let cancelled = false,
291
337
  timer: ReturnType<typeof setTimeout>;
@@ -369,11 +415,10 @@ export function ReviewApp({
369
415
  }, [task, pinned]);
370
416
  useEffect(() => {
371
417
  if (detail && current >= 0)
372
- requestAnimationFrame(() =>
373
- paper.current
374
- ?.querySelector<HTMLElement>(`[data-stop="${current}"]`)
375
- ?.scrollIntoView({ block: "start" }),
376
- );
418
+ requestAnimationFrame(() => {
419
+ const article = paper.current?.querySelector<HTMLElement>(`[data-stop="${current}"]`);
420
+ if (article) reveal(article);
421
+ });
377
422
  }, [detail?.round]);
378
423
  useEffect(() => {
379
424
  if (state) document.title = `${t(state.title)} · LaTeX Review`;
@@ -408,6 +453,7 @@ export function ReviewApp({
408
453
  kind: Kind,
409
454
  chosen = targets(stops[current]?.units ?? [], kind),
410
455
  body = "",
456
+ confirmed = false,
411
457
  ) {
412
458
  if (
413
459
  !chosen.length ||
@@ -417,6 +463,13 @@ export function ReviewApp({
417
463
  blocked(kind)
418
464
  )
419
465
  return;
466
+ if (kind === "accept" && !confirmed) {
467
+ const items = chosen.flatMap((id) => hazardsNow.current.get(id) ?? []);
468
+ if (items.length) {
469
+ setConfirm({ kind, chosen, body, items });
470
+ return;
471
+ }
472
+ }
420
473
  epoch.current++;
421
474
  setBusy(true);
422
475
  snapshot.current.busy = true;
@@ -530,7 +583,8 @@ export function ReviewApp({
530
583
  typing =
531
584
  target.matches("input,textarea,select") || target.isContentEditable;
532
585
  if (event.key === "Escape") {
533
- if (help) setHelp(false);
586
+ if (confirm) setConfirm(null);
587
+ else if (help) setHelp(false);
534
588
  else if (composer) closeComposer();
535
589
  return;
536
590
  }
@@ -543,7 +597,7 @@ export function ReviewApp({
543
597
  void submitComposer();
544
598
  return;
545
599
  }
546
- if (typing || help || window.getSelection()?.toString()) return;
600
+ if (typing || help || confirm || window.getSelection()?.toString()) return;
547
601
  if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "z") {
548
602
  event.preventDefault();
549
603
  void undo();
@@ -568,7 +622,7 @@ export function ReviewApp({
568
622
  };
569
623
  window.addEventListener("keydown", listener);
570
624
  return () => window.removeEventListener("keydown", listener);
571
- }, [state, detail, current, busy, composer, draft, help, history, shortcuts]);
625
+ }, [state, detail, current, busy, composer, draft, help, confirm, history, shortcuts]);
572
626
  function onScroll() {
573
627
  if (
574
628
  !paper.current ||
@@ -698,7 +752,10 @@ export function ReviewApp({
698
752
  clean: boolean,
699
753
  ): ReactNode {
700
754
  const { total, shown } = unit.effects!,
701
- loaded = more[`${detail!.round}:${unit.id}`],
755
+ key = `${detail!.round}:${unit.id}`,
756
+ open = effectOverrides[key] ?? (expandedEffects || unit.visual === "unmapped" || unit.visual === "identical"),
757
+ bodyId = `effects-${detail!.round}-${unit.id}`,
758
+ loaded = more[key],
702
759
  places = [...shown, ...(Array.isArray(loaded) ? loaded : [])],
703
760
  strips = (items: Strip[], side: "before" | "after") =>
704
761
  items.length > 0 && (
@@ -720,47 +777,57 @@ export function ReviewApp({
720
777
  return (
721
778
  <section className="effects" key={unit.id}>
722
779
  <p className="effects-head">
723
- {tn(total, "Affects {n} place", "Affects {n} places")}
724
- </p>
725
- {places.map((place, i) => (
726
- <div className="place" key={i}>
727
- <p className="place-where">
728
- {t("p. {pages}", { pages: place.page })}
729
- {place.section ? ` · ${place.section}` : ""}
730
- {place.with
731
- ? ` · ${t("with {changes}", {
732
- changes: place.with
733
- .map((id) =>
734
- firstStop.has(id)
735
- ? t("Change {n}", { n: firstStop.get(id)! + 1 })
736
- : (units.get(id)?.file ?? id),
737
- )
738
- .join(", "),
739
- })}`
740
- : ""}
741
- </p>
742
- {strips(place.before, "before")}
743
- {strips(place.after, "after")}
744
- </div>
745
- ))}
746
- {total > places.length && (
747
- <button
748
- className="link more-places"
749
- disabled={loaded === "loading"}
750
- onClick={() => void loadPlaces(unit.id)}
751
- >
752
- {loaded === "loading"
753
- ? t("Loading…")
754
- : tn(
755
- total - places.length,
756
- "Show {n} more",
757
- "Show {n} more",
758
- )}
780
+ <button className="link effects-toggle" aria-expanded={open} aria-controls={bodyId}
781
+ onClick={() => setEffectOverrides((values) => ({ ...values, [key]: !open }))}>
782
+ <span aria-hidden="true">{open ? "▾" : "▸"}</span>{" "}
783
+ {tn(total, "Affects {n} place", "Affects {n} places")}
759
784
  </button>
760
- )}
761
- {loaded && typeof loaded === "object" && "error" in loaded && (
762
- <p className="caption">{loaded.error}</p>
763
- )}
785
+ </p>
786
+ <div id={bodyId} hidden={!open}>
787
+ {open && (
788
+ <>
789
+ {places.map((place, i) => (
790
+ <div className="place" key={i}>
791
+ <p className="place-where">
792
+ {t("p. {pages}", { pages: place.page })}
793
+ {place.section ? ` · ${place.section}` : ""}
794
+ {place.with
795
+ ? ` · ${t("with {changes}", {
796
+ changes: place.with
797
+ .map((id) =>
798
+ firstStop.has(id)
799
+ ? t("Change {n}", { n: firstStop.get(id)! + 1 })
800
+ : (units.get(id)?.file ?? id),
801
+ )
802
+ .join(", "),
803
+ })}`
804
+ : ""}
805
+ </p>
806
+ {strips(place.before, "before")}
807
+ {strips(place.after, "after")}
808
+ </div>
809
+ ))}
810
+ {total > places.length && (
811
+ <button
812
+ className="link more-places"
813
+ disabled={loaded === "loading"}
814
+ onClick={() => void loadPlaces(unit.id)}
815
+ >
816
+ {loaded === "loading"
817
+ ? t("Loading…")
818
+ : tn(
819
+ total - places.length,
820
+ "Show {n} more",
821
+ "Show {n} more",
822
+ )}
823
+ </button>
824
+ )}
825
+ {loaded && typeof loaded === "object" && "error" in loaded && (
826
+ <p className="caption">{loaded.error}</p>
827
+ )}
828
+ </>
829
+ )}
830
+ </div>
764
831
  </section>
765
832
  );
766
833
  }
@@ -770,6 +837,10 @@ export function ReviewApp({
770
837
  inline: boolean,
771
838
  ): ReactNode {
772
839
  const known = block.units.map((id) => units.get(id)!).filter(Boolean),
840
+ // A .bib edit is shown entry by entry, every field, rather than as
841
+ // the bibliography's typeset lines alone.
842
+ bib = detail!.references?.entries.filter((e) => (e.units ?? [e.unit]).some((id) => block.units.includes(id))) ?? [],
843
+ uncited = detail!.references?.missing.filter((m) => m.unit && block.units.includes(m.unit)) ?? [],
773
844
  status = stopState(block.units, decisions),
774
845
  reviewed = !pendingUnits(stops[index], decisions).length,
775
846
  expanded = browsing || index === linkedChange,
@@ -785,15 +856,17 @@ export function ReviewApp({
785
856
  (status === "commented" ? "after" : undefined),
786
857
  source =
787
858
  openSource.has(index) ||
788
- block.identical ||
789
- (!block.before.length && !block.after.length),
859
+ (!bib.length &&
860
+ (block.identical || (!block.before.length && !block.after.length))),
790
861
  remarks = [
791
862
  ...new Set(
792
863
  block.units.map((id) => state?.comments[id]).filter(Boolean),
793
864
  ),
794
865
  ],
795
866
  location = describeLocation(known, t),
796
- kindLabel = block.reflow
867
+ kindLabel = bib.length
868
+ ? t("Bibliography")
869
+ : block.reflow
797
870
  ? t("Reflow")
798
871
  : [
799
872
  ...new Set(
@@ -905,7 +978,7 @@ export function ReviewApp({
905
978
  block.before.length > 0 &&
906
979
  images(block.before, "before")}
907
980
  {block.after.length > 0 && images(block.after, "after")}
908
- {block.identical && (
981
+ {block.identical && !bib.length && (
909
982
  <p className="caption">
910
983
  {t("No visible change in the PDF; see the LaTeX source.")}
911
984
  </p>
@@ -925,6 +998,25 @@ export function ReviewApp({
925
998
  ))}
926
999
  </div>
927
1000
  )}
1001
+ {uncited.length > 0 && (
1002
+ <p className="reference-problem reference-uncited">
1003
+ {uncited
1004
+ .map((m) => t("Cites {key}, which no bibliography entry defines.", { key: m.key }))
1005
+ .join(language() === "zh-CN" ? "" : " ")}
1006
+ </p>
1007
+ )}
1008
+ {bib.length > 0 && (
1009
+ <ReferenceEntries
1010
+ entries={bib}
1011
+ checks={references.checks}
1012
+ pending={references.pending > 0}
1013
+ onRetry={references.retry}
1014
+ confirmed={references.confirmed}
1015
+ onConfirm={(id, on) =>
1016
+ void references.confirm(id, on).catch((e) => setToast((e as Error).message))
1017
+ }
1018
+ />
1019
+ )}
928
1020
  {source && (
929
1021
  <div className="source">
930
1022
  {known.map((unit) => (
@@ -1036,7 +1128,7 @@ export function ReviewApp({
1036
1128
  {/* Batch decisions cover only undecided changes; accepted,
1037
1129
  rejected and commented changes keep their state. A single
1038
1130
  undecided change is decided from its own bar. */}
1039
- {count.pending > 1 && (
1131
+ {count.pending > 1 && batch.length > 0 && (
1040
1132
  <button
1041
1133
  className="ghost accept accept-all"
1042
1134
  disabled={
@@ -1047,30 +1139,41 @@ export function ReviewApp({
1047
1139
  }
1048
1140
  title={
1049
1141
  state?.decisionLock ||
1050
- (changes
1051
- ? tn(
1052
- count.pending,
1053
- "Stage the {n} unstaged change.",
1054
- "Stage the {n} unstaged changes.",
1055
- )
1056
- : tn(
1057
- count.pending,
1058
- "Accept {n} undecided change",
1059
- "Accept {n} undecided changes",
1060
- ))
1142
+ [
1143
+ changes
1144
+ ? tn(
1145
+ batch.length,
1146
+ "Stage the {n} unstaged change.",
1147
+ "Stage the {n} unstaged changes.",
1148
+ )
1149
+ : tn(
1150
+ batch.length,
1151
+ "Accept {n} undecided change",
1152
+ "Accept {n} undecided changes",
1153
+ ),
1154
+ undecided.length > batch.length
1155
+ ? tn(
1156
+ undecided.length - batch.length,
1157
+ "Leaves {n} change with bibliography entries to check.",
1158
+ "Leaves {n} changes with bibliography entries to check.",
1159
+ )
1160
+ : "",
1161
+ ]
1162
+ .filter(Boolean)
1163
+ .join(" ")
1061
1164
  }
1062
1165
  aria-label={
1063
1166
  changes
1064
- ? t("Stage all ({n})", { n: count.pending })
1065
- : t("Accept all ({n})", { n: count.pending })
1167
+ ? t("Stage all ({n})", { n: batch.length })
1168
+ : t("Accept all ({n})", { n: batch.length })
1066
1169
  }
1067
1170
  aria-describedby={state?.decisionLock ? "review-lock" : undefined}
1068
- onClick={() => void decide("accept", undecided)}
1171
+ onClick={() => void decide("accept", batch)}
1069
1172
  >
1070
1173
  <span className="batch-label">
1071
1174
  {changes ? t("Stage all") : t("Accept all")}
1072
1175
  </span>
1073
- <span className="batch-count">{count.pending}</span>
1176
+ <span className="batch-count">{batch.length}</span>
1074
1177
  </button>
1075
1178
  )}
1076
1179
  {count.pending > 1 && !changes && (
@@ -1223,6 +1326,70 @@ export function ReviewApp({
1223
1326
  )}
1224
1327
  <div className="review-body">
1225
1328
  <main id="paper" ref={paper} tabIndex={-1} onScroll={onScroll}>
1329
+ {changes && detail && !count.pending && (count.accepted > 0 || !stops.length) && (
1330
+ <div className="review-complete" role="status" tabIndex={-1}>
1331
+ <strong>
1332
+ {count.accepted
1333
+ ? t("All changes are staged")
1334
+ : t("No uncommitted changes")}
1335
+ </strong>
1336
+ <span>
1337
+ {count.accepted
1338
+ ? t("Commit them when you are ready.")
1339
+ : t("The PDF matches the last commit.")}
1340
+ </span>
1341
+ <div className="review-complete-actions" />
1342
+ </div>
1343
+ )}
1344
+ {!changes && detail && !history && !count.pending && (
1345
+ <div className="review-complete" role="status" tabIndex={-1}>
1346
+ <strong>
1347
+ {t(
1348
+ state?.workspace?.status === "waiting_input"
1349
+ ? "Agent needs your input"
1350
+ : state?.workspace?.status === "queued"
1351
+ ? "Feedback queued"
1352
+ : state?.workspace?.status === "running"
1353
+ ? "Feedback sent · Agent is working"
1354
+ : state?.workspace?.status === "building" ||
1355
+ state?.state === "building"
1356
+ ? "Preparing the next review…"
1357
+ : state?.workspace?.merging ? "Merge review" : "All changes reviewed",
1358
+ )}
1359
+ </strong>
1360
+ <span>
1361
+ {t(
1362
+ state?.workspace?.status === "waiting_input"
1363
+ ? "Open Chat to answer the agent's question."
1364
+ : ["running", "queued", "building"].includes(
1365
+ state?.workspace?.status || "",
1366
+ ) || state?.state === "building"
1367
+ ? "New changes will appear here when ready."
1368
+ : state?.workspace?.merging
1369
+ ? "Your manuscript is unchanged until you save the merged result."
1370
+ : state?.workspace?.comments
1371
+ ? "Your comments are saved. Send feedback so the agent can revise."
1372
+ : state?.workspace?.deferred
1373
+ ? state.workspace.accepted
1374
+ ? "Choose how to save the accepted changes to your manuscript."
1375
+ : state.workspace.destination === "branch"
1376
+ ? "Choose how to save the accepted changes to your manuscript."
1377
+ : (state.workspace.destination === "workspace" || state.workspace.destination === "commit")
1378
+ ? "Accepted changes are in your manuscript."
1379
+ : "There are no accepted changes to save."
1380
+ : state?.workspace?.accepted
1381
+ ? "Accepted changes are in your manuscript."
1382
+ : state?.workspace?.unsent && !count.accepted
1383
+ ? "Your decisions are saved. Send them to the agent to continue."
1384
+ : count.commented && !state?.workspace
1385
+ ? "Your comments are saved for the agent."
1386
+ : "There are no new changes to review. Previous decisions are available in History.",
1387
+ )}
1388
+ </span>
1389
+ {/* A host such as Papilot puts its next step here, for example a commit. */}
1390
+ <div className="review-complete-actions" />
1391
+ </div>
1392
+ )}
1226
1393
  {(state?.commentLock || state?.decisionLock) && (
1227
1394
  <div className="banner" id="review-lock" role="status">
1228
1395
  {t((state.commentLock || state.decisionLock)!)}
@@ -1281,68 +1448,19 @@ export function ReviewApp({
1281
1448
  </details>
1282
1449
  </div>
1283
1450
  ) : null}
1284
- {changes && detail && !count.pending && (count.accepted > 0 || !stops.length) && (
1285
- <div className="review-complete" role="status">
1286
- <strong>
1287
- {count.accepted
1288
- ? t("All changes are staged")
1289
- : t("No uncommitted changes")}
1290
- </strong>
1291
- <span>
1292
- {count.accepted
1293
- ? t("Commit them when you are ready.")
1294
- : t("The PDF matches the last commit.")}
1295
- </span>
1296
- <div className="review-complete-actions" />
1297
- </div>
1298
- )}
1299
- {!changes && detail && !history && !count.pending && (
1300
- <div className="review-complete" role="status">
1301
- <strong>
1302
- {t(
1303
- state?.workspace?.status === "waiting_input"
1304
- ? "Agent needs your input"
1305
- : state?.workspace?.status === "queued"
1306
- ? "Feedback queued"
1307
- : state?.workspace?.status === "running"
1308
- ? "Feedback sent · Agent is working"
1309
- : state?.workspace?.status === "building" ||
1310
- state?.state === "building"
1311
- ? "Preparing the next review…"
1312
- : "All changes reviewed",
1313
- )}
1314
- </strong>
1315
- <span>
1316
- {t(
1317
- state?.workspace?.status === "waiting_input"
1318
- ? "Open Chat to answer the agent's question."
1319
- : ["running", "queued", "building"].includes(
1320
- state?.workspace?.status || "",
1321
- ) || state?.state === "building"
1322
- ? "New changes will appear here when ready."
1323
- : state?.workspace?.comments
1324
- ? "Your comments are saved. Send feedback so the agent can revise."
1325
- : state?.workspace?.deferred
1326
- ? state.workspace.accepted
1327
- ? "Accepted changes are saved in this task. Choose where to save them below."
1328
- : state.workspace.destination === "branch"
1329
- ? "Accepted changes are committed to the task branch."
1330
- : state.workspace.destination === "workspace"
1331
- ? "Accepted changes are in your manuscript."
1332
- : "There are no accepted changes to save."
1333
- : state?.workspace?.accepted
1334
- ? "Accepted changes are in your manuscript."
1335
- : state?.workspace?.unsent && !count.accepted
1336
- ? "Your decisions are saved. Send them to the agent to continue."
1337
- : count.commented && !state?.workspace
1338
- ? "Your comments are saved for the agent."
1339
- : "There are no new changes to review. Previous decisions are available in History.",
1340
- )}
1341
- </span>
1342
- {/* A host such as Papilot puts its next step here, for example a commit. */}
1343
- <div className="review-complete-actions" />
1344
- </div>
1345
- )}
1451
+ {detail?.references &&
1452
+ (detail.references.entries.length > 0 ||
1453
+ detail.references.missing.length > 0) && (
1454
+ <ReferenceBanner
1455
+ references={detail.references}
1456
+ checks={references.checks}
1457
+ pending={references.pending}
1458
+ confirmed={references.confirmed}
1459
+ changeOf={(unit) => (unit ? (firstStop.get(unit) ?? -1) : -1)}
1460
+ onSelect={(index) => select(index)}
1461
+ onRetry={references.retry}
1462
+ />
1463
+ )}
1346
1464
  <div className="document">
1347
1465
  {detail ? (
1348
1466
  <>
@@ -1463,6 +1581,18 @@ export function ReviewApp({
1463
1581
  {toast}
1464
1582
  </div>
1465
1583
  )}
1584
+ {confirm && (
1585
+ <ConfirmReferences
1586
+ items={confirm.items}
1587
+ staging={changes}
1588
+ onCancel={() => setConfirm(null)}
1589
+ onConfirm={() => {
1590
+ const chosen = confirm;
1591
+ setConfirm(null);
1592
+ void decide(chosen.kind, chosen.chosen, chosen.body, true);
1593
+ }}
1594
+ />
1595
+ )}
1466
1596
  {help && (
1467
1597
  <div className="help-backdrop" onClick={() => setHelp(false)}>
1468
1598
  <dialog