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.
Files changed (39) hide show
  1. package/README.md +23 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/src/content/assetTransferBroker.js +169 -0
  4. package/extension/src/content/contentRuntime.js +28 -1
  5. package/extension/src/content/generated/content.bundle.js +630 -68
  6. package/extension/src/content/generated/content.bundle.meta.json +6 -3
  7. package/extension/src/content/legacyGlobalRegistry.js +3 -0
  8. package/extension/src/content/markdownDomRenderer.js +70 -0
  9. package/extension/src/content/modelPicker.js +2 -2
  10. package/extension/src/content/modelPickerSupport.js +3 -3
  11. package/extension/src/content/pageBridgeClient.js +1 -0
  12. package/extension/src/content/pendingInputView.js +16 -3
  13. package/extension/src/content/providerSettingsDialog.js +1 -0
  14. package/extension/src/content/runResultActions.js +114 -0
  15. package/extension/src/content/runTimelineView.js +20 -0
  16. package/extension/src/content/sessionForkController.js +120 -0
  17. package/extension/src/content/writebackController.js +3 -0
  18. package/extension/src/content/writebackOrchestrator.js +3 -1
  19. package/extension/src/page/binaryAssetUploader.js +343 -0
  20. package/extension/src/page/trackedChangesLifecycle.js +2 -0
  21. package/extension/src/pageBridge.js +30 -1
  22. package/extension/src/shared/compatibility.js +4 -2
  23. package/extension/src/shared/i18n.js +16 -0
  24. package/extension/src/shared/models.js +13 -2
  25. package/extension/src/shared/pageRpcContract.js +5 -1
  26. package/extension/src/shared/sessionState.js +12 -4
  27. package/extension/src/shared/storageDb.js +1 -1
  28. package/extension/src/shared/storageRunActions.js +11 -1
  29. package/extension/styles/panel.css +232 -31
  30. package/native-host/src/codexModels.js +6 -2
  31. package/native-host/src/codexRuntimeIdentity.js +79 -4
  32. package/native-host/src/codexSessionRunner.js +5 -1
  33. package/native-host/src/codexThreadFork.js +103 -0
  34. package/native-host/src/mirrorWorkspace.js +5 -19
  35. package/native-host/src/nativeAssetTransfer.js +183 -0
  36. package/native-host/src/nativeEnvironment.js +4 -1
  37. package/native-host/src/taskRunnerRuntime.js +22 -0
  38. package/native-host/src/textPatch.js +707 -259
  39. 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
- if (stat.size > SAFE_INLINE_BINARY_CHANGE_BYTES) {
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
- bytes: fs.readFileSync(target),
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
- contentBase64: contentOrBinary.bytes.toString('base64'),
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
  }