clay-server 2.41.1-beta.1 → 2.42.0-beta.1

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.
@@ -222,9 +222,9 @@ function appendCliRecord(obj, state, history) {
222
222
  }
223
223
  }
224
224
 
225
- function readCliSessionHistory(cwd, sessionId) {
225
+ function readCliSessionHistory(home, cwd, sessionId) {
226
226
  var encoded = encodeCwd(cwd);
227
- var filePath = path.join(REAL_HOME, ".claude", "projects", encoded, sessionId + ".jsonl");
227
+ var filePath = path.join(home || REAL_HOME, ".claude", "projects", encoded, sessionId + ".jsonl");
228
228
 
229
229
  return new Promise(function (resolve) {
230
230
  var history = [];
@@ -265,15 +265,15 @@ function readCliSessionHistory(cwd, sessionId) {
265
265
  // are small enough that blocking on a local read is fine.
266
266
  // Modified-time (ms) of a CLI session's jsonl, or 0 if missing. Lets callers
267
267
  // cheaply detect that the transcript grew (e.g. after a TUI turn) and re-read.
268
- function cliSessionFileMtime(cwd, sessionId) {
268
+ function cliSessionFileMtime(home, cwd, sessionId) {
269
269
  var encoded = encodeCwd(cwd);
270
- var filePath = path.join(REAL_HOME, ".claude", "projects", encoded, sessionId + ".jsonl");
270
+ var filePath = path.join(home || REAL_HOME, ".claude", "projects", encoded, sessionId + ".jsonl");
271
271
  try { return fs.statSync(filePath).mtimeMs; } catch (e) { return 0; }
272
272
  }
273
273
 
274
- function readCliSessionHistorySync(cwd, sessionId) {
274
+ function readCliSessionHistorySync(home, cwd, sessionId) {
275
275
  var encoded = encodeCwd(cwd);
276
- var filePath = path.join(REAL_HOME, ".claude", "projects", encoded, sessionId + ".jsonl");
276
+ var filePath = path.join(home || REAL_HOME, ".claude", "projects", encoded, sessionId + ".jsonl");
277
277
  var raw;
278
278
  try { raw = fs.readFileSync(filePath, "utf8"); } catch (e) { return []; }
279
279
  var history = [];
@@ -260,7 +260,8 @@ function attachSessions(ctx) {
260
260
  }
261
261
  var sid = session.cliSessionId;
262
262
  var localId = session.localId;
263
- var cmd = "claude --resume " + sid + "; exit\n";
263
+ var resumeSkip = session.dangerouslySkipPermissions ? " --dangerously-skip-permissions" : "";
264
+ var cmd = "claude --resume " + sid + resumeSkip + "; exit\n";
264
265
  var term = tm.create(80, 24, getOsUserInfoForWs(ws), ws, {
265
266
  initialInput: cmd,
266
267
  kind: "tui-session",
@@ -302,7 +303,8 @@ function attachSessions(ctx) {
302
303
  // turn appended messages), not just when history is empty. The earlier
303
304
  // "already hydrated" short-circuit kept stale history after a
304
305
  // Resume -> chat -> Close cycle (showed A instead of A').
305
- var mtime = cliSess.cliSessionFileMtime(cwd, session.cliSessionId);
306
+ var home = resolveSessionHome(session);
307
+ var mtime = cliSess.cliSessionFileMtime(home, cwd, session.cliSessionId);
306
308
  var fresh = (typeof session.terminalId !== "number") &&
307
309
  Array.isArray(session.history) && session.history.length > 0 &&
308
310
  session._historyMtime === mtime;
@@ -311,7 +313,7 @@ function attachSessions(ctx) {
311
313
  // Synchronous read: switch_session must populate session.history before
312
314
  // the session_switched broadcast replays it. readCliSessionHistory is
313
315
  // Promise-based and would resolve too late (the transcript came up empty).
314
- try { history = cliSess.readCliSessionHistorySync(cwd, session.cliSessionId); } catch (e) { history = null; }
316
+ try { history = cliSess.readCliSessionHistorySync(home, cwd, session.cliSessionId); } catch (e) { history = null; }
315
317
  if (Array.isArray(history)) {
316
318
  session.history = history;
317
319
  session._historyMtime = mtime;
@@ -436,11 +438,15 @@ function attachSessions(ctx) {
436
438
  sessionOpts.mode = "tui";
437
439
  sessionOpts.cliSessionId = crypto.randomUUID();
438
440
  sessionOpts.vendor = sessionOpts.vendor || "claude";
441
+ // Per-session bypass-permissions: TUI shell command only. The flag is
442
+ // persisted on the session so lazy-resume re-spawns the same way.
443
+ if (msg.dangerouslySkipPermissions) sessionOpts.dangerouslySkipPermissions = true;
439
444
  newSess = sm.createSessionRaw(sessionOpts);
440
445
  if (tm) {
441
446
  var tuiSid = newSess.cliSessionId;
442
447
  var tuiLocalId = newSess.localId;
443
- var tuiCmd = "claude --session-id " + tuiSid + "; exit\n";
448
+ var tuiSkip = newSess.dangerouslySkipPermissions ? " --dangerously-skip-permissions" : "";
449
+ var tuiCmd = "claude --session-id " + tuiSid + tuiSkip + "; exit\n";
444
450
  var tuiTerm = tm.create(80, 24, getOsUserInfoForWs(ws), ws, {
445
451
  initialInput: tuiCmd,
446
452
  kind: "tui-session",
@@ -1200,7 +1206,7 @@ function attachSessions(ctx) {
1200
1206
  } else {
1201
1207
  // Read history from CLI session files
1202
1208
  var cliSess = require("./cli-sessions");
1203
- return cliSess.readCliSessionHistory(cwd, result.sessionId).then(function(history) {
1209
+ return cliSess.readCliSessionHistory(resolveSessionHome(session), cwd, result.sessionId).then(function(history) {
1204
1210
  var forked = sm.resumeSession(result.sessionId, { history: history, title: forkTitle }, ws);
1205
1211
  if (forked) {
1206
1212
  ws._clayActiveSession = forked.localId;
@@ -1040,6 +1040,28 @@
1040
1040
  opacity: 1;
1041
1041
  }
1042
1042
 
1043
+ /* Split button: main action + chevron sharing one grid cell. */
1044
+ .session-top-action-split {
1045
+ display: flex;
1046
+ align-items: stretch;
1047
+ gap: 2px;
1048
+ min-width: 0;
1049
+ }
1050
+
1051
+ .session-top-action-split .split-main {
1052
+ flex: 1 1 auto;
1053
+ width: auto;
1054
+ }
1055
+
1056
+ .session-top-action-split .split-chevron {
1057
+ flex: 0 0 auto;
1058
+ width: 28px;
1059
+ min-width: 28px;
1060
+ padding: 0;
1061
+ justify-content: center;
1062
+ gap: 0;
1063
+ }
1064
+
1043
1065
  .session-favorites-section {
1044
1066
  min-height: 6px;
1045
1067
  margin: 0 8px;
@@ -296,11 +296,14 @@ function renderSessionTopActions() {
296
296
  var wrap = document.createElement("div");
297
297
  wrap.className = "session-top-actions";
298
298
 
299
- // Claude: single button. TUI vs GUI is the user's claudeOpenMode pref
300
- // (set in user settings), so we send no explicit mode and let the server
301
- // apply it - no per-click chevron/menu needed.
299
+ // Claude: split button. The main button creates a session using the user's
300
+ // claudeOpenMode pref (server applies it). The chevron opens a menu with
301
+ // alternate launch modes (e.g. bypass-permissions, TUI shell only).
302
+ var claudeCell = document.createElement("div");
303
+ claudeCell.className = "session-top-action-split";
304
+
302
305
  var claudeBtn = document.createElement("button");
303
- claudeBtn.className = "session-top-action";
306
+ claudeBtn.className = "session-top-action split-main";
304
307
  claudeBtn.type = "button";
305
308
  claudeBtn.title = "New Claude session";
306
309
  claudeBtn.innerHTML = '<img src="/claude-code-avatar.png" class="session-top-action-icon" alt=""><span>Claude</span>';
@@ -309,7 +312,23 @@ function renderSessionTopActions() {
309
312
  getWs().send(JSON.stringify({ type: "new_session", vendor: "claude" }));
310
313
  }
311
314
  });
312
- wrap.appendChild(claudeBtn);
315
+ claudeCell.appendChild(claudeBtn);
316
+
317
+ var claudeChevron = document.createElement("button");
318
+ claudeChevron.className = "session-top-action split-chevron";
319
+ claudeChevron.type = "button";
320
+ claudeChevron.title = "More Claude launch options";
321
+ claudeChevron.setAttribute("aria-label", "More Claude launch options");
322
+ claudeChevron.innerHTML = iconHtml("chevron-down");
323
+ claudeChevron.addEventListener("click", function (e) {
324
+ e.preventDefault();
325
+ e.stopPropagation();
326
+ if (sessionCtxMenu) { closeSessionCtxMenu(); return; }
327
+ showClaudeStartMenu(claudeChevron);
328
+ });
329
+ claudeCell.appendChild(claudeChevron);
330
+
331
+ wrap.appendChild(claudeCell);
313
332
 
314
333
  // Codex: always GUI (no TUI adapter for Codex).
315
334
  var codexBtn = document.createElement("button");
@@ -327,6 +346,53 @@ function renderSessionTopActions() {
327
346
  return wrap;
328
347
  }
329
348
 
349
+ // Dropdown anchored to the Claude split-button chevron. Reuses the
350
+ // session-ctx-menu element/var so the global document click handler closes it.
351
+ function showClaudeStartMenu(anchorBtn) {
352
+ closeSessionCtxMenu();
353
+
354
+ var menu = document.createElement("div");
355
+ menu.className = "session-ctx-menu";
356
+
357
+ var skipItem = document.createElement("button");
358
+ skipItem.className = "session-ctx-item";
359
+ skipItem.innerHTML = iconHtml("shield-off") + " <span>Skip permissions (TUI)</span>";
360
+ skipItem.title = "Start a terminal session with --dangerously-skip-permissions";
361
+ skipItem.addEventListener("click", function (e) {
362
+ e.stopPropagation();
363
+ closeSessionCtxMenu();
364
+ if (getWs() && store.get('connected')) {
365
+ getWs().send(JSON.stringify({
366
+ type: "new_session",
367
+ vendor: "claude",
368
+ mode: "tui",
369
+ dangerouslySkipPermissions: true,
370
+ }));
371
+ }
372
+ });
373
+ menu.appendChild(skipItem);
374
+
375
+ document.body.appendChild(menu);
376
+ sessionCtxMenu = menu;
377
+ refreshIcons();
378
+
379
+ requestAnimationFrame(function () {
380
+ var btnRect = anchorBtn.getBoundingClientRect();
381
+ menu.style.position = "fixed";
382
+ menu.style.top = (btnRect.bottom + 2) + "px";
383
+ menu.style.left = btnRect.left + "px";
384
+ menu.style.right = "auto";
385
+ var menuRect = menu.getBoundingClientRect();
386
+ if (menuRect.right > window.innerWidth - 8) {
387
+ menu.style.left = "auto";
388
+ menu.style.right = (window.innerWidth - btnRect.right) + "px";
389
+ }
390
+ if (menuRect.bottom > window.innerHeight - 8) {
391
+ menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
392
+ }
393
+ });
394
+ }
395
+
330
396
  function runSessionSearch(query) {
331
397
  var normalizedQuery = query || "";
332
398
  var trimmedQuery = normalizedQuery.trim();
package/lib/sessions.js CHANGED
@@ -100,6 +100,9 @@ function createSessionManager(opts) {
100
100
  // the click handler will respawn the PTY via `claude --resume` when
101
101
  // the user reopens the session.
102
102
  if (session.mode === "tui") metaObj.mode = "tui";
103
+ // Born-TUI sessions launched in bypass-permissions mode persist the flag
104
+ // so lazy-resume (`claude --resume`) re-spawns with the same flag.
105
+ if (session.dangerouslySkipPermissions) metaObj.dangerouslySkipPermissions = true;
103
106
  if (session.sessionVisibility) metaObj.sessionVisibility = session.sessionVisibility;
104
107
  if (session.bookmarked) metaObj.bookmarked = true;
105
108
  if (typeof session.favoriteOrder === "number") metaObj.favoriteOrder = session.favoriteOrder;
@@ -212,6 +215,7 @@ function createSessionManager(opts) {
212
215
  // here so it shows up in the sidebar with the right icon; the
213
216
  // switch_session handler respawns the PTY on click.
214
217
  session.mode = (m.mode === "tui") ? "tui" : "gui";
218
+ session.dangerouslySkipPermissions = !!m.dangerouslySkipPermissions;
215
219
  session.terminalId = null;
216
220
  session.runtimeMode = null;
217
221
  session.runtimeTerminalId = null;
@@ -497,6 +501,7 @@ function createSessionManager(opts) {
497
501
  favoriteOrder: null,
498
502
  vendor: (sessionOpts && sessionOpts.vendor) || null,
499
503
  mode: (sessionOpts && sessionOpts.mode === "tui") ? "tui" : "gui",
504
+ dangerouslySkipPermissions: !!(sessionOpts && sessionOpts.dangerouslySkipPermissions),
500
505
  terminalId: null,
501
506
  };
502
507
  sessions.set(localId, session);
package/lib/ws-schema.js CHANGED
@@ -19,7 +19,7 @@ var schema = {
19
19
  "switch_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Switch the active session by local ID" },
20
20
  "resume_tui_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Spawn the claude --resume PTY for a TUI session shown read-only (lazy resume)" },
21
21
  "suspend_tui_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Close a live TUI session's PTY now but keep it resumable (explicit Close)" },
22
- "new_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Create a new blank session" },
22
+ "new_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Create a new blank session (opts: vendor, mode, sessionVisibility, dangerouslySkipPermissions)" },
23
23
  "delete_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Delete a session by ID" },
24
24
  "rename_session": { direction: "c2s", handler: "lib/project-sessions.js", description: "Rename a session" },
25
25
  "set_session_bookmark": { direction: "c2s", handler: "lib/project-sessions.js", description: "Bookmark or unbookmark a session in the sidebar" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.41.1-beta.1",
3
+ "version": "2.42.0-beta.1",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",