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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-or-die",
3
- "version": "0.1.62",
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 when the CSS theme changes (data-theme attribute)
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' && this.terminal) {
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
- toggleFileBrowser() {
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
- if (this._fileBrowserPanel) {
3644
- this._fileBrowserPanel.toggle();
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
- if (!this._fileBrowserPanel && window.fileBrowser) {
3691
- this._fileBrowserPanel = new window.fileBrowser.FileBrowserPanel({
3692
- app: this,
3693
- authFetch: (url, opts) => this.authFetch(url, opts),
3694
- initialPath: this.getCurrentWorkingDir(),
3695
- });
3696
- }
3697
- if (this._fileBrowserPanel) {
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
 
@@ -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('?') ? '&' : '?';