ai-or-die 0.1.69 → 0.1.70

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.69",
3
+ "version": "0.1.70",
4
4
  "description": "Universal AI coding terminal — Claude, Copilot, Gemini & more in your browser",
5
5
  "main": "src/server.js",
6
6
  "bin": {
package/src/public/app.js CHANGED
@@ -2182,6 +2182,16 @@ class ClaudeCodeWebInterface {
2182
2182
  this._sessionWorkingDirs.set(message.sessionId, message.workingDir);
2183
2183
  }
2184
2184
  this.updateSessionButton(message.sessionName);
2185
+
2186
+ // Re-root the (singleton) file-browser panel to the newly
2187
+ // active session's dir. open() short-circuits when already
2188
+ // open, so without this an open panel would keep showing the
2189
+ // previous tab's directory after a tab switch. Fires only when
2190
+ // the panel is open and the session actually changed.
2191
+ if (this._fileBrowserPanel &&
2192
+ typeof this._fileBrowserPanel.notifyActiveSessionChanged === 'function') {
2193
+ this._fileBrowserPanel.notifyActiveSessionChanged(message.sessionId);
2194
+ }
2185
2195
 
2186
2196
  // Update tab status
2187
2197
  if (this.sessionTabManager) {
@@ -4138,6 +4148,10 @@ class ClaudeCodeWebInterface {
4138
4148
  // opens picks up the new cwd (per ADR-0016 / task #14).
4139
4149
  initialPath: this.getCurrentWorkingDir(),
4140
4150
  getCwd: () => this.getCurrentWorkingDir(),
4151
+ // Active session id → sent as ?session on /api/files so the
4152
+ // server can resolve the per-tab default root even when the
4153
+ // client cwd cache is cold (e.g. just after a page reload).
4154
+ getSessionId: () => this.currentClaudeSessionId,
4141
4155
  });
4142
4156
  }
4143
4157
  return this._fileBrowserPanel;
@@ -338,6 +338,17 @@
338
338
  // callback is also tolerated (defensive coding per agent-instructions/05).
339
339
  this.getCwd = typeof options.getCwd === 'function' ? options.getCwd : null;
340
340
 
341
+ // Optional callback returning the active session's id. Sent as the
342
+ // `session` query param on /api/files so the SERVER can resolve the
343
+ // default root (session.liveCwd || session.workingDir) even when the
344
+ // client's cwd cache is cold (e.g. right after a page reload). Tolerant
345
+ // of falsy/throwing callbacks like getCwd.
346
+ this.getSessionId = typeof options.getSessionId === 'function' ? options.getSessionId : null;
347
+ // Server-reported "home" for the active session (its working dir). Set
348
+ // from each /api/files response; navigateHome() roots here so "Home"
349
+ // means the tab's session dir, not the server's global baseFolder.
350
+ this._homePath = null;
351
+
341
352
  this._open = false;
342
353
  this._currentPath = null;
343
354
  this._basePath = null;
@@ -858,9 +869,23 @@
858
869
  var self = this;
859
870
  var params = new URLSearchParams();
860
871
  if (dirPath) params.append('path', dirPath);
872
+ // Always forward the active session id (when known). The server uses it
873
+ // ONLY to pick the default root when no `path` is given, and to report
874
+ // `home`; it is ignored when `path` is present, so explicit navigation
875
+ // (breadcrumbs / up / folder clicks) is unaffected.
876
+ var sid = null;
877
+ if (this.getSessionId) { try { sid = this.getSessionId(); } catch (_) { sid = null; } }
878
+ if (sid) params.append('session', sid);
879
+ this._lastRenderedSession = sid;
861
880
  params.append('limit', '500');
862
881
  params.append('offset', '0');
863
882
 
883
+ // Monotonic request token: tab switches / rapid folder clicks can leave
884
+ // several /api/files fetches in flight at once. Only the LATEST request is
885
+ // allowed to commit its response, so a slow earlier fetch can't overwrite
886
+ // the UI (or _homePath / the watcher root) with stale data.
887
+ var reqId = (this._navSeq = (this._navSeq || 0) + 1);
888
+
864
889
  this._statusBar.textContent = 'Loading...';
865
890
 
866
891
  this.authFetch('/api/files?' + params.toString())
@@ -869,14 +894,28 @@
869
894
  return resp.json();
870
895
  })
