ai-or-die 0.1.62 → 0.1.63
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/package.json +4 -1
- package/sea-bootstrap.js +27 -1
- package/src/public/app.js +136 -17
- package/src/public/auth.js +22 -0
- package/src/public/components/file-browser.css +756 -3
- package/src/public/file-browser.js +2065 -136
- package/src/public/file-diff.js +406 -0
- package/src/public/file-editor.js +418 -135
- package/src/public/file-pdf-viewer.js +473 -0
- package/src/public/file-search.js +522 -0
- package/src/public/file-tabs.js +1087 -0
- package/src/public/file-viewer-monaco.js +410 -0
- package/src/public/file-watcher-client.js +422 -0
- package/src/public/index.html +12 -1
- package/src/public/markdown-render.js +576 -0
- package/src/public/notebook-render.js +259 -0
- package/src/public/splits.js +6 -0
- package/src/public/vendor/monaco-worker-shim.js +82 -0
- package/src/public/vendor/panzoom.min.js +6 -0
- package/src/public/vendor/pdfjs/LICENSE +177 -0
- package/src/public/vendor/pdfjs/pdf.min.mjs +21 -0
- package/src/public/vendor/pdfjs/pdf.worker.min.mjs +21 -0
- package/src/server.js +705 -13
- package/src/utils/file-watcher.js +531 -0
- package/src/utils/search.js +538 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-or-die",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
4
4
|
"description": "Universal AI coding terminal — Claude, Copilot, Gemini & more in your browser",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@lydell/node-pty": "1.2.0-beta.10",
|
|
38
|
+
"@vscode/ripgrep": "^1.18.0",
|
|
39
|
+
"chokidar": "^5.0.0",
|
|
38
40
|
"commander": "^12.1.0",
|
|
39
41
|
"cors": "^2.8.5",
|
|
40
42
|
"express": "^4.19.2",
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
"devDependencies": {
|
|
56
58
|
"@playwright/test": "^1.58.2",
|
|
57
59
|
"esbuild": "^0.24.0",
|
|
60
|
+
"jsdom": "^24.1.3",
|
|
58
61
|
"mocha": "^11.7.1",
|
|
59
62
|
"postject": "^1.0.0-alpha.6"
|
|
60
63
|
}
|
package/sea-bootstrap.js
CHANGED
|
@@ -28,18 +28,44 @@ if (isSea) {
|
|
|
28
28
|
const ptyPkg = `node-pty-${process.platform}-${process.arch}`;
|
|
29
29
|
const sherpaPlatform = process.platform === 'win32' ? 'win' : process.platform;
|
|
30
30
|
const sherpaPkg = `sherpa-onnx-${sherpaPlatform}-${process.arch}`;
|
|
31
|
+
// Bundled ripgrep (ADR-0018) — see scripts/build-sea.js for the asset
|
|
32
|
+
// prefix scheme. Mirror it here for the extraction predicate AND the
|
|
33
|
+
// post-extract path computation that feeds global.__SEA_RG_PATH__.
|
|
34
|
+
const rgAssetPrefix = `vscode-ripgrep-${process.platform}-${process.arch}`;
|
|
35
|
+
const rgBinName = process.platform === 'win32' ? 'rg.exe' : 'rg';
|
|
31
36
|
const assetKeys = sea.getAssetKeys();
|
|
32
37
|
|
|
33
38
|
for (const key of assetKeys) {
|
|
34
39
|
if (key.startsWith(ptyPkg + '/') ||
|
|
35
40
|
key.startsWith(sherpaPkg + '/') ||
|
|
36
|
-
key.startsWith('sherpa-onnx-node/')
|
|
41
|
+
key.startsWith('sherpa-onnx-node/') ||
|
|
42
|
+
key.startsWith(rgAssetPrefix + '/')) {
|
|
37
43
|
const targetPath = path.join(tempDir, key);
|
|
38
44
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
39
45
|
fs.writeFileSync(targetPath, new Uint8Array(sea.getRawAsset(key)));
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
// chmod +x the extracted ripgrep binary on POSIX. fs.writeFileSync
|
|
50
|
+
// doesn't preserve the executable bit; without this chmod, the
|
|
51
|
+
// search backend's fs.accessSync(X_OK) liveness check would reject
|
|
52
|
+
// the binary on macOS / Linux SEA builds even though the bytes are
|
|
53
|
+
// intact. Windows ignores file mode bits — the .exe extension
|
|
54
|
+
// already conveys executability.
|
|
55
|
+
const rgExtractedPath = path.join(tempDir, rgAssetPrefix, 'bin', rgBinName);
|
|
56
|
+
if (fs.existsSync(rgExtractedPath)) {
|
|
57
|
+
if (process.platform !== 'win32') {
|
|
58
|
+
try { fs.chmodSync(rgExtractedPath, 0o755); }
|
|
59
|
+
catch (e) {
|
|
60
|
+
console.warn('[SEA] failed to chmod +x bundled ripgrep:', e.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Surface the resolved path to the search backend (src/utils/search.js)
|
|
64
|
+
// via a global, since the bundled module's require('@vscode/ripgrep')
|
|
65
|
+
// can't resolve at SEA runtime (no node_modules on disk).
|
|
66
|
+
global.__SEA_RG_PATH__ = rgExtractedPath;
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
// Store references for pty-sea-shim.js and server.js to use
|
|
44
70
|
global.__SEA_MODE__ = true;
|
|
45
71
|
global.__SEA_TEMP_DIR__ = tempDir;
|
package/src/public/app.js
CHANGED
|
@@ -672,6 +672,7 @@ class ClaudeCodeWebInterface {
|
|
|
672
672
|
|
|
673
673
|
this.setupTerminalSearch();
|
|
674
674
|
this.setupTerminalContextMenu();
|
|
675
|
+
this._setupTerminalLinking(this.terminal);
|
|
675
676
|
|
|
676
677
|
this.terminal.onData((data) => {
|
|
677
678
|
if (this._ctrlModifierPending) {
|
|
@@ -714,11 +715,20 @@ class ClaudeCodeWebInterface {
|
|
|
714
715
|
}
|
|
715
716
|
});
|
|
716
717
|
|
|
717
|
-
// Sync terminal colors
|
|
718
|
+
// Sync terminal colors AND any live Monaco instances when the CSS
|
|
719
|
+
// theme changes (data-theme attribute on documentElement).
|
|
718
720
|
const themeObserver = new MutationObserver((mutations) => {
|
|
719
721
|
for (const m of mutations) {
|
|
720
|
-
if (m.attributeName === 'data-theme'
|
|
721
|
-
this.syncTerminalTheme();
|
|
722
|
+
if (m.attributeName === 'data-theme') {
|
|
723
|
+
if (this.terminal) this.syncTerminalTheme();
|
|
724
|
+
// Editor pane + read-only code preview re-theme live so
|
|
725
|
+
// they don't get stuck on whatever theme was active when
|
|
726
|
+
// they were created. Loader reads the new data-theme via
|
|
727
|
+
// resolveMonacoTheme() internally.
|
|
728
|
+
if (window.fileViewerMonaco &&
|
|
729
|
+
typeof window.fileViewerMonaco.applyThemeToAll === 'function') {
|
|
730
|
+
try { window.fileViewerMonaco.applyThemeToAll(); } catch (_) { /* swallow */ }
|
|
731
|
+
}
|
|
722
732
|
}
|
|
723
733
|
}
|
|
724
734
|
});
|
|
@@ -1055,6 +1065,34 @@ class ClaudeCodeWebInterface {
|
|
|
1055
1065
|
}
|
|
1056
1066
|
});
|
|
1057
1067
|
|
|
1068
|
+
// Ctrl/Cmd+Shift+F → toggle the file browser's cross-file search panel.
|
|
1069
|
+
// Opens the file browser (if closed) and the search panel together so
|
|
1070
|
+
// the user can hit one shortcut from anywhere in the app and start
|
|
1071
|
+
// typing a query immediately. Mirrors the VS Code/JetBrains binding.
|
|
1072
|
+
document.addEventListener('keydown', (e) => {
|
|
1073
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'F' || e.key === 'f')) {
|
|
1074
|
+
// Don't steal the shortcut from a focused editor that owns
|
|
1075
|
+
// its own find — Monaco's find-in-files for the editor pane
|
|
1076
|
+
// is handled inside the editor surface; the file-browser
|
|
1077
|
+
// search is for the project-wide case.
|
|
1078
|
+
const tag = (e.target && e.target.tagName) || '';
|
|
1079
|
+
const isEditableField = (tag === 'INPUT' || tag === 'TEXTAREA' ||
|
|
1080
|
+
(e.target && e.target.isContentEditable));
|
|
1081
|
+
// Allow the shortcut when typing in our OWN search panel
|
|
1082
|
+
// input (it'll just refocus); skip for any other text field.
|
|
1083
|
+
const insideSearchPanel = e.target &&
|
|
1084
|
+
typeof e.target.closest === 'function' &&
|
|
1085
|
+
e.target.closest('.fb-search-panel');
|
|
1086
|
+
if (isEditableField && !insideSearchPanel) return;
|
|
1087
|
+
|
|
1088
|
+
e.preventDefault();
|
|
1089
|
+
const panel = this._ensureFileBrowser ? this._ensureFileBrowser() : this._fileBrowserPanel;
|
|
1090
|
+
if (!panel) return;
|
|
1091
|
+
if (!panel.isOpen()) panel.open();
|
|
1092
|
+
if (typeof panel.toggleSearchPanel === 'function') panel.toggleSearchPanel();
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1058
1096
|
// Header overflow menu (tablet/mobile three-dot button)
|
|
1059
1097
|
this._setupOverflowMenu();
|
|
1060
1098
|
|
|
@@ -2914,6 +2952,81 @@ class ClaudeCodeWebInterface {
|
|
|
2914
2952
|
});
|
|
2915
2953
|
}
|
|
2916
2954
|
|
|
2955
|
+
/**
|
|
2956
|
+
* Wire up clickable file paths in a terminal:
|
|
2957
|
+
* 1. xterm registerLinkProvider for hover-underline + click-to-open
|
|
2958
|
+
* (synchronous regex only — NO network I/O on render).
|
|
2959
|
+
* 2. TerminalPathDetector for the right-click "Open in File Viewer /
|
|
2960
|
+
* Edit / Download" context menu (selection-driven).
|
|
2961
|
+
*
|
|
2962
|
+
* Safe to call multiple times for the same terminal — each call returns a
|
|
2963
|
+
* disposable that we track on the terminal object so we can clean up if
|
|
2964
|
+
* the terminal is destroyed.
|
|
2965
|
+
*/
|
|
2966
|
+
_setupTerminalLinking(terminal) {
|
|
2967
|
+
if (!terminal || !window.fileBrowser) return;
|
|
2968
|
+
|
|
2969
|
+
// 1) Link provider — clickable links for paths/filenames in output.
|
|
2970
|
+
if (typeof window.fileBrowser.attachLinkProvider === 'function' &&
|
|
2971
|
+
typeof terminal.registerLinkProvider === 'function' &&
|
|
2972
|
+
!terminal._fbLinkProvider) {
|
|
2973
|
+
try {
|
|
2974
|
+
terminal._fbLinkProvider = window.fileBrowser.attachLinkProvider({
|
|
2975
|
+
terminal: terminal,
|
|
2976
|
+
authFetch: (url, opts) => this.authFetch(url, opts),
|
|
2977
|
+
openInViewer: (path, line, col) => this.openFileInViewer(path, line, col),
|
|
2978
|
+
getCwd: () => this.getCurrentWorkingDir(),
|
|
2979
|
+
feedback: window.feedback,
|
|
2980
|
+
});
|
|
2981
|
+
} catch (err) {
|
|
2982
|
+
console.warn('[file-browser] link provider attach failed:', err);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
// 2) Right-click selection-based context menu (lazy panel via getter).
|
|
2987
|
+
if (typeof window.fileBrowser.TerminalPathDetector === 'function' &&
|
|
2988
|
+
!terminal._fbPathDetector) {
|
|
2989
|
+
try {
|
|
2990
|
+
terminal._fbPathDetector = new window.fileBrowser.TerminalPathDetector({
|
|
2991
|
+
getFileBrowserPanel: () => this._ensureFileBrowser(),
|
|
2992
|
+
authFetch: (url, opts) => this.authFetch(url, opts),
|
|
2993
|
+
terminal: terminal,
|
|
2994
|
+
app: this,
|
|
2995
|
+
});
|
|
2996
|
+
terminal._fbPathDetector.init();
|
|
2997
|
+
} catch (err) {
|
|
2998
|
+
console.warn('[file-browser] path detector init failed:', err);
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// 3) Lifecycle: on terminal disposal, release the link provider, the
|
|
3003
|
+
// path-detector handlers, and the floating menu DOM node. Without
|
|
3004
|
+
// this, splitting + closing terminals leaks document-level
|
|
3005
|
+
// listeners and orphaned <div class="fb-terminal-context-menu">
|
|
3006
|
+
// nodes (peer-review MEDIUM-1 on commit 9a05963).
|
|
3007
|
+
if (typeof terminal.onDispose === 'function' && !terminal._fbLinkingDisposeWired) {
|
|
3008
|
+
terminal._fbLinkingDisposeWired = true;
|
|
3009
|
+
try {
|
|
3010
|
+
terminal.onDispose(() => {
|
|
3011
|
+
try {
|
|
3012
|
+
if (terminal._fbLinkProvider && typeof terminal._fbLinkProvider.dispose === 'function') {
|
|
3013
|
+
terminal._fbLinkProvider.dispose();
|
|
3014
|
+
}
|
|
3015
|
+
} catch (_) {}
|
|
3016
|
+
try {
|
|
3017
|
+
if (terminal._fbPathDetector && typeof terminal._fbPathDetector.destroy === 'function') {
|
|
3018
|
+
terminal._fbPathDetector.destroy();
|
|
3019
|
+
}
|
|
3020
|
+
} catch (_) {}
|
|
3021
|
+
terminal._fbLinkProvider = null;
|
|
3022
|
+
terminal._fbPathDetector = null;
|
|
3023
|
+
});
|
|
3024
|
+
} catch (err) {
|
|
3025
|
+
console.warn('[file-browser] onDispose wiring failed:', err);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
2917
3030
|
setupTerminalContextMenu() {
|
|
2918
3031
|
const menu = document.getElementById('termContextMenu');
|
|
2919
3032
|
if (!menu) return;
|
|
@@ -3632,17 +3745,24 @@ class ClaudeCodeWebInterface {
|
|
|
3632
3745
|
}
|
|
3633
3746
|
|
|
3634
3747
|
// File Browser Methods
|
|
3635
|
-
|
|
3748
|
+
_ensureFileBrowser() {
|
|
3636
3749
|
if (!this._fileBrowserPanel && window.fileBrowser) {
|
|
3637
3750
|
this._fileBrowserPanel = new window.fileBrowser.FileBrowserPanel({
|
|
3638
3751
|
app: this,
|
|
3639
3752
|
authFetch: (url, opts) => this.authFetch(url, opts),
|
|
3753
|
+
// initialPath kept as a back-compat fallback; getCwd is the
|
|
3754
|
+
// source of truth on each open() so a session switch between
|
|
3755
|
+
// opens picks up the new cwd (per ADR-0016 / task #14).
|
|
3640
3756
|
initialPath: this.getCurrentWorkingDir(),
|
|
3757
|
+
getCwd: () => this.getCurrentWorkingDir(),
|
|
3641
3758
|
});
|
|
3642
3759
|
}
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3760
|
+
return this._fileBrowserPanel;
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
toggleFileBrowser() {
|
|
3764
|
+
const panel = this._ensureFileBrowser();
|
|
3765
|
+
if (panel) panel.toggle();
|
|
3646
3766
|
}
|
|
3647
3767
|
|
|
3648
3768
|
// VS Code Tunnel Methods
|
|
@@ -3686,16 +3806,15 @@ class ClaudeCodeWebInterface {
|
|
|
3686
3806
|
}
|
|
3687
3807
|
}
|
|
3688
3808
|
|
|
3689
|
-
openFileInViewer(filePath) {
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
this._fileBrowserPanel.openToFile(filePath);
|
|
3809
|
+
openFileInViewer(filePath, line, col) {
|
|
3810
|
+
const panel = this._ensureFileBrowser();
|
|
3811
|
+
if (panel) {
|
|
3812
|
+
panel.openToFile(filePath);
|
|
3813
|
+
// Future: pass {line, col} to Monaco viewer for cursor placement
|
|
3814
|
+
// (Stage 1.5 work — for now we just open the file).
|
|
3815
|
+
if (line && this._fileBrowserPanel) {
|
|
3816
|
+
this._fileBrowserPanel._pendingJumpTo = { line: line, col: col || 1 };
|
|
3817
|
+
}
|
|
3699
3818
|
}
|
|
3700
3819
|
}
|
|
3701
3820
|
|
package/src/public/auth.js
CHANGED
|
@@ -195,6 +195,28 @@ class AuthManager {
|
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Append the auth token as a `?token=` query param.
|
|
200
|
+
*
|
|
201
|
+
* Use this for asset URLs that the browser fetches WITHOUT being able
|
|
202
|
+
* to attach custom headers — `<img src>`, `<iframe src>`, PDF.js
|
|
203
|
+
* `getDocument({url})`, and any other browser-driven fetch where
|
|
204
|
+
* `getAuthHeaders()` can't be threaded through. The auth middleware
|
|
205
|
+
* accepts both `Authorization: Bearer <t>` and `?token=<t>` so this is
|
|
206
|
+
* the canonical fallback when the header path isn't available.
|
|
207
|
+
*
|
|
208
|
+
* Trade-off: query-param tokens can leak into server access logs and
|
|
209
|
+
* `Referer` headers. The download endpoint mitigates with
|
|
210
|
+
* `Cache-Control: no-store` + `X-Content-Type-Options: nosniff`. A
|
|
211
|
+
* future hardening pass should migrate to short-lived HMAC-signed
|
|
212
|
+
* file-scoped tokens; tracked separately.
|
|
213
|
+
*/
|
|
214
|
+
appendAuthToUrl(url) {
|
|
215
|
+
if (!this.token) return url;
|
|
216
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
217
|
+
return `${url}${separator}token=${encodeURIComponent(this.token)}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
198
220
|
getWebSocketUrl(baseUrl) {
|
|
199
221
|
if (!this.token) return baseUrl;
|
|
200
222
|
const separator = baseUrl.includes('?') ? '&' : '?';
|