codex-overleaf-link 1.1.3 → 1.2.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 +27 -27
- package/extension/src/shared/compatibility.js +1 -1
- package/extension/src/shared/lineReferences.js +404 -0
- package/extension/src/shared/sessionState.js +120 -23
- package/extension/src/shared/storageDb.js +59 -1
- package/native-host/src/codexPrompt.js +45 -5
- package/native-host/src/codexPromptAssembly.js +38 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<h1>Codex Overleaf Link</h1>
|
|
4
4
|
<p><strong>Empower Overleaf with Codex.</strong></p>
|
|
5
5
|
<p>
|
|
6
|
-
<img src="https://img.shields.io/badge/version-1.
|
|
6
|
+
<img src="https://img.shields.io/badge/version-1.2.0-blue" alt="version">
|
|
7
7
|
<img src="https://img.shields.io/badge/platform-macOS%20%2F%20Windows%20%2F%20Linux-lightgrey" alt="platform">
|
|
8
8
|
<img src="https://img.shields.io/badge/chrome-MV3-green" alt="chrome manifest v3">
|
|
9
9
|
<img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="node version">
|
|
@@ -34,10 +34,10 @@ The recommended release path is npm-first for the native host, plus Chrome's req
|
|
|
34
34
|
1. Install or update the native host with the pinned npm package:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
npm exec --yes codex-overleaf-link@1.
|
|
37
|
+
npm exec --yes codex-overleaf-link@1.2.0 -- install-native
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
2. Download `codex-overleaf-link-extension-v1.
|
|
40
|
+
2. Download `codex-overleaf-link-extension-v1.2.0.zip` from the [v1.2.0 GitHub Release](https://github.com/Ghqqqq/codex-overleaf-link/releases/tag/v1.2.0), then unzip it to a stable local folder.
|
|
41
41
|
|
|
42
42
|
3. Open `chrome://extensions`, enable **Developer mode**, click **Load unpacked**, and select the unzipped extension folder.
|
|
43
43
|
|
|
@@ -48,14 +48,14 @@ If you modify the extension key or load a custom build that gets a different id,
|
|
|
48
48
|
Source installer fallback for macOS / Linux, mainly for development or source checkout installs:
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
CODEX_OVERLEAF_REF=v1.
|
|
51
|
+
CODEX_OVERLEAF_REF=v1.2.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.sh)"
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Source installer fallback for Windows from PowerShell:
|
|
55
55
|
|
|
56
56
|
```powershell
|
|
57
|
-
iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.
|
|
58
|
-
$env:CODEX_OVERLEAF_REF='v1.
|
|
57
|
+
iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.ps1 -OutFile install.ps1
|
|
58
|
+
$env:CODEX_OVERLEAF_REF='v1.2.0'
|
|
59
59
|
powershell -ExecutionPolicy Bypass -File install.ps1
|
|
60
60
|
```
|
|
61
61
|
|
|
@@ -66,19 +66,19 @@ npm installs, updates, uninstalls, and diagnoses the native host only. npm does
|
|
|
66
66
|
Install or update the native host for the official release extension id:
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
npm exec --yes codex-overleaf-link@1.
|
|
69
|
+
npm exec --yes codex-overleaf-link@1.2.0 -- install-native
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
Diagnose the registered native host:
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
npm exec --yes codex-overleaf-link@1.
|
|
75
|
+
npm exec --yes codex-overleaf-link@1.2.0 -- doctor
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
Uninstall the native host:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
npm exec --yes codex-overleaf-link@1.
|
|
81
|
+
npm exec --yes codex-overleaf-link@1.2.0 -- uninstall-native
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
Use `--extension-id <chrome-extension-id>` only for a custom/dev unpacked extension id that differs from the official bundled id.
|
|
@@ -116,10 +116,10 @@ If Chrome assigns a different extension id, rerun `npm run install:native -- --e
|
|
|
116
116
|
<details>
|
|
117
117
|
<summary><strong>Update</strong></summary>
|
|
118
118
|
|
|
119
|
-
For a deterministic v1.
|
|
119
|
+
For a deterministic v1.2.0 update, run the pinned npm command. This is also the native mismatch recovery command shown by the popup and panel when they report **Native host update required**.
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
|
-
npm exec --yes codex-overleaf-link@1.
|
|
122
|
+
npm exec --yes codex-overleaf-link@1.2.0 -- install-native
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
If npm is unavailable, use the GitHub Release script fallback for your platform.
|
|
@@ -127,14 +127,14 @@ If npm is unavailable, use the GitHub Release script fallback for your platform.
|
|
|
127
127
|
macOS / Linux:
|
|
128
128
|
|
|
129
129
|
```bash
|
|
130
|
-
CODEX_OVERLEAF_REF=v1.
|
|
130
|
+
CODEX_OVERLEAF_REF=v1.2.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.sh)"
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
Windows PowerShell:
|
|
134
134
|
|
|
135
135
|
```powershell
|
|
136
|
-
iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.
|
|
137
|
-
$env:CODEX_OVERLEAF_REF='v1.
|
|
136
|
+
iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.ps1 -OutFile install.ps1
|
|
137
|
+
$env:CODEX_OVERLEAF_REF='v1.2.0'
|
|
138
138
|
powershell -ExecutionPolicy Bypass -File install.ps1
|
|
139
139
|
```
|
|
140
140
|
|
|
@@ -144,13 +144,13 @@ Then reload the extension in `chrome://extensions` and refresh the Overleaf page
|
|
|
144
144
|
|
|
145
145
|
## GitHub Release Artifacts
|
|
146
146
|
|
|
147
|
-
The v1.
|
|
147
|
+
The v1.2.0 GitHub Release contains:
|
|
148
148
|
|
|
149
|
-
- `codex-overleaf-link-extension-v1.
|
|
150
|
-
- `codex-overleaf-native-host-v1.
|
|
151
|
-
- `codex-overleaf-link-1.
|
|
152
|
-
- `install.sh`: release-pinned macOS / Linux installer that defaults to `v1.
|
|
153
|
-
- `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v1.
|
|
149
|
+
- `codex-overleaf-link-extension-v1.2.0.zip`: loadable Chrome extension package for manual unpacked installation.
|
|
150
|
+
- `codex-overleaf-native-host-v1.2.0.tar.gz`: native host runtime files used by the installer and release verification.
|
|
151
|
+
- `codex-overleaf-link-1.2.0.tgz`: npm native host CLI package for pinned install, doctor, and uninstall flows.
|
|
152
|
+
- `install.sh`: release-pinned macOS / Linux installer that defaults to `v1.2.0` when run directly from the release artifact.
|
|
153
|
+
- `install.ps1`: release-pinned Windows PowerShell installer that defaults to `v1.2.0` when run directly from the release artifact.
|
|
154
154
|
- `uninstall-native-host.mjs`: native host uninstaller that removes the Chrome Native Messaging manifest, bridge executable, and runtime copy.
|
|
155
155
|
- `nativeHostPlatform.js`, `manifest.js`, `runtimeInstaller.js`: helper files required by the loose uninstaller asset.
|
|
156
156
|
- `SHA256SUMS` and `release-manifest.json`: checksum and artifact metadata for release verification.
|
|
@@ -202,7 +202,7 @@ The uninstaller removes the Native Messaging registration, bridge executable, an
|
|
|
202
202
|
Linux Chromium install or update:
|
|
203
203
|
|
|
204
204
|
```bash
|
|
205
|
-
CODEX_OVERLEAF_REF=v1.
|
|
205
|
+
CODEX_OVERLEAF_REF=v1.2.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.sh)" -- --browser chromium
|
|
206
206
|
```
|
|
207
207
|
|
|
208
208
|
Linux Chromium uninstall:
|
|
@@ -344,7 +344,7 @@ Composer attachments are turn-scoped Codex context. Limits are 8 attachments per
|
|
|
344
344
|
Run the pinned npm native-host installer, reload the extension in `chrome://extensions`, then refresh the Overleaf tab. This also fixes extension/native version mismatch and native protocol mismatch.
|
|
345
345
|
|
|
346
346
|
```bash
|
|
347
|
-
npm exec --yes codex-overleaf-link@1.
|
|
347
|
+
npm exec --yes codex-overleaf-link@1.2.0 -- install-native
|
|
348
348
|
```
|
|
349
349
|
|
|
350
350
|
If npm is unavailable, use the GitHub Release script fallback for your platform.
|
|
@@ -352,14 +352,14 @@ If npm is unavailable, use the GitHub Release script fallback for your platform.
|
|
|
352
352
|
macOS/Linux:
|
|
353
353
|
|
|
354
354
|
```bash
|
|
355
|
-
CODEX_OVERLEAF_REF=v1.
|
|
355
|
+
CODEX_OVERLEAF_REF=v1.2.0 bash -c "$(curl -fsSL https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.sh)"
|
|
356
356
|
```
|
|
357
357
|
|
|
358
358
|
Windows PowerShell:
|
|
359
359
|
|
|
360
360
|
```powershell
|
|
361
|
-
iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.
|
|
362
|
-
$env:CODEX_OVERLEAF_REF='v1.
|
|
361
|
+
iwr https://raw.githubusercontent.com/Ghqqqq/codex-overleaf-link/v1.2.0/install.ps1 -OutFile install.ps1
|
|
362
|
+
$env:CODEX_OVERLEAF_REF='v1.2.0'
|
|
363
363
|
powershell -ExecutionPolicy Bypass -File install.ps1
|
|
364
364
|
```
|
|
365
365
|
|
|
@@ -418,8 +418,8 @@ Use this matrix for release-candidate signoff and compatibility reports. Record
|
|
|
418
418
|
| Browser/channel/version | Google Chrome channel and version. | Google Chrome channel and version. | Google Chrome channel and version. | Chromium channel/package and version. |
|
|
419
419
|
| Install mode | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout. | Manual unpacked extension from GitHub Release zip or checkout; native host installed with `--browser chromium`. |
|
|
420
420
|
| Extension id | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. | Bundled id `illdpneeeopfffmiepaejglgmhpmdhdc`, or actual custom id passed with `--extension-id`. |
|
|
421
|
-
| Installer/update command | `npm exec --yes codex-overleaf-link@1.
|
|
422
|
-
| Uninstall command | `npm exec --yes codex-overleaf-link@1.
|
|
421
|
+
| Installer/update command | `npm exec --yes codex-overleaf-link@1.2.0 -- install-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- install-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- install-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- install-native --browser chromium` |
|
|
422
|
+
| Uninstall command | `npm exec --yes codex-overleaf-link@1.2.0 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- uninstall-native` | `npm exec --yes codex-overleaf-link@1.2.0 -- uninstall-native --browser chromium` |
|
|
423
423
|
| Manifest/registry path | `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.codex.overleaf.json` | `HKCU\Software\Google\Chrome\NativeMessagingHosts\com.codex.overleaf` -> `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime\com.codex.overleaf.json` | `~/.config/google-chrome/NativeMessagingHosts/com.codex.overleaf.json` | `~/.config/chromium/NativeMessagingHosts/com.codex.overleaf.json` |
|
|
424
424
|
| Bridge/runtime/source path | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `%LOCALAPPDATA%\CodexOverleaf\codex-overleaf-bridge.cmd`; runtime `%LOCALAPPDATA%\CodexOverleaf\native-host-runtime`; source `%LOCALAPPDATA%\CodexOverleaf\source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. | Bridge `~/.codex-overleaf/codex-overleaf-bridge`; runtime `~/.codex-overleaf/native-host-runtime`; source `~/.codex-overleaf/source`. |
|
|
425
425
|
| Node/Git/Codex/TeX | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. | Node.js >= 20; Git; Codex CLI installed and logged in; TeX optional. |
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
const MIN_NATIVE_VERSION = '1.0.0';
|
|
13
13
|
const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
|
|
14
14
|
const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
|
|
15
|
-
const BUILD_TARGET_VERSION = '1.
|
|
15
|
+
const BUILD_TARGET_VERSION = '1.2.0';
|
|
16
16
|
const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
|
|
17
17
|
const REQUIRED_CAPABILITIES = Object.freeze([
|
|
18
18
|
'bridgePing',
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
(function initLineReferences(root, factory) {
|
|
2
|
+
if (typeof module === 'object' && module.exports) {
|
|
3
|
+
module.exports = factory();
|
|
4
|
+
} else {
|
|
5
|
+
root.CodexOverleafLineReferences = factory();
|
|
6
|
+
}
|
|
7
|
+
})(typeof globalThis !== 'undefined' ? globalThis : window, function lineReferencesFactory() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const VALID_PARSE_MODES = new Set([
|
|
11
|
+
'plain-text-token',
|
|
12
|
+
'markdown-link-label',
|
|
13
|
+
'markdown-link-target'
|
|
14
|
+
]);
|
|
15
|
+
const TEXT_EXTENSION_PATTERN = '(?:tex|bib|sty|cls|bst|bbx|cbx|lbx|cfg|def|clo|ist|txt|md|latex)';
|
|
16
|
+
const REFERENCE_PREFIX_PATTERN = '(^|[\\s\\[({"\'])';
|
|
17
|
+
const PATH_PATTERN = `([^\\s\\[\\](){}<>"'\`,;]+?\\.${TEXT_EXTENSION_PATTERN})`;
|
|
18
|
+
const MARKDOWN_LINK_PATTERN = /\[([^\]]*)\]\(([^)]*)\)/g;
|
|
19
|
+
const BARE_LOCAL_PATH_PATTERN = /(?:file:\/\/\/?[^\s)\]]+|[A-Za-z]:[\\/][^\s)\]]+|\/(?:Users|home|private|var|tmp)\/[^\s)\]]+|[^\s)\]]*[\\/]\.codex-overleaf[\\/]projects[\\/][^\s)\]]+)/gi;
|
|
20
|
+
|
|
21
|
+
function parseLineReferencesFromText({ text, mode }) {
|
|
22
|
+
return collectLineReferences(text, mode).map(toPublicReference);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveProjectReference({ rawPath, projectFiles }) {
|
|
26
|
+
const normalizedRawPath = normalizeReferencePath(rawPath);
|
|
27
|
+
if (!normalizedRawPath || hasUnsafePathSegments(normalizedRawPath)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const textFiles = collectTextProjectFiles(projectFiles);
|
|
32
|
+
const exactMatch = textFiles.find(file => file.normalizedPath === normalizedRawPath);
|
|
33
|
+
if (exactMatch) {
|
|
34
|
+
return buildResolvedReference(exactMatch, 'exact');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const suffixMatches = textFiles.filter(file => normalizedRawPath.endsWith(`/${file.normalizedPath}`));
|
|
38
|
+
if (suffixMatches.length === 1) {
|
|
39
|
+
return buildResolvedReference(suffixMatches[0], 'suffix');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const rawBasename = getBasename(normalizedRawPath);
|
|
43
|
+
const basenameMatches = textFiles.filter(file => getBasename(file.normalizedPath) === rawBasename);
|
|
44
|
+
if (basenameMatches.length === 1) {
|
|
45
|
+
return buildResolvedReference(basenameMatches[0], 'basename');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sanitizeLocalReferences(text, { projectFiles, context } = {}) {
|
|
52
|
+
if (typeof text !== 'string' || !text) {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let sanitized = text.replace(MARKDOWN_LINK_PATTERN, (rawMarkdown, label, target) => {
|
|
57
|
+
const sanitizedLabel = sanitizeLocalReferenceText(label, {
|
|
58
|
+
projectFiles,
|
|
59
|
+
placeholderBrackets: false
|
|
60
|
+
});
|
|
61
|
+
const trimmedTarget = String(target || '').trim();
|
|
62
|
+
if (/^https?:\/\//i.test(trimmedTarget)) {
|
|
63
|
+
return `[${sanitizedLabel}](${target})`;
|
|
64
|
+
}
|
|
65
|
+
if (containsLocalTarget(trimmedTarget)) {
|
|
66
|
+
return `[${sanitizedLabel}]`;
|
|
67
|
+
}
|
|
68
|
+
const sanitizedTarget = sanitizeLocalReferenceText(target, {
|
|
69
|
+
projectFiles,
|
|
70
|
+
placeholderBrackets: false
|
|
71
|
+
});
|
|
72
|
+
return `[${sanitizedLabel}](${sanitizedTarget})`;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
sanitized = sanitizeLocalReferenceText(sanitized, {
|
|
76
|
+
projectFiles,
|
|
77
|
+
placeholderBrackets: true
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (context === 'render' || context === 'persist') {
|
|
81
|
+
return sanitized;
|
|
82
|
+
}
|
|
83
|
+
return sanitized;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isLocalPathLike(value) {
|
|
87
|
+
if (typeof value !== 'string') {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rawValue = value.trim();
|
|
92
|
+
if (!rawValue || /^https?:\/\//i.test(rawValue)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const normalizedPath = normalizeReferencePath(rawValue);
|
|
97
|
+
return /^file:\/\//i.test(rawValue)
|
|
98
|
+
|| /^[A-Za-z]:[\\/]/.test(rawValue)
|
|
99
|
+
|| rawValue.startsWith('/')
|
|
100
|
+
|| rawValue.includes('.codex-overleaf/projects')
|
|
101
|
+
|| rawValue.includes('.codex-overleaf\\projects')
|
|
102
|
+
|| normalizedPath.includes('/.codex-overleaf/projects/')
|
|
103
|
+
|| /^Users\/[^/]+\//.test(normalizedPath)
|
|
104
|
+
|| /^home\/[^/]+\//.test(normalizedPath);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeReferencePath(value) {
|
|
108
|
+
if (typeof value !== 'string') {
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let normalized = value.trim();
|
|
113
|
+
if (!normalized) {
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (/^file:\/\//i.test(normalized)) {
|
|
118
|
+
normalized = normalized.replace(/^file:\/\/\/?/i, '/');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
normalized = decodeURI(normalized);
|
|
123
|
+
} catch (_error) {
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
normalized = normalized
|
|
128
|
+
.replace(/\\/g, '/')
|
|
129
|
+
.replace(/\/+/g, '/')
|
|
130
|
+
.trim();
|
|
131
|
+
|
|
132
|
+
let previous;
|
|
133
|
+
do {
|
|
134
|
+
previous = normalized;
|
|
135
|
+
normalized = normalized
|
|
136
|
+
.replace(/^@+/, '')
|
|
137
|
+
.replace(/^\/+/, '')
|
|
138
|
+
.replace(/^\.\//, '');
|
|
139
|
+
} while (normalized !== previous);
|
|
140
|
+
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectLineReferences(text, mode) {
|
|
145
|
+
if (!VALID_PARSE_MODES.has(mode) || typeof text !== 'string' || !text) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const refs = [];
|
|
150
|
+
collectColonReferences(text, mode, refs);
|
|
151
|
+
collectLineWordReferences(text, mode, refs);
|
|
152
|
+
collectChineseLineReferences(text, mode, refs);
|
|
153
|
+
|
|
154
|
+
return refs
|
|
155
|
+
.filter(ref => !shouldSkipReference(ref))
|
|
156
|
+
.sort((left, right) => left.index - right.index || right.rawText.length - left.rawText.length)
|
|
157
|
+
.reduce((deduped, ref) => {
|
|
158
|
+
if (deduped.some(existing => rangesOverlap(existing, ref))) {
|
|
159
|
+
return deduped;
|
|
160
|
+
}
|
|
161
|
+
deduped.push(ref);
|
|
162
|
+
return deduped;
|
|
163
|
+
}, []);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function collectColonReferences(text, mode, refs) {
|
|
167
|
+
const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}:(\\d+)(?::(\\d+))?(?![-:\\d])`, 'gi');
|
|
168
|
+
let match;
|
|
169
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
170
|
+
const line = parsePositiveInteger(match[3]);
|
|
171
|
+
const column = match[4] ? parsePositiveInteger(match[4]) : null;
|
|
172
|
+
if (!line || (match[4] && !column)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const prefixLength = match[1].length;
|
|
176
|
+
const rawText = match[0].slice(prefixLength);
|
|
177
|
+
refs.push({
|
|
178
|
+
rawText,
|
|
179
|
+
displayText: rawText,
|
|
180
|
+
rawPath: match[2],
|
|
181
|
+
line,
|
|
182
|
+
column,
|
|
183
|
+
source: mode,
|
|
184
|
+
index: match.index + prefixLength
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function collectLineWordReferences(text, mode, refs) {
|
|
190
|
+
const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}\\s+line\\s+(\\d+)(?![-:\\d])`, 'gi');
|
|
191
|
+
let match;
|
|
192
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
193
|
+
const line = parsePositiveInteger(match[3]);
|
|
194
|
+
if (!line) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const prefixLength = match[1].length;
|
|
198
|
+
const rawText = match[0].slice(prefixLength);
|
|
199
|
+
refs.push({
|
|
200
|
+
rawText,
|
|
201
|
+
displayText: `${match[2]}:${line}`,
|
|
202
|
+
rawPath: match[2],
|
|
203
|
+
line,
|
|
204
|
+
column: null,
|
|
205
|
+
source: mode,
|
|
206
|
+
index: match.index + prefixLength
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function collectChineseLineReferences(text, mode, refs) {
|
|
212
|
+
const pattern = new RegExp(`${REFERENCE_PREFIX_PATTERN}${PATH_PATTERN}\\s+第\\s*(\\d+)\\s*行`, 'gi');
|
|
213
|
+
let match;
|
|
214
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
215
|
+
const line = parsePositiveInteger(match[3]);
|
|
216
|
+
if (!line) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const prefixLength = match[1].length;
|
|
220
|
+
const rawText = match[0].slice(prefixLength);
|
|
221
|
+
refs.push({
|
|
222
|
+
rawText,
|
|
223
|
+
displayText: `${match[2]}:${line}`,
|
|
224
|
+
rawPath: match[2],
|
|
225
|
+
line,
|
|
226
|
+
column: null,
|
|
227
|
+
source: mode,
|
|
228
|
+
index: match.index + prefixLength
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function sanitizeLocalReferenceText(text, { projectFiles, placeholderBrackets }) {
|
|
234
|
+
let sanitized = replaceLineReferences(String(text || ''), {
|
|
235
|
+
projectFiles,
|
|
236
|
+
placeholderBrackets
|
|
237
|
+
});
|
|
238
|
+
sanitized = replaceBareLocalPaths(sanitized, {
|
|
239
|
+
projectFiles,
|
|
240
|
+
placeholderBrackets
|
|
241
|
+
});
|
|
242
|
+
return sanitized;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function replaceLineReferences(text, { projectFiles, placeholderBrackets }) {
|
|
246
|
+
const refs = collectLineReferences(text, 'plain-text-token')
|
|
247
|
+
.filter(ref => isLocalPathLike(ref.rawPath))
|
|
248
|
+
.sort((left, right) => right.index - left.index);
|
|
249
|
+
let sanitized = text;
|
|
250
|
+
for (const ref of refs) {
|
|
251
|
+
const replacement = formatReferenceReplacement(ref, {
|
|
252
|
+
projectFiles,
|
|
253
|
+
placeholderBrackets
|
|
254
|
+
});
|
|
255
|
+
sanitized = `${sanitized.slice(0, ref.index)}${replacement}${sanitized.slice(ref.index + ref.rawText.length)}`;
|
|
256
|
+
}
|
|
257
|
+
return sanitized;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function replaceBareLocalPaths(text, { projectFiles, placeholderBrackets }) {
|
|
261
|
+
return text.replace(BARE_LOCAL_PATH_PATTERN, (rawPath, offset, fullText) => {
|
|
262
|
+
if (/^[A-Za-z]:[\\/]/.test(rawPath) && offset > 0 && /[A-Za-z]/.test(fullText[offset - 1])) {
|
|
263
|
+
return rawPath;
|
|
264
|
+
}
|
|
265
|
+
const { value, trailing } = splitTrailingPunctuation(rawPath);
|
|
266
|
+
const resolved = resolveProjectReference({ rawPath: value, projectFiles });
|
|
267
|
+
if (resolved) {
|
|
268
|
+
return `${resolved.path}${trailing}`;
|
|
269
|
+
}
|
|
270
|
+
return `${formatLocalPathPlaceholder(null, placeholderBrackets)}${trailing}`;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function formatReferenceReplacement(ref, { projectFiles, placeholderBrackets }) {
|
|
275
|
+
const resolved = resolveProjectReference({
|
|
276
|
+
rawPath: ref.rawPath,
|
|
277
|
+
projectFiles
|
|
278
|
+
});
|
|
279
|
+
if (resolved) {
|
|
280
|
+
return `${resolved.path}:${ref.line}${ref.column ? `:${ref.column}` : ''}`;
|
|
281
|
+
}
|
|
282
|
+
return formatLocalPathPlaceholder(ref.line, placeholderBrackets);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function formatLocalPathPlaceholder(line, includeBrackets) {
|
|
286
|
+
const value = line ? `local path:${line}` : 'local path';
|
|
287
|
+
return includeBrackets ? `[${value}]` : value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function containsLocalTarget(target) {
|
|
291
|
+
if (!target) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
if (isLocalPathLike(target)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
return collectLineReferences(target, 'markdown-link-target')
|
|
298
|
+
.some(ref => isLocalPathLike(ref.rawPath));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function collectTextProjectFiles(projectFiles) {
|
|
302
|
+
if (!Array.isArray(projectFiles)) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const result = [];
|
|
307
|
+
for (const file of projectFiles) {
|
|
308
|
+
if (!file || file.kind !== 'text' || typeof file.path !== 'string') {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const normalizedPath = normalizeReferencePath(file.path);
|
|
313
|
+
if (!normalizedPath || hasUnsafePathSegments(normalizedPath)) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
result.push({
|
|
318
|
+
entry: file,
|
|
319
|
+
path: normalizedPath,
|
|
320
|
+
normalizedPath
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function buildResolvedReference(file, resolution) {
|
|
327
|
+
return {
|
|
328
|
+
path: file.path,
|
|
329
|
+
file: file.entry,
|
|
330
|
+
normalizedPath: file.normalizedPath,
|
|
331
|
+
resolution
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function shouldSkipReference(ref) {
|
|
336
|
+
const rawPath = String(ref.rawPath || '');
|
|
337
|
+
return /^https?:\/\//i.test(rawPath)
|
|
338
|
+
|| isEmailLikePath(rawPath)
|
|
339
|
+
|| hasUnsafeLineRange(ref.rawText);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function isEmailLikePath(rawPath) {
|
|
343
|
+
return /^[^@\s/\\]+@[^@\s/\\]+$/.test(rawPath);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function hasUnsafeLineRange(rawText) {
|
|
347
|
+
return /:\d+-\d+(?=$|[^\d])/.test(rawText);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function hasUnsafePathSegments(path) {
|
|
351
|
+
return normalizeReferencePath(path)
|
|
352
|
+
.split('/')
|
|
353
|
+
.some(segment => segment === '.' || segment === '..');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function rangesOverlap(left, right) {
|
|
357
|
+
const leftEnd = left.index + left.rawText.length;
|
|
358
|
+
const rightEnd = right.index + right.rawText.length;
|
|
359
|
+
return left.index < rightEnd && right.index < leftEnd;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function parsePositiveInteger(value) {
|
|
363
|
+
const parsed = Number(value);
|
|
364
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
return parsed;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function getBasename(path) {
|
|
371
|
+
const parts = String(path || '').split('/');
|
|
372
|
+
return parts[parts.length - 1] || '';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function splitTrailingPunctuation(value) {
|
|
376
|
+
const match = String(value || '').match(/^(.*?)([.,;!?]+)$/);
|
|
377
|
+
if (!match) {
|
|
378
|
+
return { value, trailing: '' };
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
value: match[1],
|
|
382
|
+
trailing: match[2]
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function toPublicReference(ref) {
|
|
387
|
+
return {
|
|
388
|
+
rawText: ref.rawText,
|
|
389
|
+
displayText: ref.displayText,
|
|
390
|
+
rawPath: ref.rawPath,
|
|
391
|
+
line: ref.line,
|
|
392
|
+
column: ref.column,
|
|
393
|
+
source: ref.source
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
isLocalPathLike,
|
|
399
|
+
normalizeReferencePath,
|
|
400
|
+
parseLineReferencesFromText,
|
|
401
|
+
resolveProjectReference,
|
|
402
|
+
sanitizeLocalReferences
|
|
403
|
+
};
|
|
404
|
+
});
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
/\b(?:sk|pk)-[A-Za-z0-9][A-Za-z0-9_-]{7,}\b/g,
|
|
82
82
|
/\b(?:api[_-]?key|token|password|passwd|secret)\b\s*[:=]\s*["']?[^"'\s,;]+["']?/gi
|
|
83
83
|
];
|
|
84
|
+
const LineReferences = loadLineReferences();
|
|
84
85
|
|
|
85
86
|
function normalizePanelState(input = {}, options = {}) {
|
|
86
87
|
const state = {
|
|
@@ -143,7 +144,7 @@
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
function normalizeSession(session, fallbackState = DEFAULT_PANEL_STATE, options = {}) {
|
|
146
|
-
const history =
|
|
147
|
+
const history = normalizeHistoryEntries(session.history);
|
|
147
148
|
const normalizedRuns = normalizeRuns(session.runs, options);
|
|
148
149
|
const runs = normalizedRuns.length ? normalizedRuns : recoverRunsFromHistory(session, history);
|
|
149
150
|
const rawTitle = typeof session.title === 'string' ? session.title.trim() : '';
|
|
@@ -159,7 +160,7 @@
|
|
|
159
160
|
updatedAt: typeof session.updatedAt === 'string' ? session.updatedAt : new Date().toISOString(),
|
|
160
161
|
history,
|
|
161
162
|
runs,
|
|
162
|
-
task:
|
|
163
|
+
task: sanitizeAssistantVisibleText(session.task),
|
|
163
164
|
mode: VALID_MODES.has(session.mode) ? session.mode : fallbackState.mode,
|
|
164
165
|
model: typeof session.model === 'string' && session.model ? session.model : fallbackState.model,
|
|
165
166
|
reasoningEffort: VALID_REASONING.has(session.reasoningEffort)
|
|
@@ -267,9 +268,9 @@
|
|
|
267
268
|
titleSource,
|
|
268
269
|
createdAt: overrides.createdAt || new Date().toISOString(),
|
|
269
270
|
updatedAt: overrides.updatedAt || new Date().toISOString(),
|
|
270
|
-
history:
|
|
271
|
+
history: normalizeHistoryEntries(overrides.history),
|
|
271
272
|
runs,
|
|
272
|
-
task:
|
|
273
|
+
task: sanitizeAssistantVisibleText(overrides.task),
|
|
273
274
|
mode: VALID_MODES.has(overrides.mode) ? overrides.mode : DEFAULT_PANEL_STATE.mode,
|
|
274
275
|
model: typeof overrides.model === 'string' && overrides.model ? overrides.model : DEFAULT_PANEL_STATE.model,
|
|
275
276
|
reasoningEffort: VALID_REASONING.has(overrides.reasoningEffort)
|
|
@@ -286,6 +287,8 @@
|
|
|
286
287
|
|
|
287
288
|
function recordSessionResult(session, entry) {
|
|
288
289
|
const base = session?.id ? session : createSession();
|
|
290
|
+
const task = sanitizeAssistantVisibleText(entry?.task) || 'untitled task';
|
|
291
|
+
const result = sanitizeAssistantVisibleText(entry?.result || entry?.status) || 'completed';
|
|
289
292
|
return {
|
|
290
293
|
...base,
|
|
291
294
|
id: base.id,
|
|
@@ -293,12 +296,12 @@
|
|
|
293
296
|
history: [
|
|
294
297
|
...(Array.isArray(base.history) ? base.history : []),
|
|
295
298
|
{
|
|
296
|
-
task
|
|
297
|
-
result
|
|
298
|
-
at: entry
|
|
299
|
+
task,
|
|
300
|
+
result,
|
|
301
|
+
at: entry?.at || new Date().toISOString()
|
|
299
302
|
}
|
|
300
303
|
].slice(-10),
|
|
301
|
-
updatedAt: entry
|
|
304
|
+
updatedAt: entry?.at || new Date().toISOString()
|
|
302
305
|
};
|
|
303
306
|
}
|
|
304
307
|
|
|
@@ -419,7 +422,7 @@
|
|
|
419
422
|
}
|
|
420
423
|
|
|
421
424
|
function sanitizeAutoTitle(value) {
|
|
422
|
-
const title =
|
|
425
|
+
const title = sanitizeAssistantVisibleText(value)
|
|
423
426
|
.replace(/@file:[^\s]+/g, ' ')
|
|
424
427
|
.replace(/@(context|compile-log)\b/g, ' ')
|
|
425
428
|
.replace(/\s+/g, ' ')
|
|
@@ -506,13 +509,13 @@
|
|
|
506
509
|
|
|
507
510
|
return {
|
|
508
511
|
id: run.id,
|
|
509
|
-
task:
|
|
512
|
+
task: sanitizeAssistantVisibleText(run.task) || 'untitled task',
|
|
510
513
|
mode: typeof run.mode === 'string' ? run.mode : '',
|
|
511
514
|
model: typeof run.model === 'string' ? run.model : '',
|
|
512
515
|
reasoningEffort: typeof run.reasoningEffort === 'string' ? run.reasoningEffort : '',
|
|
513
516
|
speedTier: typeof run.speedTier === 'string' ? run.speedTier : '',
|
|
514
517
|
status: shouldStopRestoredRun ? 'failed' : normalizeRunStatus(run.status),
|
|
515
|
-
statusText: shouldStopRestoredRun ? '页面刷新后已停止跟踪' :
|
|
518
|
+
statusText: shouldStopRestoredRun ? '页面刷新后已停止跟踪' : sanitizeAssistantVisibleText(run.statusText),
|
|
516
519
|
startedAt: typeof run.startedAt === 'string' ? run.startedAt : '',
|
|
517
520
|
finishedAt: shouldStopRestoredRun ? new Date().toISOString() : typeof run.finishedAt === 'string' ? run.finishedAt : '',
|
|
518
521
|
events: events.slice(-MAX_RUN_EVENTS),
|
|
@@ -522,7 +525,7 @@
|
|
|
522
525
|
undoBaseFiles: normalizeRunFiles(run.undoBaseFiles),
|
|
523
526
|
undoTrackedChanges: normalizeRunTrackedChanges(run.undoTrackedChanges),
|
|
524
527
|
undoExpectedFiles: normalizeRunFiles(run.undoExpectedFiles),
|
|
525
|
-
undoStatus:
|
|
528
|
+
undoStatus: sanitizeAssistantVisibleText(run.undoStatus)
|
|
526
529
|
};
|
|
527
530
|
}
|
|
528
531
|
|
|
@@ -538,14 +541,14 @@
|
|
|
538
541
|
return (Array.isArray(events) ? events : [])
|
|
539
542
|
.filter(event => event && typeof event.title === 'string')
|
|
540
543
|
.map(event => ({
|
|
541
|
-
title: event.title,
|
|
544
|
+
title: sanitizeAssistantVisibleText(event.title) || 'Event',
|
|
542
545
|
status: typeof event.status === 'string' ? event.status : 'info',
|
|
543
|
-
detail: event.detail,
|
|
546
|
+
detail: sanitizeAssistantVisibleValue(event.detail),
|
|
544
547
|
timestamp: typeof event.timestamp === 'string' ? event.timestamp : '',
|
|
545
548
|
kind: typeof event.kind === 'string' ? event.kind : 'activity',
|
|
546
|
-
technicalDetail: event.technicalDetail,
|
|
547
|
-
streamKey:
|
|
548
|
-
streamRole:
|
|
549
|
+
technicalDetail: sanitizeAssistantVisibleValue(event.technicalDetail),
|
|
550
|
+
streamKey: sanitizeAssistantVisibleText(event.streamKey),
|
|
551
|
+
streamRole: sanitizeAssistantVisibleText(event.streamRole)
|
|
549
552
|
}))
|
|
550
553
|
.slice(-MAX_RUN_EVENTS);
|
|
551
554
|
}
|
|
@@ -624,8 +627,8 @@
|
|
|
624
627
|
normalized.push({
|
|
625
628
|
key,
|
|
626
629
|
id: typeof change.id === 'string' ? change.id : '',
|
|
627
|
-
path:
|
|
628
|
-
label:
|
|
630
|
+
path: sanitizeAssistantVisibleText(change.path),
|
|
631
|
+
label: sanitizeAssistantVisibleText(change.label)
|
|
629
632
|
});
|
|
630
633
|
}
|
|
631
634
|
return normalized;
|
|
@@ -638,7 +641,7 @@
|
|
|
638
641
|
if (typeof item !== 'string') {
|
|
639
642
|
continue;
|
|
640
643
|
}
|
|
641
|
-
const path =
|
|
644
|
+
const path = sanitizeAssistantVisibleText(item.trim());
|
|
642
645
|
if (!path || seen.has(path)) {
|
|
643
646
|
continue;
|
|
644
647
|
}
|
|
@@ -914,7 +917,7 @@
|
|
|
914
917
|
if (typeof value !== 'string' || !value.trim()) {
|
|
915
918
|
return '';
|
|
916
919
|
}
|
|
917
|
-
const text =
|
|
920
|
+
const text = sanitizeAssistantVisibleText(value);
|
|
918
921
|
if (isOmittedStorageSummary(text)) {
|
|
919
922
|
return text;
|
|
920
923
|
}
|
|
@@ -938,7 +941,7 @@
|
|
|
938
941
|
for (const key of Object.keys(value)) {
|
|
939
942
|
const item = value[key];
|
|
940
943
|
if (/(^|[A-Z_])path$/i.test(key) && typeof item === 'string') {
|
|
941
|
-
const path =
|
|
944
|
+
const path = sanitizeAssistantVisibleText(item).replace(/\\/g, '/').replace(/^\/+/, '').trim();
|
|
942
945
|
if (path) {
|
|
943
946
|
state.count += 1;
|
|
944
947
|
if (!state.seen.has(path) && state.items.length < 5) {
|
|
@@ -991,13 +994,107 @@
|
|
|
991
994
|
}
|
|
992
995
|
|
|
993
996
|
function normalizeTextField(value, maxChars) {
|
|
994
|
-
const text =
|
|
997
|
+
const text = sanitizeAssistantVisibleText(value);
|
|
995
998
|
if (!Number.isFinite(maxChars) || maxChars <= 0 || text.length <= maxChars) {
|
|
996
999
|
return text;
|
|
997
1000
|
}
|
|
998
1001
|
return `${text.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
999
1002
|
}
|
|
1000
1003
|
|
|
1004
|
+
function normalizeHistoryEntries(history) {
|
|
1005
|
+
return (Array.isArray(history) ? history : [])
|
|
1006
|
+
.slice(-10)
|
|
1007
|
+
.map(entry => ({
|
|
1008
|
+
task: sanitizeAssistantVisibleText(entry?.task) || 'untitled task',
|
|
1009
|
+
result: sanitizeAssistantVisibleText(entry?.result || entry?.status) || 'completed',
|
|
1010
|
+
at: typeof entry?.at === 'string' ? entry.at : ''
|
|
1011
|
+
}));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function sanitizeAssistantVisibleValue(value, depth = 0) {
|
|
1015
|
+
if (typeof value === 'string') {
|
|
1016
|
+
return sanitizeAssistantVisibleText(value);
|
|
1017
|
+
}
|
|
1018
|
+
if (value === undefined || value === null || typeof value === 'number' || typeof value === 'boolean') {
|
|
1019
|
+
return value;
|
|
1020
|
+
}
|
|
1021
|
+
if (depth > 12) {
|
|
1022
|
+
return '[redacted nested value]';
|
|
1023
|
+
}
|
|
1024
|
+
if (Array.isArray(value)) {
|
|
1025
|
+
return value.map(item => sanitizeAssistantVisibleValue(item, depth + 1));
|
|
1026
|
+
}
|
|
1027
|
+
if (typeof value === 'object') {
|
|
1028
|
+
const result = {};
|
|
1029
|
+
for (const key of Object.keys(value)) {
|
|
1030
|
+
const safeKey = sanitizeAssistantVisibleText(key) || key;
|
|
1031
|
+
result[safeKey] = sanitizeAssistantVisibleValue(value[key], depth + 1);
|
|
1032
|
+
}
|
|
1033
|
+
return result;
|
|
1034
|
+
}
|
|
1035
|
+
return value;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function sanitizeAssistantVisibleText(value) {
|
|
1039
|
+
if (typeof value !== 'string' || !value) {
|
|
1040
|
+
return '';
|
|
1041
|
+
}
|
|
1042
|
+
const secretRedacted = redactSecretLikeText(value);
|
|
1043
|
+
if (!mightContainLocalReferenceText(secretRedacted)) {
|
|
1044
|
+
return secretRedacted;
|
|
1045
|
+
}
|
|
1046
|
+
if (LineReferences?.sanitizeLocalReferences) {
|
|
1047
|
+
try {
|
|
1048
|
+
return LineReferences.sanitizeLocalReferences(secretRedacted, {
|
|
1049
|
+
projectFiles: [],
|
|
1050
|
+
context: 'persist'
|
|
1051
|
+
});
|
|
1052
|
+
} catch (_error) {
|
|
1053
|
+
return fallbackSanitizeLocalReferences(secretRedacted);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return fallbackSanitizeLocalReferences(secretRedacted);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function mightContainLocalReferenceText(value) {
|
|
1060
|
+
return /(?:file:\/\/\/?|[A-Za-z]:[\\/]|\/(?:Users|home|private|var|tmp)\/|[\\/]\.codex-overleaf[\\/]projects[\\/]|\.codex-overleaf[\\/]projects[\\/])/i.test(String(value || ''));
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function loadLineReferences() {
|
|
1064
|
+
if (typeof globalThis !== 'undefined' && globalThis.CodexOverleafLineReferences) {
|
|
1065
|
+
return globalThis.CodexOverleafLineReferences;
|
|
1066
|
+
}
|
|
1067
|
+
if (typeof require === 'function') {
|
|
1068
|
+
try {
|
|
1069
|
+
return require('./lineReferences');
|
|
1070
|
+
} catch (_error) {
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function fallbackSanitizeLocalReferences(value) {
|
|
1078
|
+
return String(value || '')
|
|
1079
|
+
.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_raw, label, target) => {
|
|
1080
|
+
const safeLabel = fallbackSanitizeBareLocalPaths(label);
|
|
1081
|
+
return /^https?:\/\//i.test(String(target || '').trim())
|
|
1082
|
+
? `[${safeLabel}](${target})`
|
|
1083
|
+
: `[${safeLabel}]`;
|
|
1084
|
+
})
|
|
1085
|
+
.replace(/(?:file:\/\/\/?[^\s)\]]+|[A-Za-z]:[\\/][^\s)\]]+|\/(?:Users|home|private|var|tmp)\/[^\s)\]]+|[^\s)\]]*[\\/]\.codex-overleaf[\\/][^\s)\]]+)/gi, rawPath => {
|
|
1086
|
+
const line = String(rawPath || '').match(/:(\d+)(?::\d+)?(?:[.,;!?])?$/)?.[1];
|
|
1087
|
+
return line ? `[local path:${line}]` : '[local path]';
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function fallbackSanitizeBareLocalPaths(value) {
|
|
1092
|
+
return String(value || '').replace(/(?:file:\/\/\/?[^\s)\]]+|[A-Za-z]:[\\/][^\s)\]]+|\/(?:Users|home|private|var|tmp)\/[^\s)\]]+|[^\s)\]]*[\\/]\.codex-overleaf[\\/][^\s)\]]+)/gi, rawPath => {
|
|
1093
|
+
const line = String(rawPath || '').match(/:(\d+)(?::\d+)?(?:[.,;!?])?$/)?.[1];
|
|
1094
|
+
return line ? `[local path:${line}]` : '[local path]';
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1001
1098
|
function redactSecretLikeText(value) {
|
|
1002
1099
|
if (typeof value !== 'string') {
|
|
1003
1100
|
return '';
|
|
@@ -812,13 +812,71 @@
|
|
|
812
812
|
}
|
|
813
813
|
|
|
814
814
|
function normalizeTextField(value, maxChars) {
|
|
815
|
-
var text =
|
|
815
|
+
var text = sanitizeAssistantVisibleText(value);
|
|
816
816
|
if (!Number.isFinite(maxChars) || maxChars <= 0 || text.length <= maxChars) {
|
|
817
817
|
return text;
|
|
818
818
|
}
|
|
819
819
|
return text.slice(0, Math.max(0, maxChars - 1)) + '…';
|
|
820
820
|
}
|
|
821
821
|
|
|
822
|
+
function sanitizeAssistantVisibleText(value) {
|
|
823
|
+
if (typeof value !== 'string' || !value) {
|
|
824
|
+
return '';
|
|
825
|
+
}
|
|
826
|
+
var text = redactSecretLikeText(value);
|
|
827
|
+
if (!mightContainLocalReferenceText(text)) {
|
|
828
|
+
return text;
|
|
829
|
+
}
|
|
830
|
+
return sanitizeLocalReferencesForStorage(text);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function mightContainLocalReferenceText(value) {
|
|
834
|
+
return /(?:file:\/\/\/?|[A-Za-z]:[\\/]|\/(?:Users|home|private|var|tmp)\/|[\\/]\.codex-overleaf[\\/]projects[\\/]|\.codex-overleaf[\\/]projects[\\/])/i.test(String(value || ''));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function sanitizeLocalReferencesForStorage(value) {
|
|
838
|
+
return String(value || '')
|
|
839
|
+
.replace(/\[([^\]]*)\]\(([^)]*)\)/g, function (_raw, label, target) {
|
|
840
|
+
var safeLabel = sanitizeBareLocalPaths(label, false);
|
|
841
|
+
var trimmedTarget = String(target || '').trim();
|
|
842
|
+
if (/^https?:\/\//i.test(trimmedTarget)) {
|
|
843
|
+
return '[' + safeLabel + '](' + sanitizeHttpTarget(trimmedTarget) + ')';
|
|
844
|
+
}
|
|
845
|
+
if (mightContainLocalReferenceText(trimmedTarget)) {
|
|
846
|
+
return '[' + safeLabel + ']';
|
|
847
|
+
}
|
|
848
|
+
return '[' + safeLabel + '](' + sanitizeBareLocalPaths(trimmedTarget, false) + ')';
|
|
849
|
+
})
|
|
850
|
+
.replace(/(?:file:\/\/\/?[^\s)\]]+|[A-Za-z]:[\\/][^\s)\]]+|\/(?:Users|home|private|var|tmp)\/[^\s)\]]+|[^\s)\]]*[\\/]\.codex-overleaf[\\/]projects[\\/][^\s)\]]+)/gi, function (rawPath) {
|
|
851
|
+
return formatLocalPathPlaceholder(rawPath, true);
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function sanitizeBareLocalPaths(value, includeBrackets) {
|
|
856
|
+
return String(value || '').replace(/(?:file:\/\/\/?[^\s)\]]+|[A-Za-z]:[\\/][^\s)\]]+|\/(?:Users|home|private|var|tmp)\/[^\s)\]]+|[^\s)\]]*[\\/]\.codex-overleaf[\\/]projects[\\/][^\s)\]]+)/gi, function (rawPath) {
|
|
857
|
+
return formatLocalPathPlaceholder(rawPath, includeBrackets);
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function sanitizeHttpTarget(value) {
|
|
862
|
+
try {
|
|
863
|
+
var parsed = new URL(value);
|
|
864
|
+
if (mightContainLocalReferenceText(parsed.href)) {
|
|
865
|
+
parsed.search = '';
|
|
866
|
+
parsed.hash = '';
|
|
867
|
+
}
|
|
868
|
+
return parsed.href;
|
|
869
|
+
} catch (_error) {
|
|
870
|
+
return '';
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function formatLocalPathPlaceholder(rawPath, includeBrackets) {
|
|
875
|
+
var lineMatch = String(rawPath || '').match(/:(\d+)(?::\d+)?(?:[.,;!?])?$/);
|
|
876
|
+
var value = lineMatch ? 'local path:' + lineMatch[1] : 'local path';
|
|
877
|
+
return includeBrackets ? '[' + value + ']' : value;
|
|
878
|
+
}
|
|
879
|
+
|
|
822
880
|
function nonNegativeInteger(value) {
|
|
823
881
|
var number = Number(value);
|
|
824
882
|
return Number.isFinite(number) && number > 0 ? Math.floor(number) : 0;
|
|
@@ -4,6 +4,7 @@ function buildCodexPrompt(request) {
|
|
|
4
4
|
const project = request.project || {};
|
|
5
5
|
const files = Array.isArray(project.files) ? project.files : [];
|
|
6
6
|
const focusFiles = normalizeFocusFiles(request.focusFiles || request.session?.focusFiles);
|
|
7
|
+
const activePath = normalizeProjectPath(project.activePath);
|
|
7
8
|
const orderedFiles = orderFilesByFocus(files, focusFiles);
|
|
8
9
|
const inventory = orderedFiles.map(file => `- ${file.path} (${String(file.content || '').length} chars)`).join('\n') || '- no files supplied';
|
|
9
10
|
|
|
@@ -15,7 +16,7 @@ function buildCodexPrompt(request) {
|
|
|
15
16
|
`Reasoning effort: ${request.reasoningEffort || 'default'}`,
|
|
16
17
|
`Session: ${request.session?.id || 'none'}`,
|
|
17
18
|
`Task: ${request.task}`,
|
|
18
|
-
`Active file: ${
|
|
19
|
+
`Active file: ${activePath || 'unknown'}`,
|
|
19
20
|
'',
|
|
20
21
|
'Recent session history:',
|
|
21
22
|
formatSessionHistory(request.session?.history),
|
|
@@ -23,6 +24,12 @@ function buildCodexPrompt(request) {
|
|
|
23
24
|
'Focus files:',
|
|
24
25
|
formatFocusFiles(focusFiles, files),
|
|
25
26
|
'',
|
|
27
|
+
'Project location citation rules:',
|
|
28
|
+
formatProjectLocationCitationRules(),
|
|
29
|
+
'',
|
|
30
|
+
'Focused project file inventory:',
|
|
31
|
+
formatFocusedProjectFileInventory([activePath, ...focusFiles]),
|
|
32
|
+
'',
|
|
26
33
|
'Files:',
|
|
27
34
|
inventory,
|
|
28
35
|
'',
|
|
@@ -62,10 +69,7 @@ function normalizeFocusFiles(value) {
|
|
|
62
69
|
const seen = new Set();
|
|
63
70
|
const files = [];
|
|
64
71
|
for (const item of Array.isArray(value) ? value : []) {
|
|
65
|
-
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const path = item.trim();
|
|
72
|
+
const path = normalizeProjectPath(item);
|
|
69
73
|
if (!path || seen.has(path)) {
|
|
70
74
|
continue;
|
|
71
75
|
}
|
|
@@ -75,6 +79,42 @@ function normalizeFocusFiles(value) {
|
|
|
75
79
|
return files.slice(0, 5);
|
|
76
80
|
}
|
|
77
81
|
|
|
82
|
+
function normalizeProjectPath(value) {
|
|
83
|
+
const path = String(value || '')
|
|
84
|
+
.replace(/^@file:/i, '')
|
|
85
|
+
.replace(/\\/g, '/')
|
|
86
|
+
.trim();
|
|
87
|
+
if (!path || /^file:\/\//i.test(path) || /^[A-Za-z]:\//.test(path)) {
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
const projectPath = path.replace(/^\/+/, '');
|
|
91
|
+
const wasAbsolutePath = path.startsWith('/');
|
|
92
|
+
if (wasAbsolutePath && /^(Users|home|tmp|var|private|Volumes)\//i.test(projectPath)) {
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
if (/(^|\/)\.codex-overleaf\/projects(\/|$)/.test(projectPath)) {
|
|
96
|
+
return '';
|
|
97
|
+
}
|
|
98
|
+
return projectPath;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatProjectLocationCitationRules() {
|
|
102
|
+
return [
|
|
103
|
+
'- In any user-visible output, cite project locations using only Overleaf project-relative paths from the project file inventory below.',
|
|
104
|
+
'- Use path:LINE[:COLUMN] (for example, path/to/file.tex:12:3).',
|
|
105
|
+
'- Do not cite local absolute paths, temporary workspace paths, file:// URLs, or markdown links to local files.',
|
|
106
|
+
'- If a line number is uncertain, cite only the project-relative file path.'
|
|
107
|
+
].join('\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatFocusedProjectFileInventory(files) {
|
|
111
|
+
const focusFiles = normalizeFocusFiles(files);
|
|
112
|
+
if (!focusFiles.length) {
|
|
113
|
+
return '- none selected.';
|
|
114
|
+
}
|
|
115
|
+
return focusFiles.map(filePath => `- ${filePath}`).join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
78
118
|
function formatFocusFiles(focusFiles, files) {
|
|
79
119
|
if (!focusFiles.length) {
|
|
80
120
|
return '- none (default to whole project)';
|
|
@@ -30,10 +30,15 @@ function buildCodexTurnPrompt(options = {}) {
|
|
|
30
30
|
'',
|
|
31
31
|
'Current Overleaf workspace:',
|
|
32
32
|
`- Project: ${context.projectKey || context.projectId || 'unknown'}`,
|
|
33
|
-
`- Local workspace: ${context.workspacePath || 'current cwd'}`,
|
|
34
33
|
'- The local workspace was synced from Overleaf immediately before this turn.',
|
|
35
34
|
'- If the recent session history conflicts with the files in the workspace, trust the files.',
|
|
36
35
|
'',
|
|
36
|
+
'Project location citation rules:',
|
|
37
|
+
formatProjectLocationCitationRules(),
|
|
38
|
+
'',
|
|
39
|
+
'Focused project file inventory:',
|
|
40
|
+
formatFocusedProjectFileInventory(context.focusFiles),
|
|
41
|
+
'',
|
|
37
42
|
'Focus files:',
|
|
38
43
|
formatFocusFiles(context.focusFiles),
|
|
39
44
|
'',
|
|
@@ -222,11 +227,40 @@ function normalizeFocusFiles(value) {
|
|
|
222
227
|
}
|
|
223
228
|
|
|
224
229
|
function normalizeProjectPath(value) {
|
|
225
|
-
|
|
230
|
+
const path = String(value || '')
|
|
226
231
|
.replace(/^@file:/i, '')
|
|
227
232
|
.replace(/\\/g, '/')
|
|
228
|
-
.trim()
|
|
229
|
-
|
|
233
|
+
.trim();
|
|
234
|
+
if (!path || /^file:\/\//i.test(path) || /^[A-Za-z]:\//.test(path)) {
|
|
235
|
+
return '';
|
|
236
|
+
}
|
|
237
|
+
const projectPath = path.replace(/^\/+/, '');
|
|
238
|
+
const wasAbsolutePath = path.startsWith('/');
|
|
239
|
+
if (wasAbsolutePath && /^(Users|home|tmp|var|private|Volumes)\//i.test(projectPath)) {
|
|
240
|
+
return '';
|
|
241
|
+
}
|
|
242
|
+
if (/(^|\/)\.codex-overleaf\/projects(\/|$)/.test(projectPath)) {
|
|
243
|
+
return '';
|
|
244
|
+
}
|
|
245
|
+
return projectPath;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function formatProjectLocationCitationRules() {
|
|
249
|
+
return [
|
|
250
|
+
'- In any user-visible output, cite project locations using only Overleaf project-relative paths from the project file inventory below.',
|
|
251
|
+
'- Use path:LINE[:COLUMN] (for example, path/to/file.tex:12:3).',
|
|
252
|
+
'- Do not wrap project location citations in backticks or inline code; write main.tex:28 as normal text so the browser can turn it into a jump button.',
|
|
253
|
+
'- Do not cite local absolute paths, temporary workspace paths, file:// URLs, or markdown links to local files.',
|
|
254
|
+
'- If a line number is uncertain, cite only the project-relative file path.'
|
|
255
|
+
].join('\n');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatFocusedProjectFileInventory(files) {
|
|
259
|
+
const focusFiles = normalizeFocusFiles(files);
|
|
260
|
+
if (!focusFiles.length) {
|
|
261
|
+
return '- none selected.';
|
|
262
|
+
}
|
|
263
|
+
return focusFiles.map(filePath => `- ${filePath}`).join('\n');
|
|
230
264
|
}
|
|
231
265
|
|
|
232
266
|
function formatFocusFiles(files) {
|