papilot 0.8.0 → 0.8.2
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.de.md +110 -0
- package/README.es.md +110 -0
- package/README.fr.md +110 -0
- package/README.it.md +110 -0
- package/README.ja.md +110 -0
- package/README.ko.md +110 -0
- package/README.md +54 -121
- package/README.pt.md +110 -0
- package/README.sv.md +110 -0
- package/README.zh-CN.md +58 -117
- package/README.zh-Hant.md +110 -0
- package/bin/doctor.mjs +7 -5
- package/dist/assets/{_virtual_papilot-licenses-D7ro6ecN.js → _virtual_papilot-licenses-CMfCtEYQ.js} +1 -1
- package/dist/assets/index-oAIkxhfp.js +265 -0
- package/dist/assets/index-t_qCOF_B.css +1 -0
- package/dist/assets/{math-V9uUaUzA.js → math-CEU4m4-H.js} +1 -1
- package/dist/assets/{pdf-view-Bnb2uIBE.js → pdf-view-HWRGBLsZ.js} +1 -1
- package/dist/index.html +2 -2
- package/examples/papilot-paper/preview/main.pdf +0 -0
- package/examples/papilot-paper/preview/main.synctex.gz +0 -0
- package/package.json +18 -5
- package/src/server/acp.ts +44 -7
- package/src/server/agents.ts +5 -3
- package/src/server/app.ts +10 -3
- package/src/server/desktop-demo.ts +38 -0
- package/src/server/imports.ts +60 -21
- package/src/server/manuscript.ts +44 -8
- package/src/server/reconcile.ts +20 -0
- package/src/server/review.ts +3 -2
- package/src/server/start.ts +26 -4
- package/src/server/store.ts +0 -8
- package/src/server/update-install.ts +22 -2
- package/src/server/updates.ts +32 -12
- package/src/server/workspace.ts +78 -15
- package/src/shared.ts +20 -9
- package/src/skill-metadata.ts +1 -1
- package/vendor/latex-review/dist/web/assets/index-DVs7cTBl.js +19 -0
- package/vendor/latex-review/dist/web/assets/index-ysOpu05D.css +1 -0
- package/vendor/latex-review/dist/web/index.html +2 -2
- package/vendor/latex-review/src/core/build.ts +180 -97
- package/vendor/latex-review/src/core/current-commit.ts +104 -0
- package/vendor/latex-review/src/core/delivery.ts +62 -16
- package/vendor/latex-review/src/core/git.ts +135 -29
- package/vendor/latex-review/src/core/outline.ts +0 -9
- package/vendor/latex-review/src/core/publish.ts +7 -1
- package/vendor/latex-review/src/core/reconcile.ts +101 -0
- package/vendor/latex-review/src/core/review.ts +5 -2
- package/vendor/latex-review/src/core/store.ts +1 -0
- package/vendor/latex-review/src/core/tasks.ts +1 -1
- package/vendor/latex-review/src/locales.ts +47 -0
- package/vendor/latex-review/src/server.ts +12 -1
- package/vendor/latex-review/src/types.ts +5 -2
- package/vendor/latex-review/src/web/i18n.ts +9 -21
- package/vendor/latex-review/src/web/locales/de.json +250 -0
- package/vendor/latex-review/src/web/locales/es.json +250 -0
- package/vendor/latex-review/src/web/locales/fr.json +250 -0
- package/vendor/latex-review/src/web/locales/index.ts +16 -0
- package/vendor/latex-review/src/web/locales/it.json +250 -0
- package/vendor/latex-review/src/web/locales/ja.json +250 -0
- package/vendor/latex-review/src/web/locales/ko.json +250 -0
- package/vendor/latex-review/src/web/locales/pt.json +250 -0
- package/vendor/latex-review/src/web/locales/sv.json +250 -0
- package/vendor/latex-review/src/web/locales/zh-Hant.json +250 -0
- package/vendor/latex-review/src/web/references.tsx +1 -1
- package/vendor/latex-review/src/web/review.tsx +8 -6
- package/vendor/latex-review/src/web/style.css +1 -11
- package/vendor/latex-review/src/web/zh-CN.ts +4 -2
- package/dist/assets/index-Cc-awdNn.css +0 -1
- package/dist/assets/index-D_VX51no.js +0 -265
- package/vendor/latex-review/dist/web/assets/index-7VKAte5x.js +0 -19
- package/vendor/latex-review/dist/web/assets/index-DEz-LsGl.css +0 -1
package/src/server/store.ts
CHANGED
|
@@ -83,14 +83,6 @@ export class Store extends EventEmitter {
|
|
|
83
83
|
});
|
|
84
84
|
this.emit('change');
|
|
85
85
|
}
|
|
86
|
-
// Runs a one-off change the first time this database sees its key.
|
|
87
|
-
migrate(key: string, fn: () => void) {
|
|
88
|
-
this.transaction(() => {
|
|
89
|
-
if (this.db.prepare('SELECT 1 FROM migrations WHERE key=?').get(key)) return;
|
|
90
|
-
fn();
|
|
91
|
-
this.db.prepare('INSERT INTO migrations VALUES(?,?)').run(key, Date.now());
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
86
|
transaction<T>(fn: () => T): T { this.db.exec('BEGIN IMMEDIATE'); try { const result = fn(); this.db.exec('COMMIT'); return result; } catch (e) { this.db.exec('ROLLBACK'); throw e; } }
|
|
95
87
|
recover() {
|
|
96
88
|
this.transaction(() => {
|
|
@@ -4,7 +4,7 @@ import net from 'node:net';
|
|
|
4
4
|
import { execFile, spawn } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
|
-
import { devRevision, readJson, stableVersion } from '../../bin/installation.mjs';
|
|
7
|
+
import { devRevision, newerVersion, readJson, stableVersion } from '../../bin/installation.mjs';
|
|
8
8
|
|
|
9
9
|
export const DEV_REPOSITORY = 'https://github.com/PapilotAI/papilot.git';
|
|
10
10
|
const git = async (args: string[], cwd: string) => (await promisify(execFile)('git', args, { cwd, timeout: 120000, maxBuffer: 1024 * 1024, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } })).stdout.trim();
|
|
@@ -18,6 +18,19 @@ export function npmCli(env = process.env, executable = process.execPath) {
|
|
|
18
18
|
}
|
|
19
19
|
throw new Error('npm is unavailable to Papilot.');
|
|
20
20
|
}
|
|
21
|
+
// A desktop installer runs a newer release when it provides the shell level the
|
|
22
|
+
// release declares and its bundled Node meets the release's engine range.
|
|
23
|
+
export const NEEDS_INSTALLER = 'This version needs the latest desktop app. Download it from GitHub Releases.';
|
|
24
|
+
export function desktopSupports(pkg: any, shell: number, node = process.versions.node) {
|
|
25
|
+
if (!Number.isInteger(pkg?.desktopShell) || pkg.desktopShell < 1 || pkg.desktopShell > shell) return false;
|
|
26
|
+
// npm's --engine-strict checks other range forms during installation.
|
|
27
|
+
const minimum = typeof pkg.engines?.node === 'string' ? /^\s*>=\s*v?(\d+\.\d+\.\d+)\s*$/.exec(pkg.engines.node)?.[1] : undefined;
|
|
28
|
+
return !minimum || !newerVersion(minimum, node);
|
|
29
|
+
}
|
|
30
|
+
// The installer's Codex serves releases that test the same or an older one.
|
|
31
|
+
export function codexVersion(bin: string | undefined) {
|
|
32
|
+
try { return readJson(path.join(path.dirname(fs.realpathSync(bin!)), '../package.json'))?.version || null; } catch { return null; }
|
|
33
|
+
}
|
|
21
34
|
export const loaderFor = (root: string) => createRequire(path.join(root, 'package.json')).resolve('tsx/esm');
|
|
22
35
|
export function runInstall(args: string[], cwd: string, log: number, timeout = 600000) {
|
|
23
36
|
return new Promise<void>((resolve, reject) => {
|
|
@@ -102,7 +115,7 @@ async function installDev(data: string, version: string) {
|
|
|
102
115
|
} catch (error) { fs.rmSync(stage, { recursive: true, force: true }); throw error; }
|
|
103
116
|
finally { fs.closeSync(log); }
|
|
104
117
|
}
|
|
105
|
-
export async function installRelease(data: string, version: string) {
|
|
118
|
+
export async function installRelease(data: string, version: string, desktop?: { shell: number; codex?: string }) {
|
|
106
119
|
if (devRevision(version)) return installDev(data, version);
|
|
107
120
|
if (!stableVersion(version)) throw new Error('Invalid release version.');
|
|
108
121
|
const releases = path.join(data, 'updates/releases'); fs.mkdirSync(releases, { recursive: true, mode: 0o700 });
|
|
@@ -116,7 +129,14 @@ export async function installRelease(data: string, version: string) {
|
|
|
116
129
|
for (const file of ['dist/index.html', 'src/server/start.ts', 'src/server/main.ts', 'bin/papilot.mjs', 'vendor/latex-review/package-lock.json']) {
|
|
117
130
|
if (!fs.existsSync(path.join(root, file))) throw new Error('The downloaded release is incomplete.');
|
|
118
131
|
}
|
|
132
|
+
if (desktop && !desktopSupports(pkg, desktop.shell)) throw new Error(NEEDS_INSTALLER);
|
|
119
133
|
await runInstall(['ci', '--prefix', path.join(root, 'vendor/latex-review'), ...flags], stage, log);
|
|
134
|
+
// Like the CLI's first run, Codex installs with its own scripts. The app
|
|
135
|
+
// finds it beside the release.
|
|
136
|
+
const tested = pkg.devDependencies?.['@openai/codex'], bundled = desktop && codexVersion(desktop.codex);
|
|
137
|
+
if (desktop && stableVersion(tested) && (!bundled || newerVersion(tested, bundled))) {
|
|
138
|
+
await runInstall(['install', '--prefix', path.join(stage, 'codex'), '--registry=https://registry.npmjs.org', '--no-audit', '--no-fund', `@openai/codex@${tested}`], stage, log);
|
|
139
|
+
}
|
|
120
140
|
await probeInstallation(root, version, stage, log);
|
|
121
141
|
return root;
|
|
122
142
|
} catch (error) { fs.rmSync(stage, { recursive: true, force: true }); throw error; }
|
package/src/server/updates.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { devRevision, newerVersion, readJson, stableVersion, writeJson } from '../../bin/installation.mjs';
|
|
6
|
-
import { installRelease, loaderFor, npmCli } from './update-install.ts';
|
|
6
|
+
import { desktopSupports, installRelease, loaderFor, NEEDS_INSTALLER, npmCli } from './update-install.ts';
|
|
7
7
|
import type { UpdateChannel, UpdateStatus } from '../shared.ts';
|
|
8
8
|
|
|
9
9
|
const activePhases = new Set(['installing', 'waiting', 'restarting']);
|
|
@@ -15,26 +15,32 @@ async function releaseInfo(url: string, fetcher: typeof fetch) {
|
|
|
15
15
|
if (text.length > 256000) throw new Error('Invalid release information.');
|
|
16
16
|
return JSON.parse(text);
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
// The package.json of the newest release on a channel.
|
|
19
|
+
export async function latestManifest(channel: UpdateChannel = 'stable', fetcher = fetch): Promise<Record<string, any>> {
|
|
19
20
|
if (channel === 'dev') {
|
|
20
21
|
const commit = await releaseInfo('https://api.github.com/repos/PapilotAI/papilot/git/ref/heads/main', fetcher);
|
|
21
22
|
const revision = commit.object?.sha;
|
|
22
23
|
if (!/^[a-f0-9]{40}$/.test(revision)) throw new Error('Invalid development revision.');
|
|
23
24
|
const pkg = await releaseInfo(`https://raw.githubusercontent.com/PapilotAI/papilot/${revision}/package.json`, fetcher);
|
|
24
25
|
if (pkg.name !== 'papilot' || !stableVersion(pkg.version)) throw new Error('Invalid development version.');
|
|
25
|
-
return `${pkg.version}-dev.${revision}
|
|
26
|
+
return { ...pkg, version: `${pkg.version}-dev.${revision}` };
|
|
26
27
|
}
|
|
27
28
|
const pkg = await releaseInfo('https://registry.npmjs.org/papilot/latest', fetcher);
|
|
28
29
|
if (pkg.name !== 'papilot' || !stableVersion(pkg.version)) throw new Error('Invalid release information.');
|
|
29
|
-
return pkg
|
|
30
|
+
return pkg;
|
|
30
31
|
}
|
|
32
|
+
export const latestRelease = async (channel: UpdateChannel = 'stable', fetcher = fetch) => (await latestManifest(channel, fetcher)).version as string;
|
|
31
33
|
const validTarget = (version: unknown, channel: UpdateChannel): version is string => channel === 'dev' ? Boolean(devRevision(version)) : stableVersion(version);
|
|
32
34
|
const available = (next: string, current: string, channel: UpdateChannel) => validTarget(next, channel)
|
|
33
35
|
&& (channel === 'dev' || devRevision(current) ? next !== current : newerVersion(next, current));
|
|
34
36
|
interface Options {
|
|
37
|
+
// Desktop builds that cannot install releases link to this download page.
|
|
38
|
+
downloadUrl?: string;
|
|
39
|
+
// A desktop installer that runs newer releases from npm (see desktop/runtime.ts).
|
|
40
|
+
desktop?: { shell: number; installer: string; downloadUrl: string };
|
|
35
41
|
root: string; data: string; version: string; port: number;
|
|
36
42
|
busy: () => boolean; shutdown: () => void;
|
|
37
|
-
latest?: (channel: UpdateChannel) => Promise<string
|
|
43
|
+
latest?: (channel: UpdateChannel) => Promise<string | Record<string, any>>; install?: (data: string, version: string) => Promise<string>;
|
|
38
44
|
restart?: (root: string, version: string) => Promise<void>; supported?: boolean;
|
|
39
45
|
}
|
|
40
46
|
// Both HTTP listeners share one updater. Downloads do not interrupt editing;
|
|
@@ -42,7 +48,6 @@ interface Options {
|
|
|
42
48
|
export class Updates {
|
|
43
49
|
private state: UpdateStatus;
|
|
44
50
|
private checking: Promise<UpdateStatus> | null = null;
|
|
45
|
-
private operation: Promise<void> | null = null;
|
|
46
51
|
private prepared = '';
|
|
47
52
|
private inFlight = 0;
|
|
48
53
|
private timer?: NodeJS.Timeout;
|
|
@@ -50,10 +55,12 @@ export class Updates {
|
|
|
50
55
|
private file: string;
|
|
51
56
|
private lock: string;
|
|
52
57
|
private channel: UpdateChannel;
|
|
58
|
+
// The latest release needs a newer desktop installer.
|
|
59
|
+
private needsInstaller = false;
|
|
53
60
|
constructor(private options: Options) {
|
|
54
61
|
this.file = path.join(options.data, 'updates/status.json'); this.lock = path.join(options.data, 'updates/install.lock');
|
|
55
62
|
const preference = readJson(path.join(options.data, 'updates/channel.json'))?.channel;
|
|
56
|
-
this.channel = preference === 'dev' || preference === 'stable' ? preference : devRevision(options.version) ? 'dev' : 'stable';
|
|
63
|
+
this.channel = options.downloadUrl || options.desktop ? 'stable' : preference === 'dev' || preference === 'stable' ? preference : devRevision(options.version) ? 'dev' : 'stable';
|
|
57
64
|
let supported = options.supported ?? true;
|
|
58
65
|
if (options.supported === undefined) try { npmCli(); } catch { supported = false; }
|
|
59
66
|
this.state = { currentVersion: options.version, latestVersion: null, phase: 'idle', checkedAt: null, error: null, canUpdate: supported };
|
|
@@ -62,14 +69,18 @@ export class Updates {
|
|
|
62
69
|
if (saved && activePhases.has(saved.phase) && !alive(saved.pid)) this.change({ phase: 'failed', checkedAt: Date.now(), error: 'The update was interrupted. Your current installation is still available; retry the update.' });
|
|
63
70
|
}
|
|
64
71
|
snapshot(): UpdateStatus {
|
|
72
|
+
if (this.options.downloadUrl) return { currentVersion: this.options.version, latestVersion: null, phase: 'idle', checkedAt: null, error: null, canUpdate: false, channel: 'stable', downloadUrl: this.options.downloadUrl };
|
|
65
73
|
const saved = readJson(this.file);
|
|
66
74
|
if (saved && activePhases.has(this.state.phase) && ['updated', 'failed'].includes(saved.phase)) this.state = { ...this.state, ...saved, currentVersion: this.options.version };
|
|
67
|
-
const { currentVersion, latestVersion, phase, checkedAt, error, canUpdate } = this.state;
|
|
75
|
+
const { currentVersion, latestVersion, phase, checkedAt, error, canUpdate } = this.state, desktop = this.options.desktop;
|
|
76
|
+
if (desktop) return { currentVersion, latestVersion, phase, checkedAt, error, channel: this.channel, installerVersion: desktop.installer,
|
|
77
|
+
...(this.needsInstaller ? { canUpdate: false, downloadUrl: desktop.downloadUrl } : { canUpdate }) };
|
|
68
78
|
return { currentVersion, latestVersion, phase, checkedAt, error, canUpdate, channel: this.channel };
|
|
69
79
|
}
|
|
70
80
|
get restarting() { return this.snapshot().phase === 'restarting'; }
|
|
71
81
|
enter() { this.inFlight++; let released = false; return () => { if (!released) { released = true; this.inFlight--; } }; }
|
|
72
82
|
start() {
|
|
83
|
+
if (this.options.downloadUrl) return;
|
|
73
84
|
if (this.state.phase !== 'failed') void this.check();
|
|
74
85
|
this.timer = setInterval(() => {
|
|
75
86
|
if (this.state.phase === 'waiting') void this.activate();
|
|
@@ -82,6 +93,7 @@ export class Updates {
|
|
|
82
93
|
writeJson(this.file, { ...this.state, pid: process.pid });
|
|
83
94
|
}
|
|
84
95
|
setChannel(channel: unknown) {
|
|
96
|
+
if (this.options.downloadUrl || this.options.desktop) throw new Error('Download desktop updates from GitHub Releases.');
|
|
85
97
|
if (channel !== 'stable' && channel !== 'dev') throw new Error('Choose a valid update channel.');
|
|
86
98
|
if (activePhases.has(this.state.phase)) throw new Error('Wait for the current update to finish before changing channels.');
|
|
87
99
|
if (channel === this.channel) return this.check();
|
|
@@ -92,13 +104,19 @@ export class Updates {
|
|
|
92
104
|
return this.check();
|
|
93
105
|
}
|
|
94
106
|
check(): Promise<UpdateStatus> {
|
|
107
|
+
if (this.options.downloadUrl) return Promise.resolve(this.snapshot());
|
|
95
108
|
if (this.checking) return this.checking;
|
|
96
109
|
if (activePhases.has(this.state.phase)) return Promise.resolve(this.snapshot());
|
|
97
110
|
this.change({ phase: 'checking' });
|
|
98
111
|
const channel = this.channel;
|
|
99
|
-
const checking = (this.options.latest ||
|
|
112
|
+
const checking = (this.options.latest || latestManifest)(channel).then(found => {
|
|
113
|
+
const manifest = typeof found === 'string' ? { version: found } : found, version = manifest?.version;
|
|
100
114
|
if (!validTarget(version, channel)) throw new Error('Invalid release version.');
|
|
101
|
-
if (!this.closed && this.checking === checking && !activePhases.has(this.state.phase))
|
|
115
|
+
if (!this.closed && this.checking === checking && !activePhases.has(this.state.phase)) {
|
|
116
|
+
const newer = available(version, this.options.version, channel);
|
|
117
|
+
this.needsInstaller = Boolean(this.options.desktop && newer && !desktopSupports(manifest, this.options.desktop.shell));
|
|
118
|
+
this.change({ latestVersion: version, checkedAt: Date.now(), error: null, phase: newer ? 'available' : 'idle' });
|
|
119
|
+
}
|
|
102
120
|
return this.snapshot();
|
|
103
121
|
}).catch(() => {
|
|
104
122
|
if (!this.closed && this.checking === checking && !activePhases.has(this.state.phase)) this.change({ phase: this.state.latestVersion && available(this.state.latestVersion, this.options.version, channel) ? 'available' : 'idle', checkedAt: Date.now(), error: 'Could not check for updates. Try again when connected.' });
|
|
@@ -108,7 +126,9 @@ export class Updates {
|
|
|
108
126
|
return this.checking;
|
|
109
127
|
}
|
|
110
128
|
install(version: unknown) {
|
|
129
|
+
if (this.options.downloadUrl) throw new Error('Download desktop updates from GitHub Releases.');
|
|
111
130
|
if (activePhases.has(this.state.phase)) return this.snapshot();
|
|
131
|
+
if (this.needsInstaller) throw new Error(NEEDS_INSTALLER);
|
|
112
132
|
if (!this.state.canUpdate) throw new Error('npm is unavailable to Papilot.');
|
|
113
133
|
if (!validTarget(version, this.channel) || version !== this.state.latestVersion || !available(version, this.options.version, this.channel)) throw new Error('Check for a newer release before updating.');
|
|
114
134
|
fs.mkdirSync(path.dirname(this.lock), { recursive: true, mode: 0o700 });
|
|
@@ -117,11 +137,11 @@ export class Updates {
|
|
|
117
137
|
if (previous) fs.rmSync(this.lock);
|
|
118
138
|
const fd = fs.openSync(this.lock, 'wx', 0o600); fs.writeFileSync(fd, JSON.stringify({ pid: process.pid })); fs.closeSync(fd);
|
|
119
139
|
this.change({ phase: 'installing', error: null });
|
|
120
|
-
|
|
140
|
+
void (this.options.install || installRelease)(this.options.data, version).then(root => {
|
|
121
141
|
if (this.closed) return;
|
|
122
142
|
this.prepared = root; this.change({ phase: 'waiting' });
|
|
123
143
|
return this.activate();
|
|
124
|
-
}).catch(error => { this.change({ phase: 'failed', error: error instanceof Error ? error.message : 'The update failed.' }); this.release(); })
|
|
144
|
+
}).catch(error => { this.change({ phase: 'failed', error: error instanceof Error ? error.message : 'The update failed.' }); this.release(); });
|
|
125
145
|
return this.snapshot();
|
|
126
146
|
}
|
|
127
147
|
// Public for deterministic tests; the periodic tick calls this in production.
|
package/src/server/workspace.ts
CHANGED
|
@@ -17,13 +17,14 @@ import { VERSION } from './paths.ts';
|
|
|
17
17
|
import { prepareRepository } from './imports.ts';
|
|
18
18
|
import { findSkill, type SkillFile } from './skills.ts';
|
|
19
19
|
import { buildFixPrompt, reviewInstructions, skillPrompt, summarizeFeedback } from '../conversation.ts';
|
|
20
|
+
import { reconciliationPrompt } from './reconcile.ts';
|
|
20
21
|
import { commitMessageFrom } from '../commit.ts';
|
|
21
22
|
import { taskCaptionFrom, taskCaptionInstructions } from '../task-caption.ts';
|
|
22
23
|
import { notesInstructions, reviewNotesInstructions, summarizeNotes } from '../notes.ts';
|
|
23
24
|
import { canonicalGroup, groupsWithoutInstructions, parseGroupInstructions, type GroupInstructions } from '../note-groups.ts';
|
|
24
25
|
import { commitChanges, conflictMarkers, gitCommit, gitRemote, gitStatus, mergeConflicts, mergeInProgress } from './manuscript.ts';
|
|
25
26
|
import { AGENT_IDS, isTaskReviewNote } from '../shared.ts';
|
|
26
|
-
import type { Task, Run, TimelineItem, PendingRequest, RuntimeStatus, Snapshot, PendingReview, AgentModel, Usage, AgentId, MessageMeta, FeedbackSummary, BuildFailure, Author, Annotation, NoteSummary } from '../shared.ts';
|
|
27
|
+
import type { Task, Run, TimelineItem, PendingRequest, RuntimeStatus, Snapshot, PendingReview, AgentModel, Usage, AgentId, MessageMeta, FeedbackSummary, BuildFailure, Author, Annotation, NoteSummary, TaskReconciliation, TaskCommitReview } from '../shared.ts';
|
|
27
28
|
|
|
28
29
|
const execute = promisify(execFile);
|
|
29
30
|
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -132,7 +133,7 @@ export class Workspace extends EventEmitter {
|
|
|
132
133
|
constructor(public store: Store, public codex: AgentRuntime = new Codex(), private concurrency = 2, private connectReview = reviewClient, others: Partial<Record<AgentId, AgentRuntime>> = {}) {
|
|
133
134
|
super();
|
|
134
135
|
this.attachments = new Attachments(store);
|
|
135
|
-
this.agents = { codex, claude: others.claude || new ClaudeAgent(), cursor: others.cursor || new CursorAgent(), antigravity: others.antigravity || new AntigravityAgent(),
|
|
136
|
+
this.agents = { codex, claude: others.claude || new ClaudeAgent(), cursor: others.cursor || new CursorAgent(), copilot: others.copilot || new AcpAgent(ACP_PROFILES.copilot), grok: others.grok || new AcpAgent(ACP_PROFILES.grok), antigravity: others.antigravity || new AntigravityAgent(),
|
|
136
137
|
kimi: others.kimi || new AcpAgent(ACP_PROFILES.kimi), glm: others.glm || new GlmAgent(), deepseek: others.deepseek || new AcpAgent(ACP_PROFILES.deepseek),
|
|
137
138
|
qwen: others.qwen || new AcpAgent(ACP_PROFILES.qwen), opencode: others.opencode || new AcpAgent(ACP_PROFILES.opencode) };
|
|
138
139
|
store.on('change', () => this.emit('change'));
|
|
@@ -328,7 +329,7 @@ export class Workspace extends EventEmitter {
|
|
|
328
329
|
// The author's message, a skill run with or without one, the review
|
|
329
330
|
// feedback alone, or a build the agent broke. The chat keeps what happened;
|
|
330
331
|
// the agent gets the prompt.
|
|
331
|
-
async enqueue(taskId: string, text: string, requestKey: string, includeFeedback = true, options: { attachments?: string[]; skill?: SkillFile; notes?: Annotation[]; groupInstructions?: GroupInstructions; reviewNotes?: NoteSummary[]; feedback?: boolean; build?: BuildFailure; author?: Author } = {}) {
|
|
332
|
+
async enqueue(taskId: string, text: string, requestKey: string, includeFeedback = true, options: { attachments?: string[]; skill?: SkillFile; notes?: Annotation[]; groupInstructions?: GroupInstructions; reviewNotes?: NoteSummary[]; feedback?: boolean; build?: BuildFailure; author?: Author; merge?: TaskReconciliation } = {}) {
|
|
332
333
|
const task = this.store.task(taskId);
|
|
333
334
|
if (task.status === 'deleting') throw new Error('This task is being deleted.');
|
|
334
335
|
if (task.mode === 'review' && ((options.skill && options.skill.output !== 'annotations') || options.notes?.length || options.reviewNotes?.length || options.attachments?.length || options.feedback || options.build)) throw new Error('AI review accepts review instructions only. Start an editing task to send notes, skills or attachments.');
|
|
@@ -342,7 +343,7 @@ export class Workspace extends EventEmitter {
|
|
|
342
343
|
const pauseVersion = task.pauseVersion || 0;
|
|
343
344
|
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.');
|
|
344
345
|
const instructions = options.notes?.length ? notesInstructions(options.notes, text, options.groupInstructions) : options.reviewNotes?.length ? reviewNotesInstructions(options.reviewNotes, text) : text.trim();
|
|
345
|
-
let prompt = options.build ? buildFixPrompt(options.build) : options.feedback ? reviewInstructions : options.skill ? skillPrompt(options.skill, instructions) : instructions, cursor = task.feedbackCursor;
|
|
346
|
+
let prompt = options.merge ? reconciliationPrompt(options.merge) : options.build ? buildFixPrompt(options.build) : options.feedback ? reviewInstructions : options.skill ? skillPrompt(options.skill, instructions) : instructions, cursor = task.feedbackCursor;
|
|
346
347
|
let summary: FeedbackSummary | undefined;
|
|
347
348
|
if (task.mode === 'review') includeFeedback = false;
|
|
348
349
|
if (includeFeedback) {
|
|
@@ -417,6 +418,7 @@ export class Workspace extends EventEmitter {
|
|
|
417
418
|
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.
|
|
418
419
|
Your task is already created. Your editable working directory is ${task.worktree}.
|
|
419
420
|
Original task: ${task.instructions}
|
|
421
|
+
Papilot offers two ways to save accepted changes: Apply to workspace leaves them uncommitted; Commit to manuscript creates one commit containing only this task on the current manuscript branch. Both update the manuscript directly, including for tasks previously saved to a task branch. Do not tell the author to export patches or run Git commands to apply a task. If application conflicts, Papilot automatically asks AI to prepare another review in this task. Ask the author only when the instructions and manuscript context cannot determine the intended content; continue after their answer.
|
|
420
422
|
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.
|
|
421
423
|
${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.
|
|
422
424
|
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.
|
|
@@ -449,13 +451,21 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
449
451
|
}
|
|
450
452
|
}
|
|
451
453
|
if (this.active.get(task.id)?.id !== run.id) return;
|
|
452
|
-
|
|
454
|
+
// Settings can change while the agent loads. Keep this turn's snapshot,
|
|
455
|
+
// and never overwrite a newer choice with the thread's resolved model.
|
|
456
|
+
const current = this.store.task(task.id);
|
|
457
|
+
this.store.updateTask(task.id, { threadId: result.thread.id, ...(current.model === task.model ? { model: result.model || task.model } : {}), ...(task.threadId ? {} : { viewPdf }) });
|
|
458
|
+
task = { ...task, threadId: result.thread.id, model: result.model || task.model };
|
|
453
459
|
this.loaded.add(result.thread.id);
|
|
454
460
|
}
|
|
455
461
|
this.store.updateTask(task.id, { pendingRunId: run.id });
|
|
456
462
|
const input = [{ type: 'text', text: run.prompt }, ...(run.attachments || []).filter(file => file.mime.startsWith('image/')).map(file => ({ type: 'localImage', path: this.attachments.path(file) }))];
|
|
457
463
|
let result: any;
|
|
458
|
-
|
|
464
|
+
const models = this.runtimes[this.agentOf(task)].models;
|
|
465
|
+
const model = task.model ? models.find(model => model.id === task.model) : models.find(model => model.isDefault);
|
|
466
|
+
// Explicit defaults replace a previous turn's reasoning override.
|
|
467
|
+
const effort = task.effort || (this.agentOf(task) === 'codex' ? model?.defaultEffort : undefined);
|
|
468
|
+
try { result = await agent.request('turn/start', { threadId: task.threadId, input, cwd: task.worktree, clientUserMessageId: run.id, ...(task.model ? { model: task.model } : {}), ...(effort ? { effort } : {}), ...(this.agentOf(task) === 'codex' && task.serviceTier ? { serviceTier: task.serviceTier } : {}) }); }
|
|
459
469
|
catch (error) {
|
|
460
470
|
if (this.active.get(task.id)?.id !== run.id) return;
|
|
461
471
|
if (error instanceof RpcResponseError) throw error;
|
|
@@ -811,9 +821,16 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
811
821
|
const { unsent, comments } = await this.unsentReviewFeedback(client, this.store.task(taskId));
|
|
812
822
|
const deferred = summary.deferred === true || task.deferred;
|
|
813
823
|
const delivery = summary.delivery ?? task.delivery;
|
|
814
|
-
const
|
|
824
|
+
const reconciliation = summary.reconciliation ?? null;
|
|
825
|
+
const files = await acceptedFiles(project.path, task.name, deferred ? reconciliation?.base ?? (delivery && delivery.mode !== 'branch' ? delivery.accepted : undefined) : task.committed?.accepted);
|
|
815
826
|
const current = this.store.task(taskId);
|
|
816
|
-
if (current.status !== 'deleting') this.store.updateTask(taskId, { deferred, delivery, review: { state: summary.state, round: summary.round, counts: summary.counts, job: current.review?.job, unsent, comments, files } });
|
|
827
|
+
if (current.status !== 'deleting') this.store.updateTask(taskId, { deferred, delivery, reconciliation, review: { state: summary.state, round: summary.round, counts: summary.counts, job: current.review?.job, unsent, comments, files } });
|
|
828
|
+
// Recover an application interrupted after the engine prepared its
|
|
829
|
+
// proposal but before Papilot persisted the agent run.
|
|
830
|
+
if (reconciliation?.ready && (summary.round ?? 0) <= reconciliation.round && current.status === 'reviewing' && !this.committing.has(task.projectId) && !this.reviewWrites.has(taskId)
|
|
831
|
+
&& !current.pendingReview && !this.store.runByKey('merge-review-' + reconciliation.id)
|
|
832
|
+
&& !this.store.runs().some(run => run.taskId === taskId && run.status === 'queued'))
|
|
833
|
+
await this.queueMergeSuggestion(taskId, reconciliation);
|
|
817
834
|
} catch { /* The task remains visible even if its project is temporarily unavailable. */ }
|
|
818
835
|
finally { release?.(); }
|
|
819
836
|
}
|
|
@@ -951,7 +968,7 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
951
968
|
try {
|
|
952
969
|
const task = this.store.task(taskId), files = task.review?.files || [];
|
|
953
970
|
if (task.status !== 'reviewing' || task.review?.counts?.pending || !files.length) return;
|
|
954
|
-
if (task.deferred)
|
|
971
|
+
if (task.deferred) return;
|
|
955
972
|
const project = this.store.project(task.projectId), git = await gitStatus(project.path);
|
|
956
973
|
const paths = git.merging ? [] : files.filter(file => git.files.some(item => item.path === file));
|
|
957
974
|
if (paths.length) await this.commitMessage(project.id, paths, { agent: this.agentOf(task) });
|
|
@@ -959,9 +976,10 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
959
976
|
}
|
|
960
977
|
private async taskCommitChanges(task: Task) {
|
|
961
978
|
const root = this.store.project(task.projectId).path;
|
|
962
|
-
const
|
|
979
|
+
const delivered = task.reconciliation?.base ?? (task.delivery?.mode !== 'branch' ? task.delivery?.accepted : undefined);
|
|
980
|
+
const before = delivered ?? `refs/latex-review/${task.name}/base`;
|
|
963
981
|
const after = `refs/latex-review/${task.name}/accepted`;
|
|
964
|
-
const paths = await acceptedFiles(root, task.name,
|
|
982
|
+
const paths = await acceptedFiles(root, task.name, delivered);
|
|
965
983
|
if (!paths.length) throw new Error('This task has no accepted changes left to commit.');
|
|
966
984
|
const { stdout: patch } = await execute('git', ['-C', root, 'diff', '--no-ext-diff', '--no-textconv', before, after, '--'], { maxBuffer: 8 * 1024 * 1024 });
|
|
967
985
|
return { files: paths.map(path => ({ path, status: 'M', from: undefined })), patch: patch.slice(0, 80000), recent: [] as string[] };
|
|
@@ -982,6 +1000,38 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
982
1000
|
} finally { await this.refresh(taskId); this.reviewWrites.delete(taskId); this.schedule(); }
|
|
983
1001
|
});
|
|
984
1002
|
}
|
|
1003
|
+
async applyTask(taskId: string, keepCurrent = false, commit?: { message: string; confirmation?: string }) {
|
|
1004
|
+
const task = this.store.task(taskId), project = this.store.project(task.projectId);
|
|
1005
|
+
if (task.mode === 'review') throw new Error('AI review has no manuscript changes to apply.');
|
|
1006
|
+
if (!task.deferred) throw new Error('Accepted changes are already in the manuscript.');
|
|
1007
|
+
if (task.status !== 'reviewing' || this.active.has(taskId) || this.publishing.has(taskId) || task.pendingReview || this.store.runs().some(run => run.taskId === taskId && run.status === 'queued'))
|
|
1008
|
+
throw new Error('Wait for the task and review actions to finish before applying changes.');
|
|
1009
|
+
const result = await this.exclusiveCommit(project.id, async () => {
|
|
1010
|
+
this.reviewWrites.add(taskId);
|
|
1011
|
+
try {
|
|
1012
|
+
const client = await this.connectReview(project.path);
|
|
1013
|
+
const result = await client.post<{ delivery?: NonNullable<Task['delivery']>; reconciliation?: TaskReconciliation; commitReview?: TaskCommitReview }>(`${taskRoute(task.name)}/apply`, { keepCurrent, mode: commit ? 'commit' : 'workspace', ...(commit ? { message: commit.message, confirmation: commit.confirmation } : {}) });
|
|
1014
|
+
if (result.delivery) this.store.updateTask(taskId, { delivery: result.delivery, reconciliation: null });
|
|
1015
|
+
if (result.reconciliation) this.store.updateTask(taskId, { reconciliation: result.reconciliation });
|
|
1016
|
+
return { ...result, git: await gitStatus(project.path) };
|
|
1017
|
+
} finally { await this.refresh(taskId); this.reviewWrites.delete(taskId); }
|
|
1018
|
+
}).catch(async error => {
|
|
1019
|
+
// A lost response is not a failed application. refresh above checks the
|
|
1020
|
+
// engine's persisted result before we retry or start the suggestion.
|
|
1021
|
+
const confirmed = this.store.task(taskId);
|
|
1022
|
+
if (confirmed.reconciliation?.ready && confirmed.reconciliation.id !== task.reconciliation?.id)
|
|
1023
|
+
return { reconciliation: confirmed.reconciliation, delivery: undefined, commitReview: undefined, git: await gitStatus(project.path) };
|
|
1024
|
+
if (confirmed.delivery && confirmed.delivery.mode !== 'branch' && !confirmed.delivery.pending && (confirmed.delivery.mode !== task.delivery?.mode || confirmed.delivery.accepted !== task.delivery?.accepted))
|
|
1025
|
+
return { delivery: confirmed.delivery, reconciliation: undefined, commitReview: undefined, git: await gitStatus(project.path) };
|
|
1026
|
+
throw error;
|
|
1027
|
+
});
|
|
1028
|
+
if (result.reconciliation?.ready) await this.queueMergeSuggestion(taskId, result.reconciliation);
|
|
1029
|
+
this.schedule();
|
|
1030
|
+
return result;
|
|
1031
|
+
}
|
|
1032
|
+
private queueMergeSuggestion(taskId: string, merge: TaskReconciliation) {
|
|
1033
|
+
return this.enqueue(taskId, 'The manuscript has changed in overlapping passages. Prepare a merge suggestion for me to review; keep the manuscript unchanged.', 'merge-review-' + merge.id, false, { merge });
|
|
1034
|
+
}
|
|
985
1035
|
// Resolves a merge's conflicts in the manuscript checkout itself: the agent
|
|
986
1036
|
// edits only the conflicted files, at the model's default reasoning, and
|
|
987
1037
|
// leaves committing the merge to the author once they have read the diffs.
|
|
@@ -1006,10 +1056,9 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
1006
1056
|
async commitTask(taskId: string, message: string, push = false) {
|
|
1007
1057
|
const task = this.store.task(taskId);
|
|
1008
1058
|
if (task.deferred) {
|
|
1009
|
-
if (push) throw new Error('
|
|
1010
|
-
const result = await this.
|
|
1011
|
-
|
|
1012
|
-
return { ...result, commit: result.commit, pushError: undefined as string | undefined };
|
|
1059
|
+
if (push) throw new Error('Commit the task first, then push from Git.');
|
|
1060
|
+
const result = await this.applyTask(taskId, false, { message });
|
|
1061
|
+
return { ...result, commit: result.delivery?.commit, pushError: undefined };
|
|
1013
1062
|
}
|
|
1014
1063
|
if (task.status === 'deleting') throw new Error('This task is being deleted.');
|
|
1015
1064
|
if (task.pendingReview) throw new Error('Wait for the current review action before committing.');
|
|
@@ -1040,6 +1089,20 @@ ${this.agentOf(task) === 'antigravity' ? 'Shell commands are not available to yo
|
|
|
1040
1089
|
this.committing.add(projectId);
|
|
1041
1090
|
try { return await work(); } finally { this.committing.delete(projectId); }
|
|
1042
1091
|
}
|
|
1092
|
+
updateTaskModel(taskId: string, change: { model?: string; effort?: string; serviceTier?: string }) {
|
|
1093
|
+
const task = this.store.task(taskId), agent = this.agentOf(task);
|
|
1094
|
+
if (task.status === 'deleting') throw new Error('This task is being deleted.');
|
|
1095
|
+
const models = this.runtimes[agent].models;
|
|
1096
|
+
const id = change.model ?? task.model;
|
|
1097
|
+
const model = id ? models.find(model => model.id === id) : models.find(model => model.isDefault);
|
|
1098
|
+
if (change.model && change.model !== task.model && !model) throw new Error('Choose an available model for this agent.');
|
|
1099
|
+
const effort = change.effort ?? (change.model !== undefined && model && !model.efforts.includes(task.effort || '') ? '' : task.effort);
|
|
1100
|
+
if (change.effort && model && !model.efforts.includes(change.effort)) throw new Error('This model does not support that reasoning effort.');
|
|
1101
|
+
const serviceTier = change.serviceTier ?? (change.model !== undefined && task.serviceTier !== model?.fastMode?.serviceTier ? 'default' : task.serviceTier);
|
|
1102
|
+
if (change.serviceTier !== undefined && agent !== 'codex') throw new Error('Service tiers are only available for Codex.');
|
|
1103
|
+
if (serviceTier && serviceTier !== 'default' && serviceTier !== model?.fastMode?.serviceTier) throw new Error('Fast mode is not available for this model.');
|
|
1104
|
+
return this.store.updateTask(taskId, { model: id || null, effort: effort || null, ...(serviceTier ? { serviceTier } : {}) });
|
|
1105
|
+
}
|
|
1043
1106
|
renameTask(taskId: string, title: string) {
|
|
1044
1107
|
const task = this.store.task(taskId);
|
|
1045
1108
|
if (task.status === 'deleting') throw new Error('This task is being deleted.');
|
package/src/shared.ts
CHANGED
|
@@ -9,6 +9,10 @@ export const MAX_ATTACHMENT_TOTAL = 100 * 1024 * 1024;
|
|
|
9
9
|
export interface Attachment { id: string; projectId: string; name: string; size: number; mime: string }
|
|
10
10
|
export type UpdateChannel = 'stable' | 'dev';
|
|
11
11
|
export interface UpdateStatus {
|
|
12
|
+
// Present when the update must be downloaded as a new desktop app.
|
|
13
|
+
downloadUrl?: string;
|
|
14
|
+
// The desktop app's own Papilot version; newer releases run on top of it.
|
|
15
|
+
installerVersion?: string;
|
|
12
16
|
channel?: UpdateChannel;
|
|
13
17
|
currentVersion: string; latestVersion: string | null;
|
|
14
18
|
phase: 'idle' | 'checking' | 'available' | 'installing' | 'waiting' | 'restarting' | 'updated' | 'failed';
|
|
@@ -19,12 +23,19 @@ export interface UpdateStatus {
|
|
|
19
23
|
// accepted changes touched since its last commit, relative to the repository root.
|
|
20
24
|
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 }
|
|
21
25
|
export interface PendingReview { route: string; body: Record<string, unknown>; previousStatus: TaskStatus }
|
|
26
|
+
export interface TaskCommitReview { token: string; files: string[]; diff: string }
|
|
27
|
+
export interface TaskReconciliation {
|
|
28
|
+
id: string; round: number; files: string[];
|
|
29
|
+
sourceBase: string; sourceAccepted: string; base: string; ready: boolean;
|
|
30
|
+
intent?: { mode: 'workspace' | 'commit'; message: string };
|
|
31
|
+
}
|
|
22
32
|
export interface Task {
|
|
23
33
|
mode?: 'edit' | 'review';
|
|
24
34
|
aiReview?: { key: string; notes?: number; revision?: number };
|
|
25
|
-
//
|
|
35
|
+
// Accepted changes remain isolated until the author applies them.
|
|
36
|
+
reconciliation?: TaskReconciliation | null;
|
|
26
37
|
deferred?: boolean;
|
|
27
|
-
delivery?: { mode: 'workspace' | 'branch'; accepted: string; branch?: string; commit?: string; pending?: boolean } | null;
|
|
38
|
+
delivery?: { mode: 'workspace' | 'commit' | 'branch'; accepted: string; branch?: string; commit?: string; pending?: boolean } | null;
|
|
28
39
|
id: string; projectId: string; name: string; title: string; instructions: string;
|
|
29
40
|
titleSource?: 'prompt' | 'generated' | 'manual'; titleRevision?: number; titleGeneration?: 'pending' | 'failed' | null;
|
|
30
41
|
worktree: string; threadId: string | null; turnId: string | null; agent?: AgentId; model: string | null; effort?: string | null; serviceTier?: string;
|
|
@@ -38,18 +49,18 @@ export interface Task {
|
|
|
38
49
|
// The task's last commit from its review, and the accepted text it included.
|
|
39
50
|
committed?: { hash: string; accepted: string; at: number; pushed?: boolean } | null;
|
|
40
51
|
}
|
|
41
|
-
// A task the author is through with,
|
|
42
|
-
//
|
|
52
|
+
// A task the author is through with, so the saved result can offer archiving:
|
|
53
|
+
// every change of its last result is decided, no comment or rejection
|
|
43
54
|
// waits to go back to the agent, and what was accepted has left the task
|
|
44
55
|
// (applied or committed; older tasks accept straight into the working
|
|
45
56
|
// directory). An answer without manuscript changes is finished too.
|
|
46
57
|
export function taskFinished(task: Task) {
|
|
47
58
|
const review = task.review;
|
|
48
|
-
if (task.status !== 'reviewing' || !review || task.pendingReview || task.delivery?.pending || review.counts?.pending || review.state === 'build_failed') return false;
|
|
59
|
+
if (task.status !== 'reviewing' || !review || task.reconciliation || task.pendingReview || task.delivery?.pending || review.counts?.pending || review.state === 'build_failed') return false;
|
|
49
60
|
const unsent = review.unsent ?? ['accepted', 'rejected', 'commented'].reduce((count, kind) => count + (review.counts?.[kind] || 0), 0);
|
|
50
61
|
if ((review.comments ?? unsent) > 0 || (unsent > 0 && !review.counts?.accepted)) return false;
|
|
51
62
|
if (review.round === null) return true;
|
|
52
|
-
return review.state === 'done' && (!task.deferred || review.files?.length === 0);
|
|
63
|
+
return review.state === 'done' && (!task.deferred || (review.files?.length === 0 && task.delivery?.mode !== 'branch'));
|
|
53
64
|
}
|
|
54
65
|
// reviewRound is 0 while awaiting a result, null when no changes were produced,
|
|
55
66
|
// or the published round. Older runs omit it.
|
|
@@ -70,10 +81,10 @@ export interface BuildFailure { error: string; details: string[]; log?: string }
|
|
|
70
81
|
export interface FeedbackSummary { round: number | null; accepted: number; rejected: number; undone: number; comments: { where: string; text: string }[]; buildError?: boolean }
|
|
71
82
|
export interface Skill { output?: 'annotations'; id: string; name: string; description: string; hint: string; icon?: SkillIcon; source: 'builtin' | 'user' | 'paper' }
|
|
72
83
|
export interface PendingRequest { id: string; taskId: string; method: string; params: Record<string, any>; createdAt: number }
|
|
73
|
-
export type AgentId = 'codex' | 'claude' | 'antigravity' | 'kimi' | 'cursor' | 'opencode' | 'qwen' | 'glm' | 'deepseek';
|
|
84
|
+
export type AgentId = 'codex' | 'claude' | 'antigravity' | 'kimi' | 'cursor' | 'copilot' | 'grok' | 'opencode' | 'qwen' | 'glm' | 'deepseek';
|
|
74
85
|
// Every agent Papilot can drive, in menu order. Only the ones found on the host are offered.
|
|
75
|
-
export const AGENT_IDS: AgentId[] = ['codex', 'claude', 'cursor', 'antigravity', 'kimi', 'glm', 'deepseek', 'qwen', 'opencode'];
|
|
76
|
-
export const AGENT_NAMES: Record<AgentId, string> = { codex: 'Codex', claude: 'Claude', cursor: 'Cursor', antigravity: 'Antigravity', kimi: 'Kimi', glm: 'GLM', deepseek: 'DeepSeek', qwen: 'Qwen', opencode: 'OpenCode' };
|
|
86
|
+
export const AGENT_IDS: AgentId[] = ['codex', 'claude', 'cursor', 'copilot', 'grok', 'antigravity', 'kimi', 'glm', 'deepseek', 'qwen', 'opencode'];
|
|
87
|
+
export const AGENT_NAMES: Record<AgentId, string> = { codex: 'Codex', claude: 'Claude', cursor: 'Cursor', copilot: 'Copilot', grok: 'Grok', antigravity: 'Antigravity', kimi: 'Kimi', glm: 'GLM', deepseek: 'DeepSeek', qwen: 'Qwen', opencode: 'OpenCode' };
|
|
77
88
|
export const isAgentId = (value: unknown): value is AgentId => AGENT_IDS.includes(value as AgentId);
|
|
78
89
|
export interface AgentModel { id: string; name: string; isDefault: boolean; efforts: string[]; defaultEffort: string | null; fastMode?: { serviceTier: string; description: string } }
|
|
79
90
|
// The account's included usage: rolling windows (minutes long, reset time in ms) and credits.
|
package/src/skill-metadata.ts
CHANGED
|
@@ -40,6 +40,6 @@ export function setSkillMetadata(content: string, field: 'name' | 'description'
|
|
|
40
40
|
return `${bom}---${newline}${next.join(newline)}${newline}---${closingNewline}${source.slice(header[0].length)}`;
|
|
41
41
|
}
|
|
42
42
|
export function setSkillInstructions(content: string, body: string) {
|
|
43
|
-
const { bom,
|
|
43
|
+
const { bom, header, newline } = parts(content);
|
|
44
44
|
return header ? `${bom}${header[0]}${header[0].endsWith('\n') ? '' : newline}${body}` : `${bom}${body}`;
|
|
45
45
|
}
|