clay-server 3.4.0-beta.1 → 3.4.0-beta.11

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.
Files changed (68) hide show
  1. package/lib/daemon-projects.js +3 -3
  2. package/lib/daemon.js +1 -1
  3. package/lib/project-connection.js +20 -5
  4. package/lib/project-debate.js +4 -0
  5. package/lib/project-mate-interaction.js +21 -1
  6. package/lib/project-memory.js +3 -0
  7. package/lib/project-shell-command.js +160 -0
  8. package/lib/project-user-message.js +10 -0
  9. package/lib/project.js +19 -4
  10. package/lib/public/antigravity-avatar.png +0 -0
  11. package/lib/public/app.js +28 -0
  12. package/lib/public/copilot-avatar.svg +5 -0
  13. package/lib/public/css/command-palette.css +22 -2
  14. package/lib/public/css/icon-strip.css +29 -13
  15. package/lib/public/css/input.css +75 -0
  16. package/lib/public/css/mates.css +6 -0
  17. package/lib/public/css/menus.css +48 -14
  18. package/lib/public/css/messages.css +9 -0
  19. package/lib/public/css/pane.css +4 -0
  20. package/lib/public/css/pwa-mobile.css +17 -0
  21. package/lib/public/css/title-bar.css +19 -0
  22. package/lib/public/grok-avatar.svg +4 -0
  23. package/lib/public/index.html +38 -7
  24. package/lib/public/junie-avatar.svg +11 -0
  25. package/lib/public/kimi-avatar.svg +4 -0
  26. package/lib/public/modules/app-header.js +4 -0
  27. package/lib/public/modules/app-messages.js +28 -5
  28. package/lib/public/modules/app-panels.js +28 -8
  29. package/lib/public/modules/app-projects.js +3 -1
  30. package/lib/public/modules/app-rendering.js +19 -4
  31. package/lib/public/modules/input.js +58 -28
  32. package/lib/public/modules/mate-sidebar.js +11 -3
  33. package/lib/public/modules/notifications.js +7 -1
  34. package/lib/public/modules/pane-bridge.js +11 -0
  35. package/lib/public/modules/pane-links.js +23 -0
  36. package/lib/public/modules/project-switcher.js +7 -6
  37. package/lib/public/modules/shell-command.js +148 -0
  38. package/lib/public/modules/sidebar-mates.js +1 -1
  39. package/lib/public/modules/sidebar-mobile.js +2 -1
  40. package/lib/public/modules/sidebar-projects.js +34 -43
  41. package/lib/public/modules/sidebar-sessions.js +6 -6
  42. package/lib/public/modules/split-pair-ui.js +3 -2
  43. package/lib/public/modules/tools.js +6 -1
  44. package/lib/public/modules/vendor-priority.js +14 -0
  45. package/lib/public/modules/vendor-selection.js +20 -0
  46. package/lib/public/modules/worktree-location.js +17 -0
  47. package/lib/public/qwen-avatar.svg +4 -0
  48. package/lib/public/style.css +3 -2
  49. package/lib/sdk-bridge.js +49 -25
  50. package/lib/sdk-message-processor.js +10 -1
  51. package/lib/session-notes-mcp-server.js +7 -1
  52. package/lib/worktree.js +9 -4
  53. package/lib/ws-schema.js +3 -0
  54. package/lib/yoke/acp-agent-profiles.js +64 -14
  55. package/lib/yoke/adapters/antigravity.js +417 -0
  56. package/lib/yoke/adapters/codex.js +98 -3
  57. package/lib/yoke/adapters/copilot.js +7 -0
  58. package/lib/yoke/adapters/grok.js +7 -0
  59. package/lib/yoke/adapters/junie.js +7 -0
  60. package/lib/yoke/adapters/kimi.js +7 -0
  61. package/lib/yoke/adapters/qwen.js +7 -0
  62. package/lib/yoke/codex-app-server.js +25 -8
  63. package/lib/yoke/index.js +67 -28
  64. package/lib/yoke/instructions.js +3 -0
  65. package/lib/yoke/vendor-registry.js +61 -6
  66. package/package.json +1 -1
  67. package/lib/public/gemini-avatar.svg +0 -11
  68. package/lib/yoke/adapters/gemini.js +0 -7
@@ -41,10 +41,10 @@ function scanAndRegisterWorktrees(relay, parentPath, parentSlug, parentIcon, par
41
41
  if (worktreeRegistry[parentSlug][j] === wtSlug) { alreadyRegistered = true; break; }
42
42
  }
43
43
  if (alreadyRegistered) continue;
44
- var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, accessible: wt.accessible };
44
+ var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, external: wt.external };
45
45
  relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