871
896
  .then(function (data) {
897
+ if (reqId !== self._navSeq) return; // superseded by a newer navigateTo
872
898
  self._currentPath = data.currentPath;
873
899
  self._basePath = data.baseFolder;
900
+ // `home` is the session's working dir (server-resolved); navigateHome
901
+ // roots here. Falls back to baseFolder for sessionless/legacy responses.
902
+ self._homePath = data.home || data.baseFolder;
874
903
  self._items = data.items;
875
904
  self._renderBreadcrumbs();
876
905
  self._renderItems();
877
906
  self._showBrowseView();
878
907
  self._statusBar.textContent = data.totalCount + ' item' + (data.totalCount !== 1 ? 's' : '');
879
908
 
909
+ // fs-watcher: (re)connect to the dir the server actually resolved.
910
+ // open() only connects when it knows the path client-side; on a cold
911
+ // cache it passes null and the server resolves the session root here,
912
+ // so connect against data.currentPath to cover that case (and re-root
913
+ // when a tab switch re-navigates). connect() is idempotent per path.
914
+ var w = self._ensureFileWatcher();
915
+ if (w && typeof w.connect === 'function' && data.currentPath) {
916
+ try { w.connect(data.currentPath); } catch (_) { /* swallow */ }
917
+ }
918
+
880
919
  // fs-watcher (#41 / ADR-0017 — wire model post-ff79038): one
881
920
  // EventSource per session at panel mount, refcount-based per-path
882
921
  // subscriptions multiplexed over it. Listing direct-child paths
@@ -900,6 +939,7 @@
900
939
  }
901
940
  })
902
941
  .catch(function (err) {
942
+ if (reqId !== self._navSeq) return; // superseded; don't clobber newer status
903
943
  self._statusBar.textContent = 'Error: ' + err.message;
904
944
  });
905
945
  };
@@ -916,7 +956,9 @@
916
956
 
917
957
  FileBrowserPanel.prototype.navigateHome = function () {
918
958
  this._markManualNav();
919
- this.navigateTo(this._basePath);
959
+ // "Home" is the active session's working dir (server-reported `home`),
960
+ // falling back to the sandbox base if we haven't loaded a listing yet.
961
+ this.navigateTo(this._homePath || this._basePath);
920
962
  };
921
963
 
922
964
  // ---------------------------------------------------------------------------
@@ -945,6 +987,22 @@
945
987
  return this._activeSessionId() === sessionId;
946
988
  };
947
989
 
