papilot 0.1.1 → 0.3.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 +16 -4
- package/README.zh-CN.md +15 -3
- 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-BkowXmKr.js +206 -0
- package/dist/assets/index-Dq3s-vhH.css +1 -0
- package/dist/assets/math-BTm2hvOz.js +263 -0
- package/dist/assets/math-DEcVZfaU.css +1 -0
- package/dist/assets/{pdf-view-DJo0X38w.js → pdf-view-BEn1EztO.js} +18 -18
- package/dist/index.html +11 -2
- package/examples/papilot-paper/preamble.tex +17 -11
- package/package.json +13 -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/commit.ts +23 -0
- package/src/conversation.ts +48 -0
- package/src/notes.ts +5 -0
- package/src/server/agents.ts +4 -3
- package/src/server/main.ts +51 -8
- package/src/server/manuscript.ts +91 -10
- package/src/server/review.ts +2 -1
- package/src/server/service-environment.ts +22 -0
- package/src/server/skills.ts +57 -0
- package/src/server/workspace.ts +186 -26
- package/src/shared.ts +17 -3
- package/vendor/latex-review/dist/web/assets/index-Bg-VqnPa.css +1 -0
- package/vendor/latex-review/dist/web/assets/index-xLeqYBJt.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 +399 -145
- package/vendor/latex-review/src/web/style.css +130 -0
- package/vendor/latex-review/src/web/zh-CN.ts +188 -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
|
@@ -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,11 @@ 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 { buildFixPrompt, reviewInstructions, skillPrompt, summarizeFeedback } from '../conversation.ts';
|
|
14
|
+
import { commitMessageFrom } from '../commit.ts';
|
|
15
|
+
import { commitChanges, conflictMarkers, gitCommit, gitRemote, gitStatus, mergeConflicts, mergeInProgress } from './manuscript.ts';
|
|
16
|
+
import type { Task, Run, TimelineItem, PendingRequest, RuntimeStatus, Snapshot, PendingReview, AgentModel, Usage, AgentId, MessageMeta, FeedbackSummary, BuildFailure } from '../shared.ts';
|
|
13
17
|
|
|
14
18
|
const execute = promisify(execFile);
|
|
15
19
|
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -33,8 +37,16 @@ export function accountUsage(response: any): Usage | null {
|
|
|
33
37
|
return { plan: typeof limits.planType === 'string' ? limits.planType : null, windows, credits, reached: Boolean(limits.rateLimitReachedType) || response.ordinaryUsageAllowed === false };
|
|
34
38
|
}
|
|
35
39
|
const BUSY = new Set(['running', 'waiting_input', 'building', 'uncertain', 'deleting']);
|
|
40
|
+
// Review builds that fail on the agent's own edits (see the review engine's
|
|
41
|
+
// publish.ts), and the request keys of the turns that send them back to it.
|
|
42
|
+
const FIXABLE = /^(Some edits are outside the task scope|The revised document could not be compiled)/;
|
|
43
|
+
const BUILD_FIX = 'build-fix-';
|
|
36
44
|
|
|
37
45
|
const AGENTS: AgentId[] = ['codex', 'claude', 'antigravity'];
|
|
46
|
+
// A commit message's agent sees the changes in its prompt and needs no tools.
|
|
47
|
+
const commitInstructions = 'You write Git commit messages for the repository of a LaTeX paper. Reply with the commit message only, without a preamble, quotes or a code fence: a subject line in the imperative mood under 72 characters and, only when the change needs more explanation, a blank line and a body of a few short plain sentences. Describe what changed in the paper, such as its wording, sections, equations, figures or references, rather than listing files. Follow the style and language of the recent commit subjects, or write in English when there are none. Everything you need is in the message: do not run commands, read files or edit anything.';
|
|
48
|
+
// A merge's agent edits the conflicted files in the manuscript checkout and nothing else.
|
|
49
|
+
const mergeInstructions = 'You resolve Git merge conflicts in the repository of a LaTeX paper. Edit only the files listed as conflicted, and in each one remove every conflict marker (<<<<<<<, ||||||| , =======, >>>>>>>) so that the text reads as one coherent version. Keep the changes from both sides wherever they are compatible; where both sides rewrote the same passage, combine their intent, and where they truly contradict each other, prefer the version that is more complete and correct and say so. Keep the LaTeX valid: balanced environments and braces, labels, references and citations. You may read any file to understand the context. Do not edit other files, and do not run Git commands that change the repository, such as add, commit, merge, checkout, reset, restore or stash: the author reviews the result and commits the merge.';
|
|
38
50
|
const blankRuntime = (): RuntimeStatus => ({ connected: false, authenticated: false, account: null, error: null, models: [] });
|
|
39
51
|
|
|
40
52
|
export class Workspace extends EventEmitter {
|
|
@@ -47,6 +59,8 @@ export class Workspace extends EventEmitter {
|
|
|
47
59
|
private loaded = new Set<string>();
|
|
48
60
|
private preparing = new Set<string>();
|
|
49
61
|
private reviewWrites = new Set<string>();
|
|
62
|
+
// Papers with a commit in progress: no review write may change their files meanwhile.
|
|
63
|
+
private committing = new Set<string>();
|
|
50
64
|
private commentWrites = new Map<string, number>();
|
|
51
65
|
private deletions = new Set<string>();
|
|
52
66
|
private publishing = new Set<string>();
|
|
@@ -122,25 +136,31 @@ export class Workspace extends EventEmitter {
|
|
|
122
136
|
const project = { ...this.store.project(projectId), name };
|
|
123
137
|
this.store.saveProject(project); return project;
|
|
124
138
|
}
|
|
125
|
-
|
|
139
|
+
// A task can start from a skill; its instructions are then the author's own words, possibly none.
|
|
140
|
+
async createTask(input: { projectId: string; title: string; instructions: string; skill?: string; agent?: AgentId; model?: string; effort?: string; id: string }) {
|
|
126
141
|
if (!/^[\w-]{16,64}$/.test(input.id)) throw new Error('Invalid task ID.');
|
|
127
142
|
if (this.store.taskDeleted(input.id)) throw new Error('This task was deleted. Create a new task instead.');
|
|
128
143
|
const existing = this.store.tasks().find(t => t.id === input.id); if (existing) return existing;
|
|
129
144
|
if (this.preparing.has(input.id)) throw new Error('This task is being created. Refresh in a moment.');
|
|
130
145
|
this.preparing.add(input.id);
|
|
131
146
|
try {
|
|
132
|
-
const project = this.store.project(input.projectId),
|
|
147
|
+
const project = this.store.project(input.projectId), skill = input.skill ? findSkill(project.path, input.skill) : undefined;
|
|
148
|
+
if (!skill && !input.instructions.trim()) throw new Error('Describe the revision, or choose a skill.');
|
|
149
|
+
const client = await this.connectReview(project.path);
|
|
133
150
|
const name = 'p-' + input.id.toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 48);
|
|
134
151
|
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,
|
|
152
|
+
const task: Task = { id: input.id, projectId: project.id, name, title: input.title, instructions: skill ? skillPrompt(skill, input.instructions) : input.instructions,
|
|
136
153
|
worktree: info.worktree, threadId: null, turnId: null, agent: input.agent || 'codex', model: input.model || null, effort: input.effort || null, status: 'idle',
|
|
137
154
|
createdAt: Date.now(), updatedAt: Date.now(), feedbackCursor: 0, error: null, review: null };
|
|
138
155
|
this.store.saveTask(task);
|
|
139
|
-
await this.enqueue(task.id, input.instructions, 'initial-' + task.id, false);
|
|
156
|
+
await this.enqueue(task.id, input.instructions, 'initial-' + task.id, false, { skill });
|
|
140
157
|
return this.store.task(task.id);
|
|
141
158
|
} finally { this.preparing.delete(input.id); }
|
|
142
159
|
}
|
|
143
|
-
|
|
160
|
+
// The author's message, a skill run with or without one, the review
|
|
161
|
+
// feedback alone, or a build the agent broke. The chat keeps what happened;
|
|
162
|
+
// the agent gets the prompt.
|
|
163
|
+
async enqueue(taskId: string, text: string, requestKey: string, includeFeedback = true, options: { skill?: SkillFile; feedback?: boolean; build?: BuildFailure } = {}) {
|
|
144
164
|
const task = this.store.task(taskId);
|
|
145
165
|
if (task.status === 'deleting') throw new Error('This task is being deleted.');
|
|
146
166
|
const existing = this.store.runByKey(requestKey);
|
|
@@ -148,15 +168,18 @@ export class Workspace extends EventEmitter {
|
|
|
148
168
|
if (task.status === 'uncertain') throw new Error('Check the task status before sending more instructions.');
|
|
149
169
|
const pauseVersion = task.pauseVersion || 0;
|
|
150
170
|
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;
|
|
171
|
+
let prompt = options.build ? buildFixPrompt(options.build) : options.feedback ? reviewInstructions : options.skill ? skillPrompt(options.skill, text) : text.trim(), cursor = task.feedbackCursor;
|
|
172
|
+
let summary: FeedbackSummary | undefined;
|
|
152
173
|
if (includeFeedback) {
|
|
153
174
|
const client = await this.connectReview(this.store.project(task.projectId).path);
|
|
154
175
|
const feedback = await client.get(`${taskRoute(task.name)}/feedback?after=${task.feedbackCursor}`);
|
|
155
|
-
cursor = feedback.cursor;
|
|
176
|
+
cursor = feedback.cursor; summary = summarizeFeedback(feedback);
|
|
177
|
+
if (options.feedback && !summary) throw new Error('There is no new review feedback to send.');
|
|
156
178
|
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
179
|
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
180
|
}
|
|
159
181
|
const run: Run = { id: randomUUID(), taskId, requestKey, prompt, feedbackCursor: cursor, status: 'queued', createdAt: Date.now(), finishedAt: null };
|
|
182
|
+
const meta: MessageMeta = { ...(options.skill ? { skill: { id: options.skill.id, name: options.skill.name } } : {}), ...(summary ? { feedback: summary } : {}), ...(options.feedback ? { sendsFeedback: true } : {}), ...(options.build ? { build: options.build } : {}) };
|
|
160
183
|
const saved = this.store.transaction(() => {
|
|
161
184
|
// Feedback loading yields to other requests. Recheck identity, queue, and
|
|
162
185
|
// task status together before committing; none of the earlier reads own a lock.
|
|
@@ -172,7 +195,7 @@ export class Workspace extends EventEmitter {
|
|
|
172
195
|
if (current.status === 'uncertain') throw new Error('Check the task status before sending more instructions.');
|
|
173
196
|
this.store.saveRun(run);
|
|
174
197
|
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 });
|
|
198
|
+
this.store.saveItem({ id: run.id, taskId, type: 'user', text: options.feedback || options.build ? '' : text.trim(), createdAt: run.createdAt, ...(Object.keys(meta).length ? { meta } : {}) });
|
|
176
199
|
return run;
|
|
177
200
|
});
|
|
178
201
|
this.schedule();
|
|
@@ -195,12 +218,20 @@ export class Workspace extends EventEmitter {
|
|
|
195
218
|
void this.startRun(run).catch(e => this.failRun(run, e));
|
|
196
219
|
}
|
|
197
220
|
}
|
|
198
|
-
|
|
221
|
+
// The paths a task's review covers, or none when it covers the whole worktree.
|
|
222
|
+
private async reviewScope(task: Task): Promise<string[]> {
|
|
223
|
+
try {
|
|
224
|
+
const client = await this.connectReview(this.store.project(task.projectId).path);
|
|
225
|
+
const files: string[] = (await client.get(taskRoute(task.name))).scope?.files || [];
|
|
226
|
+
return files.some(file => file === '' || file === '.') ? [] : files;
|
|
227
|
+
} catch { return []; }
|
|
228
|
+
}
|
|
229
|
+
private instructions(task: Task, scope: string[] = []) {
|
|
199
230
|
return `You are ${this.agentOf(task) === 'codex' ? 'the official Codex agent' : AGENT_NAMES[this.agentOf(task)]} running inside Papilot, a paper editing and review workspace.
|
|
200
231
|
Your task is already created. Your editable working directory is ${task.worktree}.
|
|
201
232
|
Original task: ${task.instructions}
|
|
202
233
|
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.
|
|
234
|
+
${scope.length ? `The review covers only ${scope.join(', ')} in this worktree, and an edit to any other file stops the PDF build. Leave other files unchanged even when project instructions ask for a matching update; mention that update in your summary instead.\n` : ''}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
235
|
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
236
|
${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
237
|
}
|
|
@@ -211,7 +242,7 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
211
242
|
this.store.saveRun({ ...run, status: 'running' });
|
|
212
243
|
this.store.updateTask(task.id, { status: 'running', turnId: null, error: null });
|
|
213
244
|
if (!task.threadId || !this.loaded.has(task.threadId)) {
|
|
214
|
-
const options = { cwd: task.worktree, approvalPolicy: 'on-request', sandbox: 'workspace-write', developerInstructions: this.instructions(task), ...(task.model ? { model: task.model } : {}) };
|
|
245
|
+
const options = { cwd: task.worktree, approvalPolicy: 'on-request', sandbox: 'workspace-write', developerInstructions: this.instructions(task, await this.reviewScope(task)), ...(task.model ? { model: task.model } : {}) };
|
|
215
246
|
const result = task.threadId
|
|
216
247
|
? await agent.request('thread/resume', { ...options, threadId: task.threadId, excludeTurns: true })
|
|
217
248
|
: await agent.request('thread/start', options);
|
|
@@ -317,7 +348,7 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
317
348
|
this.store.saveRun({ ...run, status, finishedAt: Date.now() });
|
|
318
349
|
this.store.updateTask(task.id, { turnId: null, pendingRunId: null, status: status === 'completed' ? 'building' : status, error: p.turn.error?.message || null });
|
|
319
350
|
this.schedule();
|
|
320
|
-
if (status === 'completed') await this.publish(task.id,
|
|
351
|
+
if (status === 'completed') await this.publish(task.id, run);
|
|
321
352
|
} else if (method === 'error' && !p.willRetry) {
|
|
322
353
|
if (!this.active.has(task.id) || (eventTurn && !task.turnId)) return;
|
|
323
354
|
this.store.updateTask(task.id, { error: p.error?.message || `${AGENT_NAMES[agent]} encountered an error.` });
|
|
@@ -428,6 +459,8 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
428
459
|
}
|
|
429
460
|
}
|
|
430
461
|
if (this.active.has(taskId) || this.publishing.has(taskId) || BUSY.has(task.status) || task.status === 'queued') throw new Error('Pause the task before accepting, rejecting, or undoing changes. Wait for any PDF build to finish.');
|
|
462
|
+
if (this.committing.has(task.projectId)) throw new Error('A commit is in progress. Try again when it finishes.');
|
|
463
|
+
if (mergeInProgress(this.store.project(task.projectId).path)) throw new Error('Finish or abort the merge in the Git panel before accepting, rejecting, or undoing changes.');
|
|
431
464
|
if (this.reviewWrites.has(taskId)) throw new Error('A review action is already in progress. Wait for it to finish.');
|
|
432
465
|
this.reviewWrites.add(taskId);
|
|
433
466
|
if (pending) this.store.updateTask(taskId, { pendingReview: { ...pending, previousStatus: task.status } });
|
|
@@ -441,17 +474,19 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
441
474
|
}
|
|
442
475
|
finally { this.reviewWrites.delete(taskId); this.schedule(); }
|
|
443
476
|
}
|
|
444
|
-
|
|
445
|
-
|
|
477
|
+
// A build is automatic after the agent's run `after` completes. When its
|
|
478
|
+
// edits break the build, the agent gets the error in a new turn, once.
|
|
479
|
+
publish(taskId: string, after?: Run): Promise<void> {
|
|
480
|
+
const task = this.store.task(taskId), automatic = Boolean(after);
|
|
446
481
|
if (task.status === 'deleting') throw new Error('This task is being deleted.');
|
|
447
482
|
if (this.active.has(taskId)) throw new Error('Pause Codex before building a review.');
|
|
448
483
|
if (this.reviewWrites.has(taskId)) throw new Error('Wait for the current review action before building a review.');
|
|
449
484
|
if (this.publishing.has(taskId) || (!automatic && (BUSY.has(task.status) || task.status === 'queued'))) throw new Error('This task is busy. Wait for it to finish or pause it first.');
|
|
450
485
|
this.publishing.add(taskId);
|
|
451
486
|
this.store.updateTask(taskId, { status: 'building', error: null });
|
|
452
|
-
return this.buildReview(task).finally(() => { this.publishing.delete(taskId); this.schedule(); });
|
|
487
|
+
return this.buildReview(task, Boolean(after && !after.requestKey.startsWith(BUILD_FIX))).finally(() => { this.publishing.delete(taskId); this.schedule(); });
|
|
453
488
|
}
|
|
454
|
-
private async buildReview(task: Task) {
|
|
489
|
+
private async buildReview(task: Task, sendBack: boolean) {
|
|
455
490
|
const taskId = task.id;
|
|
456
491
|
try {
|
|
457
492
|
const client = await this.connectReview(this.store.project(task.projectId).path);
|
|
@@ -460,9 +495,17 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
460
495
|
if (this.closing) return;
|
|
461
496
|
const current = await client.get(`/api/jobs/${job.job}`);
|
|
462
497
|
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
|
|
498
|
+
this.store.updateTask(taskId, { review: { ...this.store.task(taskId).review, state: summary.state, round: summary.round, counts: summary.counts, job: current } });
|
|
464
499
|
if (current.status === 'done' || current.status === 'completed' || current.status === 'succeeded') break;
|
|
465
|
-
if (current.status === 'failed')
|
|
500
|
+
if (current.status === 'failed') {
|
|
501
|
+
const failure: BuildFailure = { error: current.error || 'PDF build failed.', details: current.details || [], ...(current.log ? { log: current.log } : {}) };
|
|
502
|
+
// Instructions the author queued meanwhile go first; the build after them sends any error that remains.
|
|
503
|
+
if (sendBack && FIXABLE.test(failure.error) && await this.enqueue(taskId, '', BUILD_FIX + job.job, false, { build: failure }).then(() => true, () => false)) {
|
|
504
|
+
this.store.updateTask(taskId, { status: 'queued', error: null });
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
throw new Error([failure.error, ...failure.details].join('\n'));
|
|
508
|
+
}
|
|
466
509
|
await delay(1500);
|
|
467
510
|
}
|
|
468
511
|
this.store.updateTask(taskId, { status: this.store.runs().some(r => r.taskId === taskId && r.status === 'queued') ? 'queued' : 'reviewing', error: null });
|
|
@@ -473,19 +516,121 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
473
516
|
try {
|
|
474
517
|
const task = this.store.task(taskId);
|
|
475
518
|
if (task.status === 'deleting') return;
|
|
476
|
-
const
|
|
519
|
+
const project = this.store.project(task.projectId), client = await this.connectReview(project.path);
|
|
477
520
|
const summary = await client.get(taskRoute(task.name));
|
|
478
|
-
const unsent = await this.
|
|
521
|
+
const { unsent, comments } = await this.unsentReviewFeedback(client, this.store.task(taskId));
|
|
522
|
+
const files = await acceptedFiles(project.path, task.name, task.committed?.accepted);
|
|
479
523
|
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 } });
|
|
524
|
+
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
525
|
} catch { /* The task remains visible even if its project is temporarily unavailable. */ }
|
|
482
526
|
}
|
|
483
|
-
// Review comments
|
|
484
|
-
//
|
|
485
|
-
|
|
527
|
+
// Review decisions, comments and undo updates the agent has not received yet.
|
|
528
|
+
// The engine excludes decisions undone before delivery. A queued or in-flight message
|
|
529
|
+
// already carries the feedback up to its own cursor. Comments and notes ask for a revision.
|
|
530
|
+
private async unsentReviewFeedback(client: ReviewClient, task: Task) {
|
|
486
531
|
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
532
|
const feedback = await client.get<{ feedback?: { kind?: string }[] }>(`${taskRoute(task.name)}/feedback?after=${after}`);
|
|
488
|
-
|
|
533
|
+
const items = feedback.feedback || [];
|
|
534
|
+
return { unsent: items.length, comments: items.filter(item => item.kind === 'comment' || item.kind === 'note').length };
|
|
535
|
+
}
|
|
536
|
+
// The agent that writes a commit message or resolves a merge: the one the
|
|
537
|
+
// author chose, or else the first that is signed in, with the chosen model
|
|
538
|
+
// or the default one.
|
|
539
|
+
private helper(choice: { agent?: AgentId; model?: string }, task: string) {
|
|
540
|
+
const usable = (id: AgentId) => this.agents[id].ready && this.runtimes[id].authenticated;
|
|
541
|
+
const id = choice.agent && usable(choice.agent) ? choice.agent : AGENTS.find(usable);
|
|
542
|
+
if (!id) throw new Error(`Sign in to Codex, Claude or Antigravity to ${task}.`);
|
|
543
|
+
const models = this.runtimes[id].models;
|
|
544
|
+
return { id, model: models.find(item => item.id === choice.model) || models.find(item => item.isDefault) };
|
|
545
|
+
}
|
|
546
|
+
// One turn on a thread the agent does not keep, outside any task: its
|
|
547
|
+
// reply is returned and no chat shows it.
|
|
548
|
+
private async oneOff(id: AgentId, options: { cwd: string; sandbox: 'read-only' | 'workspace-write'; instructions: string; prompt: string; model?: AgentModel; effort?: string | null; timeout: number; what: string }) {
|
|
549
|
+
const agent = this.agents[id], name = AGENT_NAMES[id];
|
|
550
|
+
const thread = await agent.request('thread/start', { cwd: options.cwd, approvalPolicy: 'never', sandbox: options.sandbox, ephemeral: true, developerInstructions: options.instructions, ...(options.model ? { model: options.model.id } : {}) });
|
|
551
|
+
const threadId = thread.thread.id;
|
|
552
|
+
return new Promise<string>((resolve, reject) => {
|
|
553
|
+
let text = '', turnId = '';
|
|
554
|
+
const listen = (message: any) => {
|
|
555
|
+
const p = message.params || {};
|
|
556
|
+
if (p.threadId !== threadId) return;
|
|
557
|
+
if (message.method === 'item/completed' && p.item?.type === 'agentMessage') text = p.item.text || text;
|
|
558
|
+
else if (message.method === 'turn/completed') finish(p.turn?.status === 'completed' ? null : new Error(p.turn?.error?.message || `${name} stopped before it could ${options.what}.`));
|
|
559
|
+
};
|
|
560
|
+
const timer = setTimeout(() => {
|
|
561
|
+
if (turnId) agent.request('turn/interrupt', { threadId, turnId }).catch(() => undefined);
|
|
562
|
+
finish(new Error(`${name} did not ${options.what} in time. Try again.`));
|
|
563
|
+
}, options.timeout);
|
|
564
|
+
const finish = (error: Error | null) => { clearTimeout(timer); agent.off('notification', listen); if (error) reject(error); else resolve(text); };
|
|
565
|
+
agent.on('notification', listen);
|
|
566
|
+
agent.request('turn/start', { threadId, input: [{ type: 'text', text: options.prompt }], cwd: options.cwd, ...(options.effort ? { effort: options.effort } : {}) })
|
|
567
|
+
.then(result => { turnId = result?.turn?.id || ''; }, error => finish(error instanceof Error ? error : new Error(String(error))));
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
// Writes a commit message for the chosen manuscript files, like Cursor's
|
|
571
|
+
// generate button: one read-only turn with the lightest reasoning the model offers.
|
|
572
|
+
async commitMessage(projectId: string, paths: string[], choice: { agent?: AgentId; model?: string } = {}) {
|
|
573
|
+
const project = this.store.project(projectId), { id, model } = this.helper(choice, 'write a commit message');
|
|
574
|
+
const { files, patch, recent } = await commitChanges(project.path, paths);
|
|
575
|
+
const tasks = this.store.tasks().filter(task => task.projectId === projectId && task.review?.files?.some(file => paths.includes(file))).map(task => task.title);
|
|
576
|
+
const prompt = [
|
|
577
|
+
recent.length ? `Recent commit subjects in this repository:\n${recent.map(subject => `- ${subject}`).join('\n')}` : 'This repository has no commits yet.',
|
|
578
|
+
tasks.length ? `Papilot tasks whose accepted changes are in these files:\n${tasks.map(title => `- ${title}`).join('\n')}` : '',
|
|
579
|
+
`Changed files:\n${files.map(file => `${file.status === '?' ? 'A' : file.status} ${file.from ? `${file.from} -> ` : ''}${file.path}`).join('\n')}`,
|
|
580
|
+
`Diff:\n${patch}`,
|
|
581
|
+
].filter(Boolean).join('\n\n');
|
|
582
|
+
const reply = await this.oneOff(id, { cwd: project.path, sandbox: 'read-only', instructions: commitInstructions, prompt, model, effort: model?.efforts[0], timeout: 120000, what: 'write a commit message' });
|
|
583
|
+
const message = commitMessageFrom(reply);
|
|
584
|
+
if (!message) throw new Error(`${AGENT_NAMES[id]} did not return a commit message. Try again.`);
|
|
585
|
+
return { message, agent: id };
|
|
586
|
+
}
|
|
587
|
+
// Resolves a merge's conflicts in the manuscript checkout itself: the agent
|
|
588
|
+
// edits only the conflicted files, at the model's default reasoning, and
|
|
589
|
+
// leaves committing the merge to the author once they have read the diffs.
|
|
590
|
+
async resolveConflicts(projectId: string, choice: { agent?: AgentId; model?: string } = {}) {
|
|
591
|
+
const project = this.store.project(projectId), { id, model } = this.helper(choice, 'resolve conflicts');
|
|
592
|
+
const { files, branch, upstream, ours, theirs } = await mergeConflicts(project.path);
|
|
593
|
+
if (!files.length) throw new Error('No file has conflict markers left. Commit the merge.');
|
|
594
|
+
const list = (items: string[]) => items.length ? items.map(item => `- ${item}`).join('\n') : '- (none listed)';
|
|
595
|
+
const prompt = [
|
|
596
|
+
`${branch} is being merged with ${upstream} in ${project.path}, and these files have conflicts:\n${list(files)}`,
|
|
597
|
+
`Commits on ${branch} since the two diverged:\n${list(ours)}`,
|
|
598
|
+
`Commits from ${upstream} being merged in:\n${list(theirs)}`,
|
|
599
|
+
'Resolve every conflict in those files, then reply with a short summary for the author: how you resolved each conflict, and any place where you had to choose one side.',
|
|
600
|
+
].join('\n\n');
|
|
601
|
+
const summary = await this.oneOff(id, { cwd: project.path, sandbox: 'workspace-write', instructions: mergeInstructions, prompt, model, effort: model?.defaultEffort, timeout: 600000, what: 'resolve the conflicts' });
|
|
602
|
+
return { summary: summary.trim(), remaining: files.filter(file => conflictMarkers(project.path, file)), agent: id, git: await gitStatus(project.path) };
|
|
603
|
+
}
|
|
604
|
+
// Commits what the author accepted from this task since its last commit,
|
|
605
|
+
// with the message they confirmed, and pushes it if they asked. Other
|
|
606
|
+
// uncommitted files stay as they are; a file another task also changed is
|
|
607
|
+
// committed as it is on disk.
|
|
608
|
+
async commitTask(taskId: string, message: string, push = false) {
|
|
609
|
+
const task = this.store.task(taskId);
|
|
610
|
+
if (task.status === 'deleting') throw new Error('This task is being deleted.');
|
|
611
|
+
if (task.pendingReview) throw new Error('Wait for the current review action before committing.');
|
|
612
|
+
const project = this.store.project(task.projectId);
|
|
613
|
+
return this.exclusiveCommit(project.id, async () => {
|
|
614
|
+
const accepted = await acceptedCommit(project.path, task.name);
|
|
615
|
+
const files = accepted ? await acceptedFiles(project.path, task.name, this.store.task(taskId).committed?.accepted) : [];
|
|
616
|
+
const paths = (await gitStatus(project.path)).files.filter(file => files.includes(file.path)).map(file => file.path);
|
|
617
|
+
if (!paths.length) throw new Error('This task has no accepted changes left to commit.');
|
|
618
|
+
const hash = await gitCommit(project.path, message, paths);
|
|
619
|
+
this.store.updateTask(taskId, { committed: { hash, accepted: accepted!, at: Date.now() } });
|
|
620
|
+
void this.refresh(taskId);
|
|
621
|
+
// The commit stands even when the push fails; the author can push again.
|
|
622
|
+
let pushError: string | undefined;
|
|
623
|
+
if (push) try { await gitRemote(project.path, 'push'); } catch (error) { pushError = errorText(error); }
|
|
624
|
+
return { commit: hash, git: await gitStatus(project.path), ...(pushError ? { pushError } : {}) };
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
// A commit reads the working tree that accepting, rejecting and undoing
|
|
628
|
+
// write, so neither starts while the other runs on the same paper.
|
|
629
|
+
async exclusiveCommit<T>(projectId: string, work: () => Promise<T>): Promise<T> {
|
|
630
|
+
if (this.committing.has(projectId)) throw new Error('A commit is already in progress. Wait for it to finish.');
|
|
631
|
+
if ([...this.reviewWrites].some(id => this.store.task(id).projectId === projectId)) throw new Error('Wait for the current review action before committing.');
|
|
632
|
+
this.committing.add(projectId);
|
|
633
|
+
try { return await work(); } finally { this.committing.delete(projectId); }
|
|
489
634
|
}
|
|
490
635
|
renameTask(taskId: string, title: string) {
|
|
491
636
|
if (this.store.task(taskId).status === 'deleting') throw new Error('This task is being deleted.');
|
|
@@ -528,3 +673,18 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
528
673
|
}
|
|
529
674
|
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
675
|
}
|
|
676
|
+
|
|
677
|
+
// The review engine records each task's accepted text as a commit beside the
|
|
678
|
+
// one it started from; their difference is what the task put in the
|
|
679
|
+
// manuscript, and the difference from the accepted text at the task's last
|
|
680
|
+
// commit is what it has added since. A rename counts as both of its paths.
|
|
681
|
+
export async function acceptedFiles(root: string, name: string, since?: string) {
|
|
682
|
+
try {
|
|
683
|
+
const { stdout } = await execute('git', ['-C', root, 'diff', '--name-only', '--no-renames', '-z', since || `refs/latex-review/${name}/base`, `refs/latex-review/${name}/accepted`, '--']);
|
|
684
|
+
return stdout.split('\0').filter(Boolean);
|
|
685
|
+
} catch { return []; }
|
|
686
|
+
}
|
|
687
|
+
export async function acceptedCommit(root: string, name: string) {
|
|
688
|
+
try { return (await execute('git', ['-C', root, 'rev-parse', '--verify', '-q', `refs/latex-review/${name}/accepted^{commit}`])).stdout.trim() || undefined; }
|
|
689
|
+
catch { return undefined; }
|
|
690
|
+
}
|
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 since its last commit, 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;
|
|
@@ -9,9 +12,19 @@ export interface Task {
|
|
|
9
12
|
error: string | null; review: ReviewSummary | null;
|
|
10
13
|
pauseVersion?: number; pendingRunId?: string | null;
|
|
11
14
|
pendingReview?: PendingReview | null;
|
|
15
|
+
// The task's last commit from its review, and the accepted text it included.
|
|
16
|
+
committed?: { hash: string; accepted: string; at: number } | null;
|
|
12
17
|
}
|
|
13
18
|
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 }
|
|
19
|
+
export interface TimelineItem { id: string; taskId: string; type: string; text: string; detail?: string; status?: string; createdAt: number; meta?: MessageMeta }
|
|
20
|
+
// A user message can run a skill or carry review feedback; the chat shows
|
|
21
|
+
// those as what they are rather than as the prompt the agent receives.
|
|
22
|
+
// Papilot itself sends the agent a build its own edits broke.
|
|
23
|
+
export interface MessageMeta { skill?: { id: string; name: string }; feedback?: FeedbackSummary; sendsFeedback?: boolean; build?: BuildFailure }
|
|
24
|
+
// A failed review build as the engine reports it; log is the compiler log's path.
|
|
25
|
+
export interface BuildFailure { error: string; details: string[]; log?: string }
|
|
26
|
+
export interface FeedbackSummary { round: number | null; accepted: number; rejected: number; undone: number; comments: { where: string; text: string }[]; buildError?: boolean }
|
|
27
|
+
export interface Skill { id: string; name: string; description: string; hint: string; source: 'builtin' | 'user' | 'paper' }
|
|
15
28
|
export interface PendingRequest { id: string; taskId: string; method: string; params: Record<string, any>; createdAt: number }
|
|
16
29
|
export type AgentId = 'codex' | 'claude' | 'antigravity';
|
|
17
30
|
export interface AgentModel { id: string; name: string; isDefault: boolean; efforts: string[]; defaultEffort: string | null }
|
|
@@ -21,7 +34,8 @@ export interface Usage { plan: string | null; windows: UsageWindow[]; credits: {
|
|
|
21
34
|
export interface RuntimeStatus { connected: boolean; authenticated: boolean; account: string | null; error: string | null; models: AgentModel[]; usage?: Usage | null }
|
|
22
35
|
export interface Snapshot { projects: Project[]; tasks: Task[]; requests: PendingRequest[]; runtime: RuntimeStatus; runtimes?: Partial<Record<AgentId, RuntimeStatus>>; version: string }
|
|
23
36
|
export interface GitFile { path: string; status: string; staged: boolean; lines?: [number, number]; from?: string }
|
|
24
|
-
|
|
37
|
+
// merging is a merge in progress, with the files whose conflict markers remain.
|
|
38
|
+
export interface GitStatus { branch: string | null; upstream: string | null; ahead: number; behind: number; files: GitFile[]; head: { hash: string; subject: string; date: number } | null; remote?: boolean; merging?: { message: string; conflicts: string[] } | null }
|
|
25
39
|
export interface GitDiff { path: string; diff: string; truncated: boolean }
|
|
26
40
|
export interface ManuscriptBuild { status: 'building' | 'ready' | 'failed'; key?: string; pages?: [number, number][]; builtAt?: number; error?: string; current: boolean }
|
|
27
41
|
export interface Annotation { id: string; projectId: string; key: string; page: number; rect: [number, number, number, number]; rects?: [number, number, number, number][]; comment: string; quote: string; source: { file: string; lines: [number, number]; excerpt?: string } | null; createdAt: number; taskId?: string }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@font-face{font-family:Review Serif;src:url(/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf)format("opentype");font-weight:400;font-display:swap}@font-face{font-family:Review Serif;src:url(/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf)format("opentype");font-weight:700;font-display:swap}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--serif:"Review Serif", Palatino, "Palatino Linotype", "Songti SC", Georgia, serif;--sans:-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Noto Sans", sans-serif;--frame:#fff;--bar:#fff;--ink:#171717;--muted:#666;--line:#e5e5e5;--paper:#fff;--accent:#356aa0;--accept:#1d7448;--reject:#b0313b;--comment:#356aa0;--before:#fbeeee;--before-edge:#d17c82;--after:#eef8f1;--after-edge:#68ab82;--head:#fafafa;--shadow:0 1px 2px #0000000f, 0 4px 18px #0000000d;--paper-top:24px;font:14px/1.5 var(--sans);color:var(--ink);background:var(--frame)}*{box-sizing:border-box}body{background:var(--frame);text-rendering:optimizelegibility;grid-template-rows:auto 1fr;height:100dvh;margin:0;display:grid;overflow:hidden}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.38}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:3px}[hidden]{display:none!important}.muted{color:var(--muted)}a{color:var(--accent);text-underline-offset:.18em}h1,h2,.brand{font-family:var(--serif);font-weight:400;line-height:1.3}.brand{letter-spacing:.02em;color:var(--ink);font-size:20px;font-variant-caps:small-caps;text-decoration:none}kbd{border:1px solid var(--line);color:var(--muted);background:var(--frame);vertical-align:1px;border-radius:4px;margin-left:6px;padding:2px 4px;font:600 10px/1 ui-monospace,SFMono-Regular,Menlo,monospace}.bar{background:var(--bar);border-bottom:1px solid var(--line);z-index:3;align-items:center;gap:18px;min-height:62px;padding:10px 24px;display:flex}.identity{flex:1;align-items:baseline;gap:10px;min-width:0;display:flex}.identity h1{letter-spacing:.02em;white-space:nowrap;text-overflow:ellipsis;margin:0;font-size:20px;font-variant-caps:small-caps;overflow:hidden}.identity span{white-space:nowrap;font-size:13px}.meter{color:var(--muted);white-space:nowrap;font-variant-numeric:tabular-nums;align-items:center;gap:10px;font-size:12px;display:flex}.track{background:var(--line);width:120px;height:2px;overflow:hidden}.track i{background:#999;width:0;height:100%;transition:width .3s;display:block}.bar-end{align-items:center;gap:8px;display:flex}.status:empty{display:none}.status{background:var(--head);color:var(--muted);white-space:nowrap;border-radius:999px;padding:3px 9px;font-size:12px}.status[data-state=build_failed],.status[data-state=offline]{color:var(--reject)}.ghost{color:var(--muted);background:0 0;border:1px solid #0000;border-radius:7px;padding:5px 9px;font-size:13px}.ghost:hover{border-color:var(--line);color:var(--ink);text-underline-offset:.18em;text-decoration:underline}.accept-all,.reject-all{color:var(--ink);border-color:var(--line);border-radius:999px;box-shadow:0 1px 3px #0000000d}.batch-count{text-align:center;font-variant-numeric:tabular-nums;min-width:18px;color:var(--muted);background:#0000000f;border-radius:999px;margin-left:6px;padding:0 5px;font-size:11.5px;line-height:18px;display:inline-block}.overview-link{color:var(--accent);flex:none;text-decoration:none}.ghost.round{border-color:var(--line);border-radius:50%;width:30px;height:30px;padding:0}#paper{padding:var(--paper-top) 24px 90px;overflow-anchor:none;outline:none;scroll-padding:70px 0 90px;overflow:auto}.document{max-width:980px;margin:0 auto}.empty{text-align:center;color:var(--muted);padding:60px 16px}.part+.part{margin-top:30px}.part-title{color:var(--muted);letter-spacing:.02em;margin:14px 0 18px;font-size:19px;font-variant-caps:small-caps;font-weight:400}.part-title .number{font-variant-numeric:tabular-nums;margin-right:8px}.sheet{background:var(--paper);box-shadow:var(--shadow);border-radius:3px;padding:10px 0}.context,.side{line-height:0}.document img{width:100%;height:auto;display:block}.fold{color:#9a9d98;letter-spacing:4px;background:repeating-linear-gradient(90deg,#0000 0 6px,#ececec 6px 12px) 50%/100% 1px no-repeat;justify-content:center;align-items:center;height:22px;margin:4px 0;font-size:16px;display:flex}.change{color:var(--ink);outline:none;scroll-margin:70px;position:relative}.change.standalone{background:var(--paper);box-shadow:var(--shadow);border-radius:3px;margin-bottom:26px}.change.inline{margin:6px 0}.change-head{background:var(--head);color:var(--muted);border-top:1px solid var(--line);border-bottom:1px solid var(--line);align-items:baseline;gap:10px;padding:8px 12px;font-size:12px;line-height:1.6;display:flex}.change-number{color:var(--ink);white-space:nowrap;font-weight:400}.change-where{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font:11px/1.8 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.change-kind{white-space:nowrap}.badge{white-space:nowrap;font-weight:600}.badge:empty{display:none}.link{color:var(--accent);background:0 0;border:0;padding:0 2px;font-size:12px}.link.active{font-weight:600;text-decoration:underline}.edits{white-space:nowrap}.side{border-left:2px solid #0000;position:relative}.side.before{background:var(--before);border-left-color:var(--before-edge)}.side.after{background:var(--after);border-left-color:var(--after-edge)}.side.after.identical{border-left-color:var(--line);background:0 0}.side img{mix-blend-mode:multiply}.side:after{font:10px/1 var(--sans);letter-spacing:.06em;text-transform:uppercase;color:var(--before-edge);opacity:.9;pointer-events:none;position:absolute;top:4px;right:8px}.side.before:after{content:"before"}.side.after:after{content:"after";color:var(--after-edge)}:where(:lang(zh-CN)) .side.before:after{content:"修改前"}:where(:lang(zh-CN)) .side.after:after{content:"修改后"}.side.after.identical:after,.change[data-resolved] .side:after{content:none}.caption{color:var(--muted);margin:0;padding:4px 12px 8px;font-size:12px}.change.current{box-shadow:0 0 0 2px var(--accent);z-index:1}.change.current .change-head{background:color-mix(in srgb, var(--accent) 8%, var(--head));color:var(--ink)}.change[data-state=accepted] .badge{color:var(--accept)}.change[data-state=rejected] .badge{color:var(--reject)}.change[data-state=commented] .badge,.change[data-state=mixed] .badge{color:var(--comment)}.change[data-resolved=after] .side.before,.change[data-resolved=before] .side.after:not(.identical){display:none}.change[data-resolved] .side{background:0 0;border-left-color:#0000}.change[data-resolved] .caption{display:none}.remark,.histories{padding:8px 14px;font-size:13px;line-height:1.55}.remark{background:color-mix(in srgb, var(--comment) 8%, #fff);border-top:1px solid var(--line);color:var(--ink)}.remark p,.histories p{white-space:pre-wrap;margin:0}.histories{border-bottom:1px solid var(--line);color:var(--muted);background:#f6f7fb}.source{background:var(--paper);border-top:1px solid var(--line);padding:10px 14px 12px;line-height:1.5}.source-unit+.source-unit{margin-top:12px}.source-title{color:#555;align-items:center;gap:8px;margin-bottom:6px;font:11px ui-monospace,SFMono-Regular,Menlo,monospace;display:flex}.unit-actions{font:13px var(--sans);align-items:center;gap:2px;margin-left:auto;display:flex}.unit-actions button{border:1px solid var(--line);background:var(--paper);border-radius:0;padding:1px 8px;font-weight:600}.unit-actions button:not(:disabled):hover{background:var(--frame)}.unit-actions .accept{color:var(--accept)}.unit-actions .reject{color:var(--reject)}.unit-actions .comment{color:var(--comment)}.unit-state{font-weight:600}.source-unit[data-state=accepted] .unit-state{color:var(--accept)}.source-unit[data-state=rejected] .unit-state{color:var(--reject)}.source-unit[data-state=commented] .unit-state{color:var(--comment)}.source-code{border:1px solid var(--line);white-space:pre-wrap;overflow-wrap:anywhere;color:#2b2f33;background:#fff;border-radius:5px;max-height:420px;margin:0;padding:10px 12px;font:12.5px/1.65 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:auto}.source-code del{color:#8e1d28;background:#fadfe1;-webkit-text-decoration:line-through #c9636b;text-decoration:line-through #c9636b}.source-code ins{color:#11603a;background:#d6f0df;text-decoration:none}.banner{background:color-mix(in srgb, var(--accent) 6%, var(--paper));border:1px solid var(--line);border-radius:8px;max-width:980px;margin:0 auto 14px;padding:10px 14px;font-size:13px}.banner p{margin:3px 0}.banner .details{background:var(--bar);white-space:pre-wrap;border-radius:5px;max-height:220px;margin:6px 0 2px;padding:8px 10px;font:11.5px/1.5 ui-monospace,Menlo,monospace;overflow:auto}.banner summary{cursor:pointer;color:var(--muted)}.waiting{text-align:center;padding:90px 16px}.waiting p{margin:6px 0}.pulse{background:var(--accent);border-radius:50%;width:12px;height:12px;margin:0 auto 18px;animation:1.6s ease-in-out infinite pulse}@keyframes pulse{0%,to{opacity:.25;transform:scale(.8)}50%{opacity:1;transform:scale(1.15)}}.task-list{background:var(--bar);border:1px solid var(--line);box-shadow:var(--shadow);border-radius:8px;margin:0;padding:0;list-style:none}.task-list li+li{border-top:1px solid var(--line)}.index .document{max-width:880px}.overview-heading{margin:32px 0}.overview-heading h1{letter-spacing:.02em;margin:0 0 6px;font-size:34px;font-variant-caps:small-caps}.overview-heading p{margin:6px 0}.overview-summary{color:var(--muted);font-size:13px}.task-tools{gap:10px;margin-bottom:16px;display:flex}.task-tools input,.task-tools select{font:inherit;color:var(--ink);background:var(--bar);border:1px solid var(--line);border-radius:7px;padding:9px 12px}.task-tools input{flex:1;min-width:0}.task-tools input::placeholder{color:var(--muted)}.task-card{color:var(--ink);border-radius:7px;padding:24px 20px;text-decoration:none;display:block}.task-card:hover{background:var(--head)}.task-card:focus-visible,.overview-link:focus-visible,.task-tools :focus-visible{outline:2px solid var(--accent);outline-offset:2px}.task-heading{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.task-heading h2{overflow-wrap:anywhere;margin:0;font-size:21px;font-weight:400}.task-meta{color:var(--muted);flex-wrap:wrap;gap:4px 14px;margin-top:5px;font-size:12px;display:flex}.task-name{overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.task-progress{color:var(--muted);align-items:center;gap:10px;margin-top:14px;font-size:12px;display:flex}.task-progress .track{width:140px}.task-footer{justify-content:space-between;align-items:baseline;gap:10px;margin-top:9px;font-size:12px;display:flex}.task-counts{color:var(--muted);flex-wrap:wrap;gap:4px 14px;display:flex}.task-counts [data-kind=pending]{color:var(--accent);font-weight:600}.task-counts [data-kind=commented]{color:var(--comment)}.task-open{color:var(--accent);flex:none;font-weight:600}.actions{z-index:4;background:var(--bar);border:1px solid var(--line);max-width:calc(100vw - 24px);box-shadow:var(--shadow);border-radius:12px;align-items:center;gap:2px;padding:5px;display:flex;position:fixed;bottom:22px;left:50%;transform:translate(-50%)}.actions button{white-space:nowrap;background:0 0;border:0;border-radius:8px;padding:9px 14px;font-size:14px}.actions button:not(:disabled):hover{background:var(--head)}.actions .nav{color:var(--muted);padding:6px 12px;font-size:20px;line-height:1}.actions .accept{color:var(--accept)}.actions .reject{color:var(--reject)}.actions .comment{color:var(--comment)}.actions .skip{font-weight:600}.actions .undo{color:var(--muted);border-left:1px solid var(--line);border-radius:0 8px 8px 0;margin-left:4px;font-size:13px}.actions .working{opacity:.55}.position{text-align:center;min-width:52px;color:var(--muted);font-variant-numeric:tabular-nums;font-size:12px}.complete{color:var(--muted);white-space:nowrap;padding:0 14px;font-size:13px}.change.current>.change-head{top:calc(-1 * var(--paper-top));z-index:4;position:sticky}.change-head .dock{flex:none;align-items:center;margin:0;display:flex}.change-head .actions{background:var(--bar);border:1px solid var(--line);border-radius:10px;gap:0;max-width:none;padding:3px;position:static;transform:none;box-shadow:0 1px 2px #00000014,0 6px 20px #0000001f}.change-head .actions button{padding:4px 8px;font-size:13px;line-height:1.5}.change-head .actions .nav{padding:2px 8px;font-size:17px}.change-head .actions .undo{margin-left:3px;padding:4px 10px;font-size:12px}.change-head .position{min-width:40px;font-size:11.5px}.change-head .complete{padding:0 8px;font-size:12px}.change-head .composer{width:min(380px,100vw - 24px);position:absolute;inset:calc(100% + 6px) 8px auto auto;transform:none}.composer{z-index:5;background:var(--bar);border:1px solid var(--line);border-radius:12px;width:min(540px,100vw - 24px);padding:12px;position:fixed;bottom:84px;left:50%;transform:translate(-50%);box-shadow:0 6px 24px #00000014}.composer label{color:var(--ink);margin-bottom:6px;font-size:17px;font-weight:400;display:block}.composer textarea{resize:vertical;border:1px solid var(--line);background:var(--paper);color:#1f2328;border-radius:7px;width:100%;min-height:84px;padding:8px 10px}.composer textarea:focus{outline:2px solid color-mix(in srgb, var(--comment) 45%, transparent);border-color:#0000}.composer-row{justify-content:space-between;align-items:center;margin-top:8px;font-size:12px;display:flex}.composer-row kbd{margin-left:0}.composer button{border:1px solid var(--line);background:var(--paper);color:var(--ink);border-radius:7px;padding:6px 16px}.toast{z-index:6;border:1px solid var(--line);background:var(--paper);max-width:min(640px,100vw - 24px);color:var(--ink);white-space:pre-wrap;padding:9px 14px;font-size:13px;position:fixed;top:74px;left:50%;transform:translate(-50%);box-shadow:0 3px 12px #0000000f}.toast.error{background:#7f2b31}dialog{border:1px solid var(--line);box-shadow:var(--shadow);background:var(--bar);color:var(--ink);border-radius:12px;max-width:min(460px,100vw - 32px);padding:20px 24px}dialog::backdrop{background:#00000040}dialog h2{margin:0 0 12px;font-size:23px}dialog dl{grid-template-columns:auto 1fr;gap:8px 16px;margin:0 0 12px;font-size:13px;display:grid}dialog dt{white-space:nowrap}dialog dt kbd{margin:0 2px 0 0}dialog dd{margin:0}dialog form{text-align:right}dialog button{border:1px solid var(--line);background:var(--frame);border-radius:7px;padding:5px 14px}@media (width<=720px){.bar{grid-template-columns:auto 1fr auto;gap:8px;padding:12px 16px;display:grid}.identity{grid-area:1/1/auto/-1}.bar .meter{justify-self:center;font-size:11px}.bar-end{gap:2px}.bar .ghost{padding:4px 2px;font-size:12px}.side{min-height:20px}.track{width:70px}.actions button{padding:9px}.change-head{flex-wrap:wrap}.change-head .dock{flex-basis:100%;order:9;justify-content:center;margin:4px 0 2px}.change-head .composer{width:calc(100vw - 24px);right:0}:root{--paper-top:10px}#paper{padding:var(--paper-top) 6px 90px}#paper.index{padding:10px 14px 40px}.overview-heading{margin-top:16px}.task-tools{flex-wrap:wrap}.task-tools input{flex-basis:100%}.task-tools select{width:100%}.task-card{padding:14px}.task-heading{flex-direction:column;gap:6px}}@media (prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}}body{display:block}#root{grid-template-rows:auto 1fr;height:100dvh;display:grid;overflow:hidden}.review-body{min-height:0;position:relative}.review-body>#paper{scrollbar-width:none;height:100%;padding-right:54px}.review-body>#paper::-webkit-scrollbar{display:none}.change-map{z-index:5;border:1px solid var(--line);touch-action:none;-webkit-user-select:none;user-select:none;background:#fafafa;border-radius:7px;width:22px;position:absolute;top:12px;bottom:12px;right:18px;box-shadow:0 1px 4px #0000000d}.change-map-viewport{cursor:grab;background:#6a6f741c;border:1px solid #c6c9cc;border-radius:5px;position:absolute;left:0;right:0}.change-map-viewport:active{cursor:grabbing}.change-map-mark{--mark:#b18a3c;background:var(--mark);border:0;border-radius:2px;width:calc(100% - 10px);min-height:0;padding:0;position:absolute;left:5px}.change-map-mark[data-state=accepted]{--mark:var(--accept)}.change-map-mark[data-state=rejected]{--mark:var(--reject)}.change-map-mark[data-state=commented]{--mark:var(--comment)}.change-map-mark[data-state=mixed]{--mark:#796195}.change-map-mark[aria-current]{outline:2px solid var(--ink);outline-offset:2px}.change-map-mark:hover,.change-map-mark:focus-visible{z-index:1;outline:2px solid var(--accent);outline-offset:2px}.change-map-tooltip{border:1px solid var(--line);background:var(--paper);width:max-content;max-width:min(360px,100vw - 64px);box-shadow:var(--shadow);color:var(--ink);font:12px/1.5 var(--sans);text-align:left;overflow-wrap:anywhere;pointer-events:none;visibility:hidden;border-radius:7px;padding:7px 10px;position:absolute;top:0;right:calc(100% + 12px)}.change-map-mark:hover .change-map-tooltip,.change-map-mark:focus-visible .change-map-tooltip{visibility:visible}@media (width<=720px){.review-body>#paper{padding-right:46px}.change-map{width:18px;right:16px}.change-map-mark{width:calc(100% - 6px);left:3px}.change-head .actions{flex-wrap:wrap;justify-content:center;max-width:100%}.change-head .actions button{padding:4px 6px;font-size:12px}.change-head .actions .nav{padding:2px 6px}.change-head .position{min-width:32px}}.pdf-strip{isolation:isolate;width:100%;position:relative;container-type:inline-size}.pdf-strip img{-webkit-user-select:none;user-select:none;pointer-events:none;position:absolute;inset:0}.pdf-text{color:#0000;-webkit-user-select:text;user-select:text;cursor:text;line-height:1;position:absolute;inset:0;overflow:hidden}.pdf-text-line{white-space:pre}.pdf-glyph{white-space:pre;transform-origin:0 0;color:#0000;font-family:Times New Roman,serif;font-weight:400;line-height:1;position:absolute}.pdf-glyph::selection{color:#0000;background:#437ee759}.copy-reference{white-space:nowrap}.change-head{flex-wrap:wrap}.change-head .change-where{min-width:160px}.document .source-unit button{line-height:1.5}.help-backdrop{z-index:8;background:#00000040;place-items:center;display:grid;position:fixed;inset:0}.help-backdrop dialog{margin:0;position:static}.composer{line-height:1.5}@media (width<=900px){.change-head .dock{flex-basis:100%;order:9;justify-content:center;margin:4px 0 2px}}#root:has(>.review-mode){grid-template-rows:auto auto minmax(0,1fr)}.reviewed-change,.change.reviewed-change.current{box-shadow:none;background:0 0;border:0}.reviewed-change>.change-head,.reviewed-change>.histories,.reviewed-change>.remark,.reviewed-change>.source,.reviewed-change>.caption,.mixed-reviewed>.side{display:none}.reviewed-placeholder{color:var(--muted);font:13px/1.5 var(--sans);text-align:center;margin:0;padding:10px 14px}.review-mode{border-bottom:1px solid var(--line);background:var(--paper);font:13px/1.4 var(--sans);flex-wrap:wrap;align-items:center;gap:8px;padding:6px 14px;display:flex}.review-mode>span,.review-mode>label{color:var(--muted);margin-right:auto}.review-mode label{align-items:center;gap:8px;display:flex}.review-mode select{border:1px solid var(--line);background:var(--paper);max-width:160px;font:inherit;color:var(--ink);border-radius:6px;padding:4px 8px}.review-mode .ghost{white-space:nowrap;padding:4px 8px;font-size:13px}.review-mode .undo{border:1px solid var(--line);border-radius:6px}.review-complete{max-width:var(--page-width);border:1px solid var(--line);background:var(--paper);font:14px/1.5 var(--sans);border-radius:10px;flex-wrap:wrap;align-items:baseline;gap:2px 8px;margin:0 auto 14px;padding:9px 14px;display:flex}.review-complete strong{font-weight:600}.review-complete span{color:var(--muted)}.review-complete-actions{align-items:center;gap:8px;margin-left:auto;display:flex}.review-complete-actions:empty{display:none}@media (width<=680px){.review-mode{padding:5px 8px}.review-mode .ghost{min-height:36px}.review-mode label{font-size:12px}.review-mode select{max-width:135px}}
|