clay-server 2.47.0-beta.1 → 2.47.0-beta.3

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 (37) hide show
  1. package/lib/codex-defaults.js +12 -2
  2. package/lib/daemon.js +38 -8
  3. package/lib/kiro-defaults.js +22 -0
  4. package/lib/project-connection.js +3 -1
  5. package/lib/project-notifications.js +3 -1
  6. package/lib/project-sessions.js +28 -9
  7. package/lib/project.js +42 -10
  8. package/lib/public/app.js +8 -0
  9. package/lib/public/css/mobile-nav.css +53 -9
  10. package/lib/public/css/sidebar.css +24 -1
  11. package/lib/public/css/tui-attention.css +4 -63
  12. package/lib/public/index.html +4 -0
  13. package/lib/public/kiro-avatar.svg +14 -0
  14. package/lib/public/modules/app-messages.js +14 -12
  15. package/lib/public/modules/app-notifications.js +12 -4
  16. package/lib/public/modules/app-panels.js +10 -4
  17. package/lib/public/modules/app-rate-limit.js +41 -13
  18. package/lib/public/modules/app-rendering.js +14 -0
  19. package/lib/public/modules/input.js +4 -3
  20. package/lib/public/modules/mate-sidebar.js +3 -3
  21. package/lib/public/modules/session-tui-view.js +4 -40
  22. package/lib/public/modules/sidebar-mates.js +1 -1
  23. package/lib/public/modules/sidebar-mobile.js +56 -23
  24. package/lib/public/modules/sidebar-sessions.js +126 -63
  25. package/lib/public/modules/tools.js +1 -1
  26. package/lib/public/modules/tui-grab.js +12 -3
  27. package/lib/sdk-bridge.js +90 -21
  28. package/lib/sdk-message-processor.js +4 -4
  29. package/lib/server.js +4 -0
  30. package/lib/users-preferences.js +3 -1
  31. package/lib/yoke/adapters/claude.js +25 -0
  32. package/lib/yoke/adapters/codex.js +34 -21
  33. package/lib/yoke/adapters/kiro.js +1170 -0
  34. package/lib/yoke/index.js +70 -6
  35. package/lib/yoke/kiro-acp-server.js +328 -0
  36. package/lib/yoke/vendor-registry.js +55 -0
  37. package/package.json +4 -3
@@ -42,7 +42,7 @@ import { handleMcpServersState } from './mcp-ui.js';
42
42
  import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
43
43
 
44
44
  // --- App module imports ---
45
- import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom } from './app-rendering.js';
45
+ import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
46
46
  import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot, updateCrossProjectBlink } from './app-favicon.js';
47
47
  import { setStatus } from './app-connection.js';
48
48
  import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