46
46
  worktreeRegistry[parentSlug].push(wtSlug);
47
- console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.accessible ? "(accessible)" : "(inaccessible)");
47
+ console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.external ? "(external)" : "(inside project folder)");
48
48
  }
49
49
  daemonSync.register(getWorktreeSyncKey(parentSlug), function () {
50
50
  rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerId);
@@ -79,7 +79,7 @@ function rescanWorktrees(relay, parentPath, parentSlug, parentIcon, parentOwnerI
79
79
  if (existingSlugs[ei] === wtSlug) { found = true; break; }
80
80
  }
81
81
  if (!found) {
82
- var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, accessible: wt.accessible };
82
+ var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, external: wt.external };
83
83
  relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
84
84
  if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
85
85
  worktreeRegistry[parentSlug].push(wtSlug);
package/lib/daemon.js CHANGED
@@ -978,7 +978,7 @@ var relay = createServer({
978
978
  if (!result.ok) return result;
979
979
  // Register the new worktree as ephemeral project
980
980
  var wtSlug = parentSlug + "--" + dirName;
981
- var wtMeta = { parentSlug: parentSlug, branch: branchName, accessible: true };
981
+ var wtMeta = { parentSlug: parentSlug, branch: branchName, external: false };
982
982
  relay.addProject(result.path, wtSlug, branchName, parent.icon, parent.ownerId, wtMeta);
983
983
  registerWorktreeSlug(parentSlug, wtSlug);
984
984
  console.log("[daemon] Created worktree:", wtSlug, "->", result.path);
@@ -22,6 +22,17 @@ function dispatchMessageSafely(handleMessage, sendTo, slug, ws, msg) {
22
22
  }
23
23
  }
24
24
 
25
+ function buildInitialModelInfo(sm, initialVendor, initialModel, initialModels) {
26
+ return {
27
+ type: "model_info",
28
+ model: initialModel || "",
29
+ models: initialModels || [],
30
+ vendor: initialVendor,
31
+ availableVendors: sm.availableVendors || [],
32
+ installedVendors: sm.installedVendors || [],
33
+ };
34
+ }
35
+
25
36
  /**
26
37
  * Attach connection/disconnection handlers to a project context.
27
38
  *
@@ -116,7 +127,7 @@ function attachConnection(ctx) {
116
127
  if (!_warmedUp) {
117
128
  _warmedUp = true;
118
129
  if (typeof warmup === "function") {
119
- try { warmup(); }
130
+ try { warmup(wsUser && wsUser.linuxUser ? wsUser.linuxUser : null); }
120
131
  catch (e) { console.error("[project-connection] warmup failed for " + slug + ":", e && e.message ? e.message : e); }
121
132
  }
122
133
  }
@@ -167,9 +178,9 @@ function attachConnection(ctx) {
167
178
  sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
168
179
  }
169
180
  var initialModel = (restoredActive && restoredActive.model) || sm.currentModel || "";
170
- if (initialModel) {
171
- sendTo(ws, { type: "model_info", model: initialModel, models: initialModels, vendor: initialVendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
172
- }
181
+ // Vendor installation state is needed even before a new project has a
182
+ // model. Always send it so reconnects cannot fall back to "Not installed."
183
+ sendTo(ws, buildInitialModelInfo(sm, initialVendor, initialModel, initialModels));
173
184
  sendTo(ws, { type: "config_state", model: initialModel, mode: sm.currentPermissionMode || "default", effort: initialEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
174
185
  sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
175
186
  sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
@@ -333,4 +344,8 @@ function attachConnection(ctx) {
333
344
  };
334
345
  }
335
346
 
336
- module.exports = { attachConnection: attachConnection, dispatchMessageSafely: dispatchMessageSafely };
347
+ module.exports = {
348
+ attachConnection: attachConnection,
349
+ buildInitialModelInfo: buildInitialModelInfo,
350
+ dispatchMessageSafely: dispatchMessageSafely,
351
+ };
@@ -604,6 +604,7 @@ function attachDebate(ctx) {
604
604
  var _modMate = matesModule.getMate(mateCtx, debate.moderatorId);
605
605
  ctx.sdk.createMentionSession({
606
606
  vendor: _modMate ? _modMate.vendor : null,
607
+ linuxUser: mateCtx.linuxUser || undefined,
607
608
  claudeMd: claudeMd,
608
609
  initialContext: digests,
609
610
  initialMessage: quickBriefPrompt,
@@ -799,6 +800,7 @@ function attachDebate(ctx) {
799
800
  var _modMate2 = matesModule.getMate(mateCtx, debate.moderatorId);
800
801
  ctx.sdk.createMentionSession({
801
802
  vendor: _modMate2 ? _modMate2.vendor : null,
803
+ linuxUser: mateCtx.linuxUser || undefined,
802
804
  claudeMd: claudeMd,
803
805
  initialContext: moderatorContext,
804
806
  initialMessage: "Begin the debate on: " + debate.topic,
@@ -994,6 +996,7 @@ function attachDebate(ctx) {
994
996
  var _panMate = matesModule.getMate(debate.mateCtx, mateId);
995
997
  ctx.sdk.createMentionSession({
996
998
  vendor: _panMate ? _panMate.vendor : null,
999
+ linuxUser: debate.mateCtx && debate.mateCtx.linuxUser ? debate.mateCtx.linuxUser : undefined,
997
1000
  claudeMd: claudeMd,
998
1001
  initialContext: panelistContext + historyContext,
999
1002
  initialMessage: "The moderator addresses you:\n\n" + moderatorText,
@@ -1530,6 +1533,7 @@ function attachDebate(ctx) {
1530
1533
  var _modMate3 = matesModule.getMate(mateCtx, debate.moderatorId);
1531
1534
  ctx.sdk.createMentionSession({
1532
1535
  vendor: _modMate3 ? _modMate3.vendor : null,
1536
+ linuxUser: mateCtx.linuxUser || undefined,
1533
1537
  claudeMd: claudeMd,
1534
1538
  initialContext: moderatorContext,
1535
1539
  initialMessage: resumePrompt,
@@ -179,6 +179,7 @@ function attachMateInteraction(ctx) {
179
179
  var _digestQueue = [];
180
180
  var _digestBusy = false;
181
181
  var _digestWorkerTurns = 0;
182
+ var _digestWorkerLinuxUser = null;
182
183
  var DIGEST_WORKER_MAX_TURNS = 20;
183
184
 
184
185
  function enqueueDigest(job) {
@@ -191,6 +192,13 @@ function attachMateInteraction(ctx) {
191
192
  _digestBusy = true;
192
193
  var job = _digestQueue.shift();
193
194
 
195
+ if (_digestWorker && _digestWorkerLinuxUser !== (job.linuxUser || null)) {
196
+ try { _digestWorker.close(); } catch (e) {}
197
+ _digestWorker = null;
198
+ _digestWorkerTurns = 0;
199
+ _digestWorkerLinuxUser = null;
200
+ }
201
+
194
202
  var mateDir = matesModule.getMateDir(job.mateCtx, job.mateId);
195
203
  var knowledgeDir = path.join(mateDir, "knowledge");
196
204
 
@@ -315,6 +323,7 @@ function attachMateInteraction(ctx) {
315
323
  try { _digestWorker.close(); } catch (e) {}
316
324
  _digestWorker = null;
317
325
  _digestWorkerTurns = 0;
326
+ _digestWorkerLinuxUser = null;
318
327
  }
319
328
 
320
329
  var responseText = "";
@@ -328,12 +337,14 @@ function attachMateInteraction(ctx) {
328
337
  console.error("[digest-worker] Error:", err);
329
338
  _digestWorker = null;
330
339
  _digestWorkerTurns = 0;
340
+ _digestWorkerLinuxUser = null;
331
341
  if (job.onError) job.onError(err);
332
342
  processDigestQueue();
333
343
  },
334
344
  });
335
345
  } else {
336
346
  sdk.createMentionSession({
347
+ linuxUser: job.linuxUser || undefined,
337
348
  claudeMd: "",
338
349
  model: "haiku",
339
350
  initialContext: "[Digest Worker] You generate memory digests. Respond with ONLY JSON.",
@@ -344,10 +355,15 @@ function attachMateInteraction(ctx) {
344
355
  onError: function (err) {
345
356
  console.error("[digest-worker] Create error:", err);
346
357
  _digestWorker = null;
358
+ _digestWorkerLinuxUser = null;
347
359
  if (job.onError) job.onError(err);
348
360
  processDigestQueue();
349
361
  },
350
- }).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function (err) {
362
+ }).then(function (ws) {
363
+ _digestWorker = ws;
364
+ _digestWorkerTurns = 1;
365
+ _digestWorkerLinuxUser = job.linuxUser || null;
366
+ }).catch(function (err) {
351
367
  if (job.onError) job.onError(err || new Error("digest worker creation failed"));
352
368
  processDigestQueue();
353
369
  });
@@ -378,6 +394,7 @@ function attachMateInteraction(ctx) {
378
394
 
379
395
  enqueueDigest({
380
396
  mateCtx: mateCtx,
397
+ linuxUser: mateCtx.linuxUser || undefined,
381
398
  mateId: mateId,
382
399
  type: "mention",
383
400
  conversationContent: conversationContent,
@@ -473,6 +490,7 @@ function attachMateInteraction(ctx) {
473
490
 
474
491
  enqueueDigest({
475
492
  mateCtx: mateCtx,
493
+ linuxUser: mateCtx.linuxUser || undefined,
476
494
  mateId: mateId,
477
495
  type: "dm",
478
496
  priorSummary: priorSummary || "",
@@ -716,6 +734,7 @@ function attachMateInteraction(ctx) {
716
734
  // Create new persistent mention session
717
735
  sdk.createMentionSession({
718
736
  vendor: mate.vendor || null,
737
+ linuxUser: getLinuxUserForSession(session) || undefined,
719
738
  claudeMd: claudeMd,
720
739
  initialContext: mentionContext,
721
740
  initialMessage: mentionFullInput,
@@ -887,6 +906,7 @@ function attachMateInteraction(ctx) {
887
906
  ].join("\n");
888
907
 
889
908
  sdk.createMentionSession({
909
+ linuxUser: mateCtx.linuxUser || undefined,
890
910
  claudeMd: "",
891
911
  model: "haiku",
892
912
  initialContext: synthesisContext,
@@ -339,6 +339,7 @@ function attachMemory(ctx) {
339
339
  var gateText = "";
340
340
  var _gateSession = null;
341
341
  sdk.createMentionSession({
342
+ linuxUser: mateCtx.linuxUser || undefined,
342
343
  claudeMd: "",
343
344
  model: "haiku",
344
345
  initialContext: gateContext,
@@ -448,6 +449,7 @@ function attachMemory(ctx) {
448
449
  var updateText = "";
449
450
  var _updateSession = null;
450
451
  sdk.createMentionSession({
452
+ linuxUser: mateCtx.linuxUser || undefined,
451
453
  claudeMd: "",
452
454
  model: "haiku",
453
455
  initialContext: updateContext,
@@ -553,6 +555,7 @@ function attachMemory(ctx) {
553
555
  var initText = "";
554
556
  var _initSession = null;
555
557
  sdk.createMentionSession({
558
+ linuxUser: mateCtx.linuxUser || undefined,
556
559
  claudeMd: "",
557
560
  model: "haiku",
558
561
  initialContext: initContext,
@@ -0,0 +1,160 @@
1
+ var { spawn } = require("child_process");
2
+ var { buildUserEnv } = require("./build-user-env");
3
+ var { wrapSpawnAsUser } = require("./os-users");
4
+
5
+ var MAX_COMMAND_LENGTH = 16 * 1024;
6
+ var MAX_OUTPUT_LENGTH = 64 * 1024;
7
+ var COMMAND_TIMEOUT_MS = 30 * 1000;
8
+
9
+ function stripTerminalCodes(value) {
10
+ return String(value || "")
11
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
12
+ .replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, "")
13
+ .replace(/\r/g, "");
14
+ }
15
+
16
+ function attachShellCommand(ctx) {
17
+ var runningBySocket = new WeakMap();
18
+
19
+ function sendResult(ws, data) {
20
+ ctx.sendTo(ws, Object.assign({ type: "shell_command_result" }, data));
21
+ }
22
+
23
+ function handleShellCommand(ws, msg) {
24
+ if (msg.type !== "shell_command") return false;
25
+
26
+ var requestId = typeof msg.requestId === "string" ? msg.requestId.slice(0, 100) : "";
27
+ var command = typeof msg.command === "string" ? msg.command.trim() : "";
28
+ if (!requestId || !command) {
29
+ sendResult(ws, { requestId: requestId, error: "Enter a command to run." });
30
+ return true;
31
+ }
32
+ if (command.length > MAX_COMMAND_LENGTH) {
33
+ sendResult(ws, { requestId: requestId, error: "Command is too long." });
34
+ return true;
35
+ }
36
+
37
+ if (ws._clayUser) {
38
+ var permissions = ctx.usersModule.getEffectivePermissions(ws._clayUser, ctx.osUsers);
39
+ if (!permissions.terminal) {
40
+ sendResult(ws, { requestId: requestId, error: "Terminal access is not permitted." });
41
+ return true;
42
+ }
43
+ }
44
+ if (runningBySocket.get(ws)) {
45
+ sendResult(ws, { requestId: requestId, error: "Another shell command is still running." });
46
+ return true;
47
+ }
48
+
49
+ var session = ctx.getSessionForWs(ws);
50
+ if (!session) {
51
+ sendResult(ws, { requestId: requestId, error: "No active session." });
52
+ return true;
53
+ }
54
+
55
+ var osUserInfo = ctx.getOsUserInfoForWs(ws);
56
+ var shell = (osUserInfo && osUserInfo.shell)
57
+ || process.env.SHELL
58
+ || (process.platform === "win32" ? process.env.COMSPEC || "cmd.exe" : "/bin/sh");
59
+ var args = process.platform === "win32" ? ["/d", "/s", "/c", command] : ["-lc", command];
60
+ var spawnOptions = {
61
+ cwd: ctx.cwd,
62
+ env: buildUserEnv(osUserInfo),
63
+ stdio: ["ignore", "pipe", "pipe"],
64
+ };
65
+ if (osUserInfo) {
66
+ spawnOptions.uid = osUserInfo.uid;
67
+ spawnOptions.gid = osUserInfo.gid;
68
+ }
69
+
70
+ var wrapped = wrapSpawnAsUser(shell, args, spawnOptions);
71
+ var child;
72
+ try {
73
+ child = spawn(wrapped.command, wrapped.args, wrapped.options);
74
+ } catch (e) {
75
+ sendResult(ws, { requestId: requestId, error: e.message || "Failed to start command." });
76
+ return true;
77
+ }
78
+
79
+ runningBySocket.set(ws, child);
80
+ var chunks = [];
81
+ var outputLength = 0;
82
+ var truncated = false;
83
+ var timedOut = false;
84
+ var finished = false;
85
+
86
+ function collect(data) {
87
+ if (outputLength >= MAX_OUTPUT_LENGTH) {
88
+ truncated = true;
89
+ return;
90
+ }
91
+ var text = data.toString("utf8");
92
+ var remaining = MAX_OUTPUT_LENGTH - outputLength;
93
+ if (text.length > remaining) {
94
+ text = text.slice(0, remaining);
95
+ truncated = true;
96
+ }
97
+ chunks.push(text);
98
+ outputLength += text.length;
99
+ }
100
+
101
+ child.stdout.on("data", collect);
102
+ child.stderr.on("data", collect);
103
+
104
+ var timer = setTimeout(function () {
105
+ timedOut = true;
106
+ try { child.kill("SIGTERM"); } catch (e) {}
107
+ setTimeout(function () {
108
+ if (!finished) {
109
+ try { child.kill("SIGKILL"); } catch (e) {}
110
+ }
111
+ }, 1000).unref();
112
+ }, COMMAND_TIMEOUT_MS);
113
+ timer.unref();
114
+
115
+ child.on("error", function (error) {
116
+ if (finished) return;
117
+ finished = true;
118
+ clearTimeout(timer);
119
+ runningBySocket.delete(ws);
120
+ sendResult(ws, { requestId: requestId, command: command, error: error.message || "Command failed." });
121
+ });
122
+
123
+ child.on("close", function (code, signal) {
124
+ if (finished) return;
125
+ finished = true;
126
+ clearTimeout(timer);
127
+ runningBySocket.delete(ws);
128
+
129
+ var output = stripTerminalCodes(chunks.join(""));
130
+ if (truncated) output += "\n… output truncated at 64 KB";
131
+ if (timedOut) output += (output ? "\n" : "") + "Command timed out after 30 seconds.";
132
+ var exitCode = typeof code === "number" ? code : null;
133
+ var context = [
134
+ "[Shell command executed by the user]",
135
+ "$ " + command,
136
+ output || "(no output)",
137
+ "[Exit code: " + (exitCode == null ? (signal || "unknown") : exitCode) + "]",
138
+ ].join("\n");
139
+ if (!session.pendingShellContexts) session.pendingShellContexts = [];
140
+ session.pendingShellContexts.push(context);
141
+
142
+ sendResult(ws, {
143
+ requestId: requestId,
144
+ sessionId: session.localId,
145
+ command: command,
146
+ output: output,
147
+ exitCode: exitCode,
148
+ signal: signal || null,
149
+ timedOut: timedOut,
150
+ truncated: truncated,
151
+ });
152
+ });
153
+
154
+ return true;
155
+ }
156
+
157
+ return { handleShellCommand: handleShellCommand };
158
+ }
159
+
160
+ module.exports = { attachShellCommand: attachShellCommand };
@@ -374,6 +374,16 @@ function attachUserMessage(ctx) {
374
374
  fullText = mentionPrefix + "\n\n" + fullText;
375
375
  }
376
376
 
377
+ // Inject one-shot shell results captured from the composer command mode.
378
+ if (session.pendingShellContexts && session.pendingShellContexts.length > 0) {
379
+ var shellPrefix = session.pendingShellContexts.join("\n\n");
380
+ session.pendingShellContexts = [];
381
+ fullText = shellPrefix + "\n\n" + fullText;
382
+ }
383
+ if (msg.shellCommandResponse) {
384
+ fullText += "\n\n[Respond to the shell command result above now.]";
385
+ }
386
+
377
387
  // Inject active terminal context sources (delta only: send new output since last message)
378
388
  var TERM_CONTEXT_MAX = 8192; // 8KB max per terminal per message
379
389
  var TERM_HEAD_SIZE = 2048; // keep first 2KB for error context
package/lib/project.js CHANGED
@@ -28,6 +28,7 @@ var { attachKnowledge } = require("./project-knowledge");
28
28
  var { attachFilesystem } = require("./project-filesystem");
29
29
  var { attachSessions } = require("./project-sessions");
30
30
  var { attachUserMessage } = require("./project-user-message");
31
+ var { attachShellCommand } = require("./project-shell-command");
31
32
  var { attachConnection } = require("./project-connection");
32
33
  var { attachMcp } = require("./project-mcp");
33
34
  var { createLocalMcp } = require("./mcp-local");
@@ -162,7 +163,7 @@ function createProjectContext(opts) {
162
163
  var updateChannel = opts.updateChannel || "stable";
163
164
  var osUsers = opts.osUsers || false;
164
165
  var projectOwnerId = opts.projectOwnerId || null;
165
- var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, accessible }
166
+ var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, external }
166
167
  var isMate = opts.isMate || false;
167
168
  var onCreateWorktree = opts.onCreateWorktree || null;
168
169
  var serverPort = opts.port || 2633;
@@ -834,6 +835,7 @@ function createProjectContext(opts) {
834
835
  getNotificationsModule: function () { return _notifications; },
835
836
  mateDisplayName: opts.mateDisplayName || "",
836
837
  isMate: isMate,
838
+ osUsers: osUsers,
837
839
  dangerouslySkipPermissions: dangerouslySkipPermissions,
838
840
  mcpServers: getLocalMcpServers,
839
841
  getRemoteMcpServers: function () { return _mcp.getMcpServers(); },
@@ -1077,6 +1079,7 @@ function createProjectContext(opts) {
1077
1079
  try {
1078
1080
  var readyResult = await vendorAdapter.init({
1079
1081
  cwd: cwd,
1082
+ linuxUser: modelLinuxUser || undefined,
1080
1083
  clayPort: serverPort,
1081
1084
  clayTls: serverTls,
1082
1085
  clayAuthToken: serverAuthToken,
@@ -1176,6 +1179,9 @@ function createProjectContext(opts) {
1176
1179
  // --- Filesystem, settings, env (delegated to project-filesystem.js) ---
1177
1180
  if (_filesystem.handleFilesystemMessage(ws, msg)) return;
1178
1181
 
1182
+ // --- Shell command context ---
1183
+ if (_shellCommand.handleShellCommand(ws, msg)) return;
1184
+
1179
1185
  // --- Notes, terminals, context, user message (delegated to project-user-message.js) ---
1180
1186
  if (_userMessage.handleUserMessage(ws, msg)) return;
1181
1187
  }
@@ -1424,6 +1430,15 @@ function createProjectContext(opts) {
1424
1430
  _email: _email,
1425
1431
  });
1426
1432
 
1433
+ var _shellCommand = attachShellCommand({
1434
+ cwd: cwd,
1435
+ osUsers: osUsers,
1436
+ usersModule: usersModule,
1437
+ sendTo: sendTo,
1438
+ getSessionForWs: getSessionForWs,
1439
+ getOsUserInfoForWs: getOsUserInfoForWs,
1440
+ });
1441
+
1427
1442
  // --- Filesystem handler (delegated to project-filesystem.js) ---
1428
1443
  var _filesystem = attachFilesystem({
1429
1444
  cwd: cwd,
@@ -1673,8 +1688,8 @@ function createProjectContext(opts) {
1673
1688
  getProject: function () { return project; },
1674
1689
  // Exposed so the first websocket connection can lazily warm up the
1675
1690
  // adapters for this project (see project-connection handleConnection).
1676
- warmup: function () {
1677
- sdk.warmup();
1691
+ warmup: function (linuxUser) {
1692
+ sdk.warmup(osUsers ? linuxUser : null);
1678
1693
  sdk.startIdleReaper();
1679
1694
  if (!osUsers && !sessionTitleMigrationScheduled) {
1680
1695
  sessionTitleMigrationScheduled = true;
@@ -1800,7 +1815,7 @@ function createProjectContext(opts) {
1800
1815
  status.isWorktree = true;
1801
1816
  status.parentSlug = worktreeMeta.parentSlug;
1802
1817
  status.branch = worktreeMeta.branch;
1803
- status.worktreeAccessible = worktreeMeta.accessible;
1818
+ status.worktreeExternal = worktreeMeta.external === true;
1804
1819
  }
1805
1820
  if (usersModule.isMultiUser()) {
1806
1821
  var seen = {};
package/lib/public/app.js CHANGED
@@ -51,6 +51,7 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
51
51
  import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } from './modules/mate-wizard.js';
52
52
  import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
53
53
  import { initLongPress } from './modules/longpress.js';
54
+ import { initShellCommand } from './modules/shell-command.js';
54
55
  import { initConnection, connect as _connConnect, setStatus as _connSetStatus, scheduleReconnect as _connScheduleReconnect, cancelReconnect as _connCancelReconnect } from './modules/app-connection.js';
55
56
  import { processMessage as _msgProcessMessage } from './modules/app-messages.js';
56
57
  import { getWs as _getWsRef, setWs as _setWsRef } from './modules/ws-ref.js';
@@ -294,6 +295,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
294
295
  slashCommands: [],
295
296
  processing: false,
296
297
  activeSessionId: null,
298
+ sessionVendorBound: false,
297
299
  cliSessionId: null,
298
300
  isHeadlessMode: false,
299
301
  savedMainSlug: null,
@@ -308,6 +310,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
308
310
  returningFromMateDm: false,
309
311
  pendingMateInterview: null,
310
312
  pendingTermCommand: null,
313
+ shellCommandMode: false,
314
+ shellCommandRunning: false,
315
+ pendingShellCommandId: null,
311
316
  mateProjectSlug: null,
312
317
  myUserId: null,
313
318
  isMultiUserMode: false,
@@ -552,6 +557,22 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
552
557
  var newMsgBtnDefault = "\u2193 Latest";
553
558
  var newMsgBtnActivity = "\u2193 New activity";
554
559
 
560
+ // The composer grows for mobile tabs, safe areas, multiline input, and the
561
+ // keyboard. Keep the jump button immediately above its real height instead
562
+ // of relying on one fixed bottom offset.
563
+ function positionNewMsgButton() {
564
+ var inputArea = $("input-area");
565
+ if (!inputArea) return;
566
+ newMsgBtn.style.bottom = (inputArea.offsetHeight + 12) + "px";
567
+ }
568
+ var inputAreaForNewMsgBtn = $("input-area");
569
+ if (inputAreaForNewMsgBtn && window.ResizeObserver) {
570
+ new ResizeObserver(positionNewMsgButton).observe(inputAreaForNewMsgBtn);
571
+ } else {
572
+ window.addEventListener("resize", positionNewMsgButton);
573
+ }
574
+ positionNewMsgButton();
575
+
555
576
  messagesEl.addEventListener("scroll", function () {
556
577
  // While sticky-bottom is armed (e.g. just after history_done or a "New
557
578
  // activity" click), suppress "user scrolled up" detection. Growth-induced
@@ -793,6 +814,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
793
814
  },
794
815
  });
795
816
 
817
+ initShellCommand();
818
+
796
819
  // --- @Mention module ---
797
820
  initMention({
798
821
  get ws() { return _getWsRef(); },
@@ -879,6 +902,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
879
902
  if (termBtn) termBtn.style.display = "none";
880
903
  var termSideBtn = document.getElementById("terminal-sidebar-btn");
881
904
  if (termSideBtn) termSideBtn.style.display = "none";
905
+ var shellCommandBtn = document.getElementById("shell-command-btn");
906
+ if (shellCommandBtn) shellCommandBtn.style.display = "none";
907
+ var mobileShellCommandBtn = document.getElementById("input-more-shell");
908
+ if (mobileShellCommandBtn) mobileShellCommandBtn.style.display = "none";
882
909
  }
883
910
  if (!_perms.fileBrowser) {
884
911
  var fbBtn = document.getElementById("file-browser-btn");
@@ -922,6 +949,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
922
949
  messagesEl: messagesEl,
923
950
  sessionListEl: sessionListEl,
924
951
  scrollToBottom: scrollToBottom,
952
+ forceScrollToBottom: forceScrollToBottom,
925
953
  basePath: basePath,
926
954
  toggleUsagePanel: toggleUsagePanel,
927
955
  toggleStatusPanel: toggleStatusPanel,
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="GitHub Copilot CLI">
2
+ <rect width="64" height="64" rx="14" fill="#24292f"/>
3
+ <path d="M15 25c0-8 7-14 17-14s17 6 17 14v15c0 8-7 13-17 13s-17-5-17-13zm8 1v13c0 4 4 7 9 7s9-3 9-7V26c0-4-4-7-9-7s-9 3-9 7z" fill="#fff"/>
4
+ <circle cx="27" cy="30" r="3" fill="#24292f"/><circle cx="37" cy="30" r="3" fill="#24292f"/>
5
+ </svg>
@@ -434,6 +434,7 @@
434
434
  on hover/active, and a 4-direction drop-shadow + white glow on the
435
435
  emoji img for readability on either background. */
436
436
  .project-switcher-item .cmd-palette-item-icon {
437
+ position: relative;
437
438
  width: 44px;
438
439
  height: 44px;
439
440
  font-size: 28px;
@@ -447,6 +448,26 @@
447
448
  flex-shrink: 0;
448
449
  transition: background 0.15s, color 0.15s;
449
450
  }
451
+ .project-switcher-external-badge {
452
+ position: absolute;
453
+ right: -5px;
454
+ bottom: -5px;
455
+ width: 17px;
456
+ height: 17px;
457
+ display: flex;
458
+ align-items: center;
459
+ justify-content: center;
460
+ border: 2px solid var(--bg);
461
+ border-radius: 50%;
462
+ background: var(--warning);
463
+ color: var(--bg);
464
+ pointer-events: none;
465
+ }
466
+ .project-switcher-item .cmd-palette-item-icon .project-switcher-external-badge .lucide {
467
+ width: 9px;
468
+ height: 9px;
469
+ stroke-width: 2.8;
470
+ }
450
471
  .project-switcher-item .cmd-palette-item-icon .lucide {
451
472
  width: 22px;
452
473
  height: 22px;
@@ -515,8 +536,7 @@
515
536
  letter-spacing: 0.5px;
516
537
  }
517
538
 
518
- /* Disabled tile (e.g. worktree with worktreeAccessible: false). Not a
519
- valid switch target, so skip highlight, skip click, visually dim. */
539
+ /* Disabled tiles are skipped by keyboard and pointer selection. */
520
540
  .project-switcher-item.disabled {
521
541
  opacity: 0.35;
522
542
  cursor: not-allowed;
@@ -1257,20 +1257,27 @@
1257
1257
  z-index: 1;
1258
1258
  }
1259
1259
 
1260
- /* Disabled/inaccessible worktree */
1261
- .icon-strip-wt-item.wt-disabled {
1262
- opacity: 0.35;
1263
- cursor: not-allowed;
1264
- }
1265
-
1266
- .icon-strip-wt-item.wt-disabled::after {
1267
- content: "\1F6AB";
1260
+ /* Worktree registered outside the main project folder. It remains usable;
1261
+ the badge communicates location without treating it as an error. */
1262
+ .worktree-external-badge {
1268
1263
  position: absolute;
1269
- font-size: 14px;
1264
+ right: 5px;
1265
+ bottom: 1px;
1266
+ width: 14px;
1267
+ height: 14px;
1268
+ display: flex;
1269
+ align-items: center;
1270
+ justify-content: center;
1271
+ border-radius: 50%;
1272
+ border: 2px solid var(--sidebar-bg);
1273
+ background: var(--warning);
1274
+ color: var(--bg);
1270
1275
  z-index: 3;
1271
- filter: grayscale(0.3);
1276
+ pointer-events: none;
1272
1277
  }
1273
1278
 
1279
+ .worktree-external-badge .lucide { width: 8px; height: 8px; stroke-width: 2.8; }
1280
+
1274
1281
  /* Group add button */
1275
1282
  .icon-strip-group-add {
1276
1283
  width: 30px;
@@ -1420,11 +1427,20 @@ select.wt-modal-input {
1420
1427
  opacity: 0.85;
1421
1428
  }
1422
1429
 
1423
- .mobile-project-item.wt-disabled {
1424
- opacity: 0.35;
1425
- cursor: not-allowed;
1430
+ .mobile-worktree-external-badge {
1431
+ width: 20px;
1432
+ height: 20px;
1433
+ display: inline-flex;
1434
+ align-items: center;
1435
+ justify-content: center;
1436
+ border-radius: 7px;
1437
+ background: color-mix(in srgb, var(--warning) 14%, transparent);
1438
+ color: var(--warning);
1439
+ flex-shrink: 0;
1426
1440
  }
1427
1441
 
1442
+ .mobile-worktree-external-badge .lucide { width: 12px; height: 12px; stroke-width: 2.4; }
1443
+
1428
1444
  .mobile-folder-chevron {
1429
1445
  font-size: 8px;
1430
1446
  color: var(--text-dimmer);