codex-overleaf-link 2.3.1 → 2.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -23
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/src/content/assetTransferBroker.js +169 -0
- package/extension/src/content/contentRuntime.js +28 -1
- package/extension/src/content/generated/content.bundle.js +630 -68
- package/extension/src/content/generated/content.bundle.meta.json +6 -3
- package/extension/src/content/legacyGlobalRegistry.js +3 -0
- package/extension/src/content/markdownDomRenderer.js +70 -0
- package/extension/src/content/modelPicker.js +2 -2
- package/extension/src/content/modelPickerSupport.js +3 -3
- package/extension/src/content/pageBridgeClient.js +1 -0
- package/extension/src/content/pendingInputView.js +16 -3
- package/extension/src/content/providerSettingsDialog.js +1 -0
- package/extension/src/content/runResultActions.js +114 -0
- package/extension/src/content/runTimelineView.js +20 -0
- package/extension/src/content/sessionForkController.js +120 -0
- package/extension/src/content/writebackController.js +3 -0
- package/extension/src/content/writebackOrchestrator.js +3 -1
- package/extension/src/page/binaryAssetUploader.js +343 -0
- package/extension/src/page/trackedChangesLifecycle.js +2 -0
- package/extension/src/pageBridge.js +30 -1
- package/extension/src/shared/compatibility.js +4 -2
- package/extension/src/shared/i18n.js +16 -0
- package/extension/src/shared/models.js +13 -2
- package/extension/src/shared/pageRpcContract.js +5 -1
- package/extension/src/shared/sessionState.js +12 -4
- package/extension/src/shared/storageDb.js +1 -1
- package/extension/src/shared/storageRunActions.js +11 -1
- package/extension/styles/panel.css +232 -31
- package/native-host/src/codexModels.js +6 -2
- package/native-host/src/codexRuntimeIdentity.js +79 -4
- package/native-host/src/codexSessionRunner.js +5 -1
- package/native-host/src/codexThreadFork.js +103 -0
- package/native-host/src/mirrorWorkspace.js +5 -19
- package/native-host/src/nativeAssetTransfer.js +183 -0
- package/native-host/src/nativeEnvironment.js +4 -1
- package/native-host/src/taskRunnerRuntime.js +22 -0
- package/native-host/src/textPatch.js +707 -259
- package/package.json +1 -1
|
@@ -152,34 +152,19 @@ async function collectMirrorChangesDetailed({ projectId, rootDir }) {
|
|
|
152
152
|
if (hashBytes(bytes) === previous.hash) {
|
|
153
153
|
continue;
|
|
154
154
|
}
|
|
155
|
-
if (stat.size > SAFE_INLINE_BINARY_CHANGE_BYTES) {
|
|
156
|
-
unsupportedChanges.push(buildOversizedBinaryPayloadChange({
|
|
157
|
-
filePath,
|
|
158
|
-
size: stat.size,
|
|
159
|
-
previous
|
|
160
|
-
}));
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
155
|
currentByPath.set(filePath, {
|
|
164
156
|
binary: true,
|
|
165
|
-
bytes,
|
|
157
|
+
sha256: hashBytes(bytes),
|
|
166
158
|
size: stat.size,
|
|
167
159
|
previous
|
|
168
160
|
});
|
|
169
161
|
continue;
|
|
170
162
|
}
|
|
171
163
|
if (!previous) {
|
|
172
|
-
|
|
173
|
-
unsupportedChanges.push(buildOversizedBinaryPayloadChange({
|
|
174
|
-
filePath,
|
|
175
|
-
size: stat.size,
|
|
176
|
-
previous
|
|
177
|
-
}));
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
164
|
+
const bytes = fs.readFileSync(target);
|
|
180
165
|
currentByPath.set(filePath, {
|
|
181
166
|
binary: true,
|
|
182
|
-
|
|
167
|
+
sha256: hashBytes(bytes),
|
|
183
168
|
size: stat.size,
|
|
184
169
|
previous
|
|
185
170
|
});
|
|
@@ -218,7 +203,8 @@ async function collectMirrorChangesDetailed({ projectId, rootDir }) {
|
|
|
218
203
|
changes.push({
|
|
219
204
|
type: previous ? 'overwrite-binary' : 'binary-create',
|
|
220
205
|
path: filePath,
|
|
221
|
-
|
|
206
|
+
assetSourcePath: filePath,
|
|
207
|
+
sha256: contentOrBinary.sha256,
|
|
222
208
|
previousExists: Boolean(previous),
|
|
223
209
|
previousKind: previous?.kind || '',
|
|
224
210
|
previousSize: previous?.size,
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { getProjectMirror } = require('./mirrorWorkspace');
|
|
7
|
+
|
|
8
|
+
const MAX_ASSET_BYTES = 10 * 1024 * 1024;
|
|
9
|
+
const ASSET_CHUNK_BYTES = 192 * 1024;
|
|
10
|
+
const ASSET_TOKEN_TTL_MS = 15 * 60 * 1000;
|
|
11
|
+
const transfers = new Map();
|
|
12
|
+
|
|
13
|
+
function prepareBinaryAssetChanges({ projectId, rootDir, changes = [] } = {}) {
|
|
14
|
+
const prepared = [];
|
|
15
|
+
const unsupportedChanges = [];
|
|
16
|
+
for (const change of changes) {
|
|
17
|
+
if (change?.type !== 'binary-create' && change?.type !== 'overwrite-binary') {
|
|
18
|
+
prepared.push(change);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
prepared.push(registerAssetChange({ projectId, rootDir, change }));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
unsupportedChanges.push({
|
|
25
|
+
type: 'unsupported-local-file',
|
|
26
|
+
path: change?.path || '',
|
|
27
|
+
reason: error.code || 'binary_asset_registration_failed',
|
|
28
|
+
size: change?.size,
|
|
29
|
+
attemptedChangeType: change?.type || 'binary-create'
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { changes: prepared, unsupportedChanges };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function registerAssetChange({ projectId, rootDir, change }) {
|
|
37
|
+
cleanupExpiredTransfers();
|
|
38
|
+
const mirror = getProjectMirror(projectId, { rootDir });
|
|
39
|
+
const relativePath = normalizeProjectPath(change.assetSourcePath || change.path);
|
|
40
|
+
const target = resolveSafeWorkspaceFile(mirror.workspacePath, relativePath);
|
|
41
|
+
const stat = fs.statSync(target);
|
|
42
|
+
if (!stat.isFile()) throw codedError('binary_asset_source_invalid', 'Asset source is not a file.');
|
|
43
|
+
if (stat.size > MAX_ASSET_BYTES) throw codedError('binary_file_too_large', 'Asset exceeds the 10 MiB writeback limit.');
|
|
44
|
+
const sha256 = hashFile(target);
|
|
45
|
+
if (change.sha256 && change.sha256 !== sha256) {
|
|
46
|
+
throw codedError('binary_asset_source_changed', 'Asset changed before transfer registration.');
|
|
47
|
+
}
|
|
48
|
+
const token = crypto.randomUUID();
|
|
49
|
+
const expiresAtMs = Date.now() + ASSET_TOKEN_TTL_MS;
|
|
50
|
+
transfers.set(token, {
|
|
51
|
+
token,
|
|
52
|
+
path: relativePath,
|
|
53
|
+
target,
|
|
54
|
+
size: stat.size,
|
|
55
|
+
sha256,
|
|
56
|
+
expiresAtMs
|
|
57
|
+
});
|
|
58
|
+
const { contentBase64, assetSourcePath, ...safeChange } = change;
|
|
59
|
+
return {
|
|
60
|
+
...safeChange,
|
|
61
|
+
size: stat.size,
|
|
62
|
+
assetRef: {
|
|
63
|
+
token,
|
|
64
|
+
path: relativePath,
|
|
65
|
+
size: stat.size,
|
|
66
|
+
sha256,
|
|
67
|
+
mimeType: inferMimeType(relativePath),
|
|
68
|
+
chunkSize: ASSET_CHUNK_BYTES,
|
|
69
|
+
expiresAt: new Date(expiresAtMs).toISOString()
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readAssetChunk(params = {}) {
|
|
75
|
+
cleanupExpiredTransfers();
|
|
76
|
+
const transfer = getTransfer(params.token);
|
|
77
|
+
const offset = Number(params.offset || 0);
|
|
78
|
+
const requestedLength = Number(params.length || ASSET_CHUNK_BYTES);
|
|
79
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > transfer.size) {
|
|
80
|
+
throw codedError('asset_chunk_offset_invalid', 'Asset chunk offset is invalid.');
|
|
81
|
+
}
|
|
82
|
+
const length = Math.min(
|
|
83
|
+
ASSET_CHUNK_BYTES,
|
|
84
|
+
Number.isSafeInteger(requestedLength) && requestedLength > 0 ? requestedLength : ASSET_CHUNK_BYTES,
|
|
85
|
+
transfer.size - offset
|
|
86
|
+
);
|
|
87
|
+
assertSourceStable(transfer);
|
|
88
|
+
const handle = fs.openSync(transfer.target, 'r');
|
|
89
|
+
try {
|
|
90
|
+
const buffer = Buffer.alloc(length);
|
|
91
|
+
const bytesRead = length ? fs.readSync(handle, buffer, 0, length, offset) : 0;
|
|
92
|
+
const nextOffset = offset + bytesRead;
|
|
93
|
+
return {
|
|
94
|
+
token: transfer.token,
|
|
95
|
+
path: transfer.path,
|
|
96
|
+
offset,
|
|
97
|
+
nextOffset,
|
|
98
|
+
size: transfer.size,
|
|
99
|
+
sha256: transfer.sha256,
|
|
100
|
+
contentBase64: buffer.subarray(0, bytesRead).toString('base64'),
|
|
101
|
+
eof: nextOffset >= transfer.size
|
|
102
|
+
};
|
|
103
|
+
} finally {
|
|
104
|
+
fs.closeSync(handle);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function releaseAsset(params = {}) {
|
|
109
|
+
return { released: transfers.delete(String(params.token || '')) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getTransfer(tokenValue) {
|
|
113
|
+
const token = String(tokenValue || '');
|
|
114
|
+
const transfer = transfers.get(token);
|
|
115
|
+
if (!transfer) throw codedError('asset_token_invalid', 'Asset transfer token is invalid or expired.');
|
|
116
|
+
if (transfer.expiresAtMs <= Date.now()) {
|
|
117
|
+
transfers.delete(token);
|
|
118
|
+
throw codedError('asset_token_expired', 'Asset transfer token expired.');
|
|
119
|
+
}
|
|
120
|
+
return transfer;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function assertSourceStable(transfer) {
|
|
124
|
+
let stat;
|
|
125
|
+
try { stat = fs.statSync(transfer.target); } catch (_error) {
|
|
126
|
+
throw codedError('binary_asset_source_missing', 'Asset source disappeared during transfer.');
|
|
127
|
+
}
|
|
128
|
+
if (!stat.isFile() || stat.size !== transfer.size) {
|
|
129
|
+
throw codedError('binary_asset_source_changed', 'Asset source changed during transfer.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveSafeWorkspaceFile(workspacePath, relativePath) {
|
|
134
|
+
const root = path.resolve(workspacePath);
|
|
135
|
+
const target = path.resolve(root, relativePath);
|
|
136
|
+
if (target === root || !target.startsWith(`${root}${path.sep}`)) {
|
|
137
|
+
throw codedError('binary_asset_path_unsafe', 'Asset path escapes the project workspace.');
|
|
138
|
+
}
|
|
139
|
+
if (fs.lstatSync(target).isSymbolicLink()) {
|
|
140
|
+
throw codedError('binary_asset_path_unsafe', 'Symbolic-link assets cannot be transferred.');
|
|
141
|
+
}
|
|
142
|
+
return target;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeProjectPath(value) {
|
|
146
|
+
const normalized = String(value || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
147
|
+
if (!normalized || normalized.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
148
|
+
throw codedError('binary_asset_path_invalid', 'Asset path is not a safe project-relative path.');
|
|
149
|
+
}
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hashFile(target) {
|
|
154
|
+
return crypto.createHash('sha256').update(fs.readFileSync(target)).digest('hex');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function inferMimeType(filePath) {
|
|
158
|
+
return {
|
|
159
|
+
'.pdf': 'application/pdf', '.png': 'image/png', '.jpg': 'image/jpeg',
|
|
160
|
+
'.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml',
|
|
161
|
+
'.webp': 'image/webp', '.eps': 'application/postscript'
|
|
162
|
+
}[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function cleanupExpiredTransfers() {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
for (const [token, transfer] of transfers) if (transfer.expiresAtMs <= now) transfers.delete(token);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function codedError(code, message) {
|
|
171
|
+
const error = new Error(message);
|
|
172
|
+
error.code = code;
|
|
173
|
+
return error;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
ASSET_CHUNK_BYTES,
|
|
178
|
+
ASSET_TOKEN_TTL_MS,
|
|
179
|
+
MAX_ASSET_BYTES,
|
|
180
|
+
prepareBinaryAssetChanges,
|
|
181
|
+
readAssetChunk,
|
|
182
|
+
releaseAsset
|
|
183
|
+
};
|
|
@@ -16,6 +16,7 @@ const SUPPORTED_PLATFORMS = new Set(['darwin', 'linux', 'win32']);
|
|
|
16
16
|
|
|
17
17
|
function buildNativeRuntimeEnv(baseEnv = process.env, options = {}) {
|
|
18
18
|
const env = { ...baseEnv };
|
|
19
|
+
const explicitCodexPath = String(baseEnv.CODEX_OVERLEAF_CODEX_PATH || '').trim();
|
|
19
20
|
const runtimeOptions = { ...options, platform: getNativeRuntimePlatform(options) };
|
|
20
21
|
const loginShellEnv = options.readLoginShellEnv
|
|
21
22
|
? options.readLoginShellEnv(baseEnv, runtimeOptions)
|
|
@@ -38,12 +39,14 @@ function buildNativeRuntimeEnv(baseEnv = process.env, options = {}) {
|
|
|
38
39
|
env[getToolEnvName(tool)] = resolveExecutable(tool, env.PATH, { ...runtimeOptions, env }) || '';
|
|
39
40
|
}
|
|
40
41
|
const codexRuntime = buildCodexRuntimeIdentity({
|
|
41
|
-
selectedPath: env.CODEX_OVERLEAF_CODEX_PATH,
|
|
42
|
+
selectedPath: explicitCodexPath || env.CODEX_OVERLEAF_CODEX_PATH,
|
|
43
|
+
selectionPolicy: explicitCodexPath ? 'explicit-path' : 'newest-version',
|
|
42
44
|
pathValue: env.PATH,
|
|
43
45
|
delimiter: runtimeOptions.delimiter,
|
|
44
46
|
env,
|
|
45
47
|
platform: runtimeOptions.platform
|
|
46
48
|
});
|
|
49
|
+
env.CODEX_OVERLEAF_CODEX_PATH = codexRuntime.selected?.path || '';
|
|
47
50
|
env.CODEX_OVERLEAF_CODEX_RUNTIME_JSON = serializeCodexRuntimeIdentity(codexRuntime);
|
|
48
51
|
env.CODEX_OVERLEAF_CODEX_VERSION = codexRuntime.selected?.version || '';
|
|
49
52
|
env.CODEX_OVERLEAF_CODEX_SOURCE = codexRuntime.selected?.source || '';
|
|
@@ -19,6 +19,8 @@ const {
|
|
|
19
19
|
resolveRunProviderForRun
|
|
20
20
|
} = require('./providerRuntime');
|
|
21
21
|
const { clearPluginCodexHistory } = require('./codexHome');
|
|
22
|
+
const { forkCodexThread } = require('./codexThreadFork');
|
|
23
|
+
const { readAssetChunk, releaseAsset } = require('./nativeAssetTransfer');
|
|
22
24
|
const { logDebug, truncateText } = require('./debugLog');
|
|
23
25
|
const { HOST_NAME } = require('./manifest');
|
|
24
26
|
const { getNativeRuntimePlatform, summarizeNativeEnvironment } = require('./nativeEnvironment');
|
|
@@ -116,6 +118,26 @@ async function handleRequest(request, env = process.env, emit = () => {}) {
|
|
|
116
118
|
return handleCodexHistoryClear(request, env);
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
if (request.method === 'codex.thread.fork') {
|
|
122
|
+
try {
|
|
123
|
+
return okResponse(request.id, await forkCodexThread(request.params || {}, env));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return errorResponse(request.id, error.code || 'codex_thread_fork_failed', error.message);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (request.method === 'asset.readChunk') {
|
|
130
|
+
try {
|
|
131
|
+
return okResponse(request.id, readAssetChunk(request.params || {}));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return errorResponse(request.id, error.code || 'asset_chunk_read_failed', error.message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (request.method === 'asset.release') {
|
|
138
|
+
return okResponse(request.id, releaseAsset(request.params || {}));
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
if (request.method === 'skills.list') {
|
|
120
142
|
return handleSkillsList(request, env);
|
|
121
143
|
}
|