@@ -289,7 +289,7 @@ export function processMessage(msg) {
289
289
  // host lives on document.body (it's position: fixed), so it
290
290
  // survives project navigation unless we detach explicitly here.
291
291
  detachTuiView();
292
- store.set({ projectName: msg.project || msg.cwd });
292
+ store.set({ projectName: msg.project || msg.cwd, vendorInfo: msg.vendors || {} });
293
293
  if (msg.cwd) store.set({ cwd: msg.cwd });
294
294
  if (msg.slug) store.set({ currentSlug: msg.slug });
295
295
  try { var _is = store.snap(); localStorage.setItem("clay-project-name-" + (_is.currentSlug || "default"), _is.projectName); } catch (e) {}
@@ -464,6 +464,7 @@ export function processMessage(msg) {
464
464
  if (msg.vendor && !store.get('vendorSelectionLocked')) _miUpdate.currentVendor = msg.vendor;
465
465
  if (msg.availableVendors) _miUpdate.availableVendors = msg.availableVendors;
466
466
  if (msg.installedVendors) _miUpdate.installedVendors = msg.installedVendors;
467
+ if (msg.capabilities) _miUpdate.vendorCapabilities = msg.capabilities;
467
468
  store.set(_miUpdate);
468
469
  updateSettingsModels(_modelVal, msg.models || []);
469
470
  break;
@@ -579,6 +580,12 @@ export function processMessage(msg) {
579
580
  updateSessionPresence(msg.presence || {});
580
581
  break;
581
582
 
583
+ // Project's last-used vendor. Seeds the sidebar's "New session" button
584
+ // on connect and keeps other tabs in sync after someone picks a vendor.
585
+ case "last_vendor":
586
+ store.set({ lastVendor: msg.vendor || "" });
587
+ break;
588
+
582
589
  case "cursor_move":
583
590
  handleRemoteCursorMove(msg);
584
591
  break;
@@ -696,15 +703,10 @@ export function processMessage(msg) {
696
703
  }
697
704
  }
698
705
  if (_avi && _avIcon) {
699
- if (msg.vendor === "claude") {
700
- _avIcon.src = "/claude-code-avatar.png";
701
- _avIcon.alt = "Claude";
702
- _avi.title = "Claude session";
703
- _avi.classList.remove("hidden");
704
- } else if (msg.vendor === "codex") {
705
- _avIcon.src = "/codex-avatar.png";
706
- _avIcon.alt = "Codex";
707
- _avi.title = "Codex session";
706
+ if (msg.vendor && VENDOR_AVATARS[msg.vendor]) {
707
+ _avIcon.src = VENDOR_AVATARS[msg.vendor];
708
+ _avIcon.alt = VENDOR_NAMES[msg.vendor] || msg.vendor;
709
+ _avi.title = (VENDOR_NAMES[msg.vendor] || msg.vendor) + " session";
708
710
  _avi.classList.remove("hidden");
709
711
  } else {
710
712
  _avi.classList.add("hidden");
@@ -1358,7 +1360,7 @@ export function processMessage(msg) {
1358
1360
  var _lm = store.get('pendingLoginModal');
1359
1361
  store.set({ pendingLoginModal: null });
1360
1362
  openTuiModal(msg.id, _lm.slug, {
1361
- sessionTitle: (_lm.vendor === "codex" ? "Codex" : "Claude") + " login",
1363
+ sessionTitle: (VENDOR_NAMES[_lm.vendor] || "Claude Code") + " login",
1362
1364
  projectName: _lm.slug,
1363
1365
  compact: true,
1364
1366
  });
@@ -19,6 +19,14 @@ var bannerContainer = null;
19
19
  var bellBtn = null;
20
20
  var badgeEl = null;
21
21
 
22
+ function getVendorLoginCommand(vendor) {
23
+ var vendors = store.get('vendorInfo') || {};
24
+ var info = vendors[vendor];
25
+ if (info && info.loginCommand) return info.loginCommand;
26
+ var fallbacks = { codex: "codex login --device-auth", claude: "claude login" };
27
+ return fallbacks[vendor] || fallbacks.claude;
28
+ }
29
+
22
30
  // --- Pending TUI attention tracking ---
23
31
  // Mirrors the icon-shake / favicon-blink behavior the SDK side already gets
24
32
  // from `pendingPermissions` on project status broadcasts. The notification
@@ -252,7 +260,7 @@ function showBanner(notif, autoDismissMs) {
252
260
  removeBanner(banner);
253
261
  dismissNotif(notif.id);
254
262
  var authMeta = notif.meta || {};
255
- startLoginInModal(authMeta.loginCommand || ((authMeta.vendor || "claude") === "codex" ? "codex login --device-auth" : "claude login"), authMeta.vendor || "claude");
263
+ startLoginInModal(authMeta.loginCommand || getVendorLoginCommand(authMeta.vendor || "claude"), authMeta.vendor || "claude");
256
264
  showLoginReminderBanner();
257
265
  });
258
266
  }
@@ -332,7 +340,7 @@ function startLoginInModal(loginCommand, vendor) {
332
340
  if (authReminderVisible) return;
333
341
  var ws = getWs();
334
342
  if (!ws || ws.readyState !== 1) return;
335
- var cmd = loginCommand || (vendor === "codex" ? "codex login --device-auth" : "claude login");
343
+ var cmd = loginCommand || getVendorLoginCommand(vendor);
336
344
  var slug = currentProjectSlug();
337
345
  if (!slug) { startLoginCommand(cmd); return; }
338
346
  store.set({ pendingLoginModal: { slug: slug, vendor: vendor || "claude" } });
@@ -366,7 +374,7 @@ export function autoStartLoginIfNeeded(msg) {
366
374
  if (authReminderVisible) return false;
367
375
  var vendor = msg.vendor || "claude";
368
376
  var cmd = msg.loginCommand
369
- || (vendor === "codex" ? "codex login --device-auth" : "claude login");
377
+ || getVendorLoginCommand(vendor);
370
378
  startLoginInModal(cmd, vendor);
371
379
  showLoginReminderBanner();
372
380
  return true;
@@ -426,7 +434,7 @@ function showLoginReminderBanner() {
426
434
  export function showAuthRequiredBanner(msg) {
427
435
  if (!bannerContainer) return;
428
436
  var vendor = (msg && (msg.vendor || (msg.meta && msg.meta.vendor))) || "claude";
429
- var loginCommand = (msg && (msg.loginCommand || (msg.meta && msg.meta.loginCommand))) || (vendor === "codex" ? "codex login --device-auth" : "claude login");
437
+ var loginCommand = (msg && (msg.loginCommand || (msg.meta && msg.meta.loginCommand))) || getVendorLoginCommand(vendor);
430
438
  activeAuthRequiredMsg = Object.assign({}, msg || {}, {
431
439
  id: (msg && msg.id) || ("_auth_" + Date.now()),
432
440
  type: "auth_required",
@@ -88,11 +88,12 @@ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
88
88
  var EFFORT_LEVELS_BY_VENDOR = {
89
89
  claude: ["low", "medium", "high", "xhigh", "max"],
90
90
  codex: ["minimal", "low", "medium", "high", "xhigh"],
91
+ kiro: ["low", "medium", "high", "xhigh", "max"],
91
92
  };
92
93
  var THINKING_OPTIONS = ["disabled", "adaptive", "budget"];
93
94
  var CODEX_APPROVAL_OPTIONS = [
94
95
  { value: "never", label: "Auto" },
95
- { value: "on-failure", label: "On Fail" },
96
+ { value: "untrusted", label: "Untrusted" },
96
97
  { value: "on-request", label: "Ask" },
97
98
  ];
98
99
  var CODEX_SANDBOX_OPTIONS = [
@@ -418,7 +419,8 @@ export function initPanels() {
418
419
  var vendorToggleWrap = $("vendor-toggle-wrap");
419
420
  var vendorBtnClaude = $("vendor-btn-claude");
420
421
  var vendorBtnCodex = $("vendor-btn-codex");
421
- var vendorBtns = { claude: vendorBtnClaude, codex: vendorBtnCodex };
422
+ var vendorBtnKiro = $("vendor-btn-kiro");
423
+ var vendorBtns = { claude: vendorBtnClaude, codex: vendorBtnCodex, kiro: vendorBtnKiro };
422
424
 
423
425
  function updateVendorToggle() {
424
426
  var installed = store.get('installedVendors') || [];
@@ -447,6 +449,7 @@ export function initPanels() {
447
449
 
448
450
  if (vendorBtnClaude) vendorBtnClaude.addEventListener("click", function() { onVendorClick("claude"); });
449
451
  if (vendorBtnCodex) vendorBtnCodex.addEventListener("click", function() { onVendorClick("codex"); });
452
+ if (vendorBtnKiro) vendorBtnKiro.addEventListener("click", function() { onVendorClick("kiro"); });
450
453
 
451
454
  // --- Reactive UI sync ---
452
455
  store.subscribe(function (state, prev) {
@@ -638,11 +641,14 @@ export function updateConfigChip() {
638
641
  rebuildModeList();
639
642
  rebuildEffortBar();
640
643
 
641
- // Vendor-specific sections
644
+ // MODE remains Claude-specific until adapter modes become capabilities.
642
645
  var isClaude = vendor === "claude";
643
- // MODE, THINKING, BETA are Claude-only
644
646
  if (configModeList && configModeList.parentElement) configModeList.parentElement.style.display = isClaude ? "" : "none";
645
647
  rebuildThinkingSection();
648
+ // capabilities.thinking means "emits a thinking stream" (true for every
649
+ // vendor), NOT "accepts thinking config". The adaptive/extended toggle and
650
+ // budget only feed Claude queries (sm.currentThinking), so the section
651
+ // stays Claude-only until a dedicated thinkingConfig capability exists.
646
652
  if (configThinkingSection) configThinkingSection.style.display = isClaude ? "" : "none";
647
653
  // BETA section deprecated (1M context is now standard)
648
654
  if (configBetaSection) configBetaSection.style.display = "none";
@@ -23,20 +23,37 @@ var fastModeIndicatorEl = null;
23
23
  // --- Internal helpers ---
24
24
 
25
25
  function getVendorUsageMeta(vendor) {
26
- if (vendor === "codex") {
27
- return {
26
+ var vendors = store.get('vendorInfo') || {};
27
+ var info = vendors[vendor];
28
+ if (info && info.usageDashboard) return info.usageDashboard;
29
+ var fallbacks = {
30
+ codex: {
28
31
  icon: "/codex-avatar.png",
29
32
  alt: "Codex",
30
33
  href: "https://chatgpt.com/admin/usage",
31
34
  title: "Check usage on ChatGPT",
32
- };
33
- }
34
- return {
35
- icon: "/claude-code-avatar.png",
36
- alt: "Claude Code",
37
- href: "https://claude.ai/settings/usage",
38
- title: "Check usage on claude.ai",
35
+ },
36
+ claude: {
37
+ icon: "/claude-code-avatar.png",
38
+ alt: "Claude Code",
39
+ href: "https://claude.ai/settings/usage",
40
+ title: "Check usage on claude.ai",
41
+ },
39
42
  };
43
+ return fallbacks[vendor] || fallbacks.claude;
44
+ }
45
+
46
+ function vendorTracksRateLimits(vendor) {
47
+ var vendors = store.get('vendorInfo') || {};
48
+ var info = vendors[vendor];
49
+ if (info) return info.rateLimitTracking !== false;
50
+ var legacyTrackedVendors = ["claude", "codex"];
51
+ return legacyTrackedVendors.indexOf(vendor) !== -1;
52
+ }
53
+
54
+ function vendorSupportsScheduledMessages(vendor) {
55
+ var scheduledMessageVendors = ["claude"];
56
+ return scheduledMessageVendors.indexOf(vendor) !== -1;
40
57
  }
41
58
 
42
59
  function rateLimitTypeLabel(type) {
@@ -178,8 +195,13 @@ function tickRateLimitUsage() {
178
195
 
179
196
  export function initRateLimit() {
180
197
  store.subscribe(function(state, prev) {
181
- if (state.currentVendor !== prev.currentVendor && state.currentVendor && state.currentVendor !== "claude") {
182
- clearScheduleDelay();
198
+ if (state.currentVendor !== prev.currentVendor && state.currentVendor) {
199
+ if (!vendorSupportsScheduledMessages(state.currentVendor)) clearScheduleDelay();
200
+ }
201
+ if (state.currentVendor !== prev.currentVendor || state.vendorInfo !== prev.vendorInfo) {
202
+ if (rateLimitUsageEl) {
203
+ rateLimitUsageEl.style.display = vendorTracksRateLimits(state.currentVendor || "claude") ? "" : "none";
204
+ }
183
205
  }
184
206
  });
185
207
  }
@@ -203,7 +225,7 @@ export function handleRateLimitEvent(msg) {
203
225
  if (rateLimitResetTimer) clearTimeout(rateLimitResetTimer);
204
226
  // Auto-switch input to schedule mode: any message typed will be queued for after reset
205
227
  var delayUntilReset = msg.resetsAt - Date.now();
206
- if (delayUntilReset > 0 && (store.get('currentVendor') || "claude") === "claude") {
228
+ if (delayUntilReset > 0 && vendorSupportsScheduledMessages(store.get('currentVendor') || "claude")) {
207
229
  setScheduleDelayMs(delayUntilReset + 60000); // +1min buffer after reset
208
230
  }
209
231
  rateLimitResetTimer = setTimeout(function () {
@@ -222,6 +244,11 @@ export function handleRateLimitEvent(msg) {
222
244
  }
223
245
 
224
246
  export function updateRateLimitUsage(msg) {
247
+ var activeVendor = store.get('currentVendor') || "claude";
248
+ if (!vendorTracksRateLimits(activeVendor)) {
249
+ if (rateLimitUsageEl) rateLimitUsageEl.style.display = "none";
250
+ return;
251
+ }
225
252
  if (msg.rateLimitType && msg.resetsAt) {
226
253
  rateLimitResetState[msg.rateLimitType] = { resetsAt: msg.resetsAt, status: msg.status };
227
254
  }
@@ -238,6 +265,7 @@ export function updateRateLimitUsage(msg) {
238
265
  var ref = document.getElementById("skip-perms-pill");
239
266
  topBarActions.insertBefore(rateLimitUsageEl, ref);
240
267
  }
268
+ rateLimitUsageEl.style.display = "";
241
269
 
242
270
  // Build label from available reset times
243
271
  var parts = [];
@@ -251,7 +279,7 @@ export function updateRateLimitUsage(msg) {
251
279
  }
252
280
 
253
281
  var label = parts.length > 0 ? parts.join(" · ") : "Check usage";
254
- var vendor = store.get('currentVendor') || "claude";
282
+ var vendor = activeVendor;
255
283
  var meta = getVendorUsageMeta(vendor);
256
284
  rateLimitUsageEl.href = meta.href;
257
285
  rateLimitUsageEl.title = meta.title;
@@ -14,13 +14,27 @@ import { sendMessage, hasSendableContent } from './input.js';
14
14
  import { getChatLayout } from './theme.js';
15
15
  import { getScheduledMsgEl } from './app-rate-limit.js';
16
16
 
17
+ // Keep these client constants aligned with lib/yoke/vendor-registry.js. The
18
+ // browser cannot import the server's CommonJS registry directly.
17
19
  export var VENDOR_AVATARS = {
18
20
  claude: "/claude-code-avatar.png",
19
21
  codex: "/codex-avatar.png",
22
+ kiro: "/kiro-avatar.svg",
20
23
  };
21
24
  export var VENDOR_NAMES = {
22
25
  claude: "Claude Code",
23
26
  codex: "Codex",
27
+ kiro: "Kiro CLI",
28
+ };
29
+ // Display order for every vendor Clay knows about, installed or not. Pickers
30
+ // render the full list so a missing CLI reads as "not installed yet" rather
31
+ // than "Clay doesn't support it".
32
+ export var VENDOR_ORDER = ["claude", "codex", "kiro"];
33
+ // Where to send the user when they pick a vendor whose CLI isn't installed.
34
+ export var VENDOR_HOMEPAGES = {
35
+ claude: "https://claude.com/product/claude-code",
36
+ codex: "https://openai.com/codex/",
37
+ kiro: "https://kiro.dev/",
24
38
  };
25
39
  var NEW_MSG_BTN_DEFAULT = "\u2193 Latest";
26
40
  var NEW_MSG_BTN_ACTIVITY = "\u2193 New activity";
@@ -5,6 +5,7 @@ import { checkForMention, showMentionMenu, hideMentionMenu, isMentionMenuVisible
5
5
  import { store } from './store.js';
6
6
  import { mateAvatarUrl } from './avatar.js';
7
7
  import { tuiIsActive, tuiSubmitText } from './session-tui-view.js';
8
+ import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
8
9
 
9
10
  var ctx;
10
11
 
@@ -300,9 +301,9 @@ export function sendMessage() {
300
301
  _vtw2.classList.remove("locked");
301
302
  }
302
303
  if (_avi && _avIcon) {
303
- _avIcon.src = _committedVendor === "codex" ? "/codex-avatar.png" : "/claude-code-avatar.png";
304
- _avIcon.alt = _committedVendor === "codex" ? "Codex" : "Claude";
305
- _avi.title = (_committedVendor === "codex" ? "Codex" : "Claude") + " session";
304
+ _avIcon.src = VENDOR_AVATARS[_committedVendor] || VENDOR_AVATARS.claude;
305
+ _avIcon.alt = VENDOR_NAMES[_committedVendor] || VENDOR_NAMES.claude;
306
+ _avi.title = (VENDOR_NAMES[_committedVendor] || VENDOR_NAMES.claude) + " session";
306
307
  _avi.classList.remove("hidden");
307
308
  }
308
309
  } else if (_vtw2) {
@@ -158,9 +158,9 @@ export function showMateSidebar(mateId, mateData) {
158
158
  if (mateVendorWrap) {
159
159
  var available = store.get('availableVendors') || [];
160
160
  var mateVendor = mateData.vendor || "claude";
161
- var vendorIcons = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png" };
162
- var vendorNames = { claude: "Claude Code", codex: "Codex" };
163
- var vendorKeys = ["claude", "codex"];
161
+ var vendorIcons = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png", kiro: "/kiro-avatar.svg" };
162
+ var vendorNames = { claude: "Claude Code", codex: "Codex", kiro: "Kiro CLI" };
163
+ var vendorKeys = ["claude", "codex", "kiro"];
164
164
  mateVendorWrap.innerHTML = "";
165
165
  for (var vi = 0; vi < vendorKeys.length; vi++) {
166
166
  var vk = vendorKeys[vi];
@@ -20,15 +20,9 @@ import { getTerminalTheme } from './theme.js';
20
20
  import { getTerminalFontFamily, getTerminalFontSize, onTerminalFontChange } from './terminal-prefs.js';
21
21
  import { showHeaderTuiFont, hideHeaderTuiFont } from './header-tui-font.js';
22
22
  import { attachTuiGrab, detachTuiGrab } from './tui-grab.js';
23
- import { openArticle as openWhatsNewArticle } from './whats-new-article.js';
24
23
  import { createKeyToolbar, TERMINAL_TOOLBAR_HTML } from './terminal-toolbar.js';
25
24
  import { refreshIcons, iconHtml } from './icons.js';
26
25
 
27
- // Stable id of the canonical "Why TUI mode?" article in
28
- // lib/whats-new-content.js. The TUI policy notice's "Learn more" button
29
- // opens that article in the blog viewer instead of a one-off modal.
30
- var TUI_POLICY_ARTICLE_ID = "2026-06-tui-default";
31
-
32
26
  // Claude TUI session terminal colors follow Clay's active theme via
33
27
  // getTerminalTheme(). Live theme switches are wired through
34
28
  // setTuiSessionTheme() below, which theme.js calls from applyTheme.
@@ -44,15 +38,6 @@ var isTouchDevice = "ontouchstart" in window;
44
38
  // touch devices and forward its send into the PTY (input.js -> tuiSubmitText).
45
39
  // xterm is render-only on mobile.
46
40
 
47
- // The TUI policy notice's "Learn more" button opens the canonical
48
- // "Why TUI mode?" entry in the What's New blog viewer. The full
49
- // authoritative text lives in lib/whats-new-content.js so there's a
50
- // single source of truth.
51
- function openPolicyArticle(e) {
52
- if (e && e.stopPropagation) e.stopPropagation();
53
- openWhatsNewArticle(TUI_POLICY_ARTICLE_ID);
54
- }
55
-
56
41
  var xterm = null; // xterm.js instance
57
42
  var fitAddon = null;
58
43
  var webglAddon = null;
@@ -81,7 +66,7 @@ var resizeDebounce = null;
81
66
  // fitAddon.fit() reads clientWidth - otherwise fit() can compute
82
67
  // cols against the pre-resize container width.
83
68
  // 2. A second rAF + second fit() pass converges any further layout
84
- // adjustments (scrollbar appear/disappear, policy notice
69
+ // adjustments (scrollbar appear/disappear, the key toolbar
85
70
  // reflowing on narrower widths, etc.). This mirrors the two-pass
86
71
  // fit used on attach (fitNow + setTimeout(fitNow, 50)).
87
72
  // 3. WebGL renderer caches a font atlas keyed on cell metrics. When
@@ -128,7 +113,7 @@ function ensureHostEl() {
128
113
  hostEl.style.boxSizing = "border-box";
129
114
  // 4px inset on all sides so the terminal doesn't kiss the edges of the
130
115
  // content area. fitAddon computes against the xterm container's
131
- // clientWidth/clientHeight (which exclude padding + the policy banner),
116
+ // clientWidth/clientHeight (which exclude padding + the key toolbar),
132
117
  // so cols/rows stay correct.
133
118
  hostEl.style.padding = "4px";
134
119
 
@@ -148,30 +133,9 @@ function ensureHostEl() {
148
133
  });
149
134
  }
150
135
 
151
- // Policy notice: explains why TUI mode exists (Anthropic split Agent
152
- // SDK usage into a separate billing bucket on 2026-06-15; running
153
- // `claude` in a real terminal keeps usage in the Interactive bucket).
154
- var noticeEl = document.createElement("div");
155
- noticeEl.className = "tui-policy-notice";
156
- noticeEl.innerHTML =
157
- '<span class="tui-policy-icon">●</span>' +
158
- '<span class="tui-policy-text">TUI is the default Claude mode in Clay because of Anthropic\'s new billing policy.</span>' +
159
- '<button type="button" class="tui-policy-learn-more">Learn more</button>' +
160
- '<button type="button" class="tui-policy-dismiss" aria-label="Dismiss this notice for the rest of the page">&times;</button>';
161
- noticeEl.querySelector(".tui-policy-learn-more").addEventListener("click", openPolicyArticle);
162
- noticeEl.querySelector(".tui-policy-dismiss").addEventListener("click", function () {
163
- // Session-only dismiss: hides the notice for the current page load.
164
- // Reloading restores it on purpose so the policy context doesn't
165
- // get lost forever - a user who comes back next month should be
166
- // re-reminded that TUI is a billing workaround.
167
- noticeEl.style.display = "none";
168
- if (typeof fitNow === "function") fitNow();
169
- });
170
- hostEl.appendChild(noticeEl);
171
-
172
- // Dedicated xterm container so the banner sits above the terminal
136
+ // Dedicated xterm container so the toolbar sits above the terminal
173
137
  // rather than xterm mounting at the host root (which would have it
174
- // side-by-side with the banner under flex layout).
138
+ // side-by-side with the toolbar under flex layout).
175
139
  xtermContainerEl = document.createElement("div");
176
140
  xtermContainerEl.className = "tui-xterm-container";
177
141
  xtermContainerEl.style.flex = "1 1 auto";
@@ -457,7 +457,7 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
457
457
  // Tooltip
458
458
  var displayName = mp.displayName || mate.name || "New Mate";
459
459
  var mateVendor = mate.vendor || "claude";
460
- var vendorLabels = { claude: "Claude Code", codex: "OpenAI Codex" };
460
+ var vendorLabels = { claude: "Claude Code", codex: "OpenAI Codex", kiro: "Kiro CLI" };
461
461
  el.addEventListener("mouseenter", function () {
462
462
  var html = '<div style="font-weight:600">' + escapeHtml(displayName);
463
463
  if (mate.primary) {
@@ -11,8 +11,11 @@ import { getMateSessions } from './mate-sidebar.js';
11
11
  import { openProjectSettings } from './project-settings.js';
12
12
  import {
13
13
  getCachedSessions,
14
- getDateGroup
14
+ getDateGroup,
15
+ resolveDefaultVendor,
16
+ startNewSession
15
17
  } from './sidebar-sessions.js';
18
+ import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES } from './app-rendering.js';
16
19
  import {
17
20
  getCachedProjectList,
18
21
  getCachedCurrentSlug,
@@ -698,36 +701,66 @@ function renderMateMobileActions(container) {
698
701
 
699
702
  // Helper: render sorted sessions into a container with date groups (with loop session grouping)
700
703
  function renderMobileSessionsInto(container) {
701
- // Vendor-aware new-session row. Mirrors the desktop sidebar's two-button
702
- // pattern (Claude defaults to TUI, Codex always GUI) so mobile users can
703
- // pick the vendor instead of being silently routed to Claude TUI.
704
+ // Mirrors the desktop sidebar's split button: one "New session" action on
705
+ // the project's last-used vendor, plus a chevron that expands the full
706
+ // vendor list. The list expands inline rather than as a fixed-position
707
+ // popover because it lives inside a scrolling bottom sheet.
708
+ var mobileDefaultVendor = resolveDefaultVendor();
709
+ var mobileInstalled = store.get('installedVendors') || [];
710
+
704
711
  var newRow = document.createElement("div");
705
712
  newRow.className = "mobile-session-new-row";
706
713
 
707
- var claudeBtn = document.createElement("button");
708
- claudeBtn.className = "mobile-session-new mobile-session-new-vendor";
709
- claudeBtn.innerHTML = '<img src="/claude-code-avatar.png" class="mobile-session-new-icon" alt=""><span>Claude</span>';
710
- claudeBtn.addEventListener("click", function () {
711
- if (getWs() && store.get('connected')) {
712
- // No explicit mode: the server applies the user's claudeOpenMode pref.
713
- getWs().send(JSON.stringify({ type: "new_session", vendor: "claude" }));
714
- }
714
+ var mainBtn = document.createElement("button");
715
+ mainBtn.className = "mobile-session-new mobile-session-new-main";
716
+ mainBtn.innerHTML = '<img src="' + (VENDOR_AVATARS[mobileDefaultVendor] || VENDOR_AVATARS.claude) +
717
+ '" class="mobile-session-new-icon" alt=""><span>New ' + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + ' session</span>';
718
+ mainBtn.addEventListener("click", function () {
719
+ startNewSession(mobileDefaultVendor);
715
720
  closeMobileSheet();
716
721
  });
717
- newRow.appendChild(claudeBtn);
718
-
719
- var codexBtn = document.createElement("button");
720
- codexBtn.className = "mobile-session-new mobile-session-new-vendor";
721
- codexBtn.innerHTML = '<img src="/codex-avatar.png" class="mobile-session-new-icon" alt=""><span>Codex</span>';
722
- codexBtn.addEventListener("click", function () {
723
- if (getWs() && store.get('connected')) {
724
- getWs().send(JSON.stringify({ type: "new_session", vendor: "codex" }));
725
- }
726
- closeMobileSheet();
722
+ newRow.appendChild(mainBtn);
723
+
724
+ var vendorList = document.createElement("div");
725
+ vendorList.className = "mobile-vendor-list hidden";
726
+
727
+ var chevronBtn = document.createElement("button");
728
+ chevronBtn.className = "mobile-session-new mobile-session-new-chevron";
729
+ chevronBtn.setAttribute("aria-label", "Choose a vendor");
730
+ chevronBtn.innerHTML = iconHtml("chevron-down");
731
+ chevronBtn.addEventListener("click", function () {
732
+ vendorList.classList.toggle("hidden");
733
+ chevronBtn.classList.toggle("expanded");
727
734
  });
728
- newRow.appendChild(codexBtn);
735
+ newRow.appendChild(chevronBtn);
736
+
737
+ // Every known vendor is listed. Uninstalled ones stay visible but disabled,
738
+ // and tapping one opens the vendor's homepage.
739
+ for (var vi = 0; vi < VENDOR_ORDER.length; vi++) {
740
+ (function (vendor) {
741
+ var isInstalled = mobileInstalled.indexOf(vendor) !== -1;
742
+ var name = VENDOR_NAMES[vendor] || vendor;
743
+ var vBtn = document.createElement("button");
744
+ vBtn.className = "mobile-session-new mobile-session-new-vendor";
745
+ if (!isInstalled) vBtn.classList.add("disabled");
746
+ if (vendor === mobileDefaultVendor) vBtn.classList.add("active");
747
+ vBtn.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="mobile-session-new-icon" alt="">' +
748
+ '<span>' + name + '</span>' +
749
+ (isInstalled ? "" : '<span class="mobile-vendor-note">Not installed</span>');
750
+ vBtn.addEventListener("click", function () {
751
+ if (!isInstalled) {
752
+ window.open(VENDOR_HOMEPAGES[vendor], "_blank", "noopener");
753
+ return;
754
+ }
755
+ startNewSession(vendor);
756
+ closeMobileSheet();
757
+ });
758
+ vendorList.appendChild(vBtn);
759
+ })(VENDOR_ORDER[vi]);
760
+ }
729
761
 
730
762
  container.appendChild(newRow);
763
+ container.appendChild(vendorList);
731
764
 
732
765
  // Partition: loop sessions vs normal sessions (same logic as desktop renderSessionList)
733
766
  var sessions = getCachedSessions();