990
+ /**
991
+ * Entry point for app.js when the active tab/session changes. The panel is
992
+ * a singleton shared across tabs, so a tab switch must re-root it to the
993
+ * new session's working dir — open() short-circuits when already open and
994
+ * would otherwise keep showing the previous tab's directory. Re-navigates
995
+ * with NO explicit path so the server resolves the new session's root from
996
+ * the `?session` param (getSessionId() now returns the new id). No-op when
997
+ * the panel is closed or the session is unchanged. _markManualNav() is NOT
998
+ * called: a tab switch should re-root and resume following the new tab.
999
+ */
1000
+ FileBrowserPanel.prototype.notifyActiveSessionChanged = function (sessionId) {
1001
+ if (!this._open) return;
1002
+ if (!sessionId || sessionId === this._lastRenderedSession) return;
1003
+ this.navigateTo(null);
1004
+ };
1005
+
948
1006
  /**
949
1007
  * Returns true when the panel is currently configured to follow the
950
1008
  * given session's OSC-7 CWD. Defaults to true on first lookup, and
package/src/server.js CHANGED
@@ -870,7 +870,21 @@ class ClaudeCodeWebServer {
870
870
 
871
871
  setupExpress() {
872
872
  this.app.use(cors());
873
- this.app.use(express.json());
873
+ // Global JSON parser for normal endpoints (Express default ~100kb limit).
874
+ // The upload route mounts its own higher-limit parser (see
875
+ // POST /api/files/upload below); exempt it here so a base64 file body
876
+ // isn't rejected by the ~100kb default before the route runs. The
877
+ // trailing-slash normalization matches exactly the set of paths Express
878
+ // routes to that handler (`/api/files/upload` and `/api/files/upload/`).
879
+ const _globalJsonParser = express.json();
880
+ this.app.use((req, res, next) => {
881
+ // Case-insensitive + trailing-slash-normalized to match Express's
882
+ // default route matching (case-insensitive routing), so every form that
883
+ // reaches the upload handler is exempt from the ~100kb global parser.
884
+ const p = req.path.replace(/\/+$/, '').toLowerCase() || '/';
885
+ if (p === '/api/files/upload') return next();
886
+ return _globalJsonParser(req, res, next);
887
+ });
874
888
 
875
889
  // Serve manifest.json with correct MIME type
876
890
  this.app.get('/manifest.json', (req, res) => {
@@ -1403,7 +1417,30 @@ class ClaudeCodeWebServer {
1403
1417
 
1404
1418
  // GET /api/files — List directory (files + folders), paginated
1405
1419
  this.app.get('/api/files', (req, res) => {
1406
- const requestedPath = req.query.path || this.baseFolder;
1420
+ // Per-tab file-browser root. Resolve the requesting session's home dir
1421
+ // (live OSC 7 cwd if tracked, else the spawn dir) when a `session` id is
1422
+ // supplied and its dir still validates. Used as (a) the default root
1423
+ // when the client sends no explicit `path`, and (b) the `home` value the
1424
+ // client points "Home" at — so Home stays the tab's dir even while
1425
+ // browsing subdirs. Mirrors GET /api/files/find. Falls back to baseFolder
1426
+ // for unknown/stale sessions so the browser never 403s on open.
1427
+ let sessionHome = null;
1428
+ const sid = typeof req.query.session === 'string' ? req.query.session : '';
1429
+ if (sid) {
1430
+ const session = this.claudeSessions.get(sid);
1431
+ if (session) {
1432
+ const candidate = session.liveCwd || session.workingDir;
1433
+ if (candidate && this.validatePath(candidate).valid) {
1434
+ sessionHome = candidate;
1435
+ } else {
1436
+ // Known session but its dir is missing or no longer inside the
1437
+ // sandbox — a real misconfiguration worth logging. (Unknown session
1438
+ // ids fall through silently: expected during cold-cache races.)
1439
+ console.warn(`/api/files: session ${sid} working dir unavailable; using baseFolder`);
1440
+ }
1441
+ }
1442
+ }
1443
+ const requestedPath = req.query.path || sessionHome || this.baseFolder;
1407
1444
  const validation = this.validatePath(requestedPath);
1408
1445
  if (!validation.valid) {
1409
1446
  return res.status(403).json({ error: validation.error });
@@ -1480,7 +1517,7 @@ class ClaudeCodeWebServer {
1480
1517
  totalCount,
1481
1518
  offset,
1482
1519
  limit,
1483
- home: normalizePath(this.baseFolder),
1520
+ home: normalizePath(sessionHome || this.baseFolder),
1484
1521
  baseFolder: normalizePath(this.baseFolder),
1485
1522
  });
1486
1523
  } catch (error) {
@@ -2739,7 +2776,12 @@ class ClaudeCodeWebServer {
2739
2776
  // per spec ("user shell config is sacrosanct" applies to .gitignore
2740
2777
  // too — we don't want to silently introduce a new tracked-by-default
2741
2778
  // side effect on the user's repo).
2742
- this.app.post('/api/files/upload', express.json({ limit: '10mb' }), async (req, res) => {
2779
+ // Route parser limit is sized for base64 of the 10 MB decoded cap
2780
+ // (~14 MB) plus the small JSON envelope. The decoded-size guard below
2781
+ // (buffer.length > 10 MB) remains the real per-file cap. This parser is
2782
+ // the ONLY one that runs for this route — the global parser above skips
2783
+ // `/api/files/upload`, so this limit governs (not the ~100kb default).
2784
+ this.app.post('/api/files/upload', express.json({ limit: '20mb' }), async (req, res) => {
2743
2785
  const { targetDir, fileName, content, overwrite } = req.body;
2744
2786
  if (!targetDir || !fileName || !content) {
2745
2787
  return res.status(400).json({ error: 'targetDir, fileName, and content are required' });
@@ -2932,6 +2974,24 @@ class ClaudeCodeWebServer {
2932
2974
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
2933
2975
  }
2934
2976
  });
2977
+
2978
+ // Body-parser error handler (4-arg, must be registered AFTER routes).
2979
+ // express.json() rejects oversized/malformed bodies via next(err) BEFORE
2980
+ // the route runs; without this, Express's default handler returns HTML,
2981
+ // which the JSON API clients can't parse. We key on err.type — the marker
2982
+ // body-parser stamps on its own errors — so unrelated next(err) calls are
2983
+ // left to the default handler.
2984
+ this.app.use((err, req, res, next) => {
2985
+ if (res.headersSent || !err || !err.type) return next(err);
2986
+ if (err.type === 'entity.too.large') {
2987
+ return res.status(413).json({ error: 'Request body too large' });
2988
+ }
2989
+ if (err.type === 'entity.parse.failed' || err.type === 'encoding.unsupported'
2990
+ || err.type === 'charset.unsupported' || err.type === 'entity.verify.failed') {
2991
+ return res.status(err.status || err.statusCode || 400).json({ error: 'Invalid request body' });
2992
+ }
2993
+ return next(err);
2994
+ });
2935
2995
  }
2936
2996
 
2937
2997
  /**