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
@@ -1,12 +1,20 @@
1
1
  var CODEX_DEFAULTS = {
2
- approval: "on-failure",
2
+ approval: "on-request",
3
3
  sandbox: "danger-full-access",
4
4
  webSearch: "live",
5
5
  };
6
6
 
7
+ var CODEX_APPROVAL_POLICIES = ["untrusted", "on-request", "granular", "never"];
8
+
9
+ function normalizeCodexApproval(value) {
10
+ if (value === "on-failure") return CODEX_DEFAULTS.approval;
11
+ if (CODEX_APPROVAL_POLICIES.indexOf(value) === -1) return CODEX_DEFAULTS.approval;
12
+ return value;
13
+ }
14
+
7
15
  function getCodexConfig(sm) {
8
16
  return {
9
- approval: (sm && sm.codexApproval) || CODEX_DEFAULTS.approval,
17
+ approval: normalizeCodexApproval(sm && sm.codexApproval),
10
18
  sandbox: (sm && sm.codexSandbox) || CODEX_DEFAULTS.sandbox,
11
19
  webSearch: (sm && sm.codexWebSearch) || CODEX_DEFAULTS.webSearch,
12
20
  };
@@ -14,5 +22,7 @@ function getCodexConfig(sm) {
14
22
 
15
23
  module.exports = {
16
24
  CODEX_DEFAULTS: CODEX_DEFAULTS,
25
+ CODEX_APPROVAL_POLICIES: CODEX_APPROVAL_POLICIES,
26
+ normalizeCodexApproval: normalizeCodexApproval,
17
27
  getCodexConfig: getCodexConfig,
18
28
  };
package/lib/daemon.js CHANGED
@@ -648,6 +648,29 @@ var relay = createServer({
648
648
  }
649
649
  return { ok: false, error: "Project not found" };
650
650
  },
651
+ onGetProjectLastVendor: function (slug) {
652
+ for (var i = 0; i < config.projects.length; i++) {
653
+ if (config.projects[i].slug === slug) {
654
+ return { vendor: config.projects[i].lastVendor || null };
655
+ }
656
+ }
657
+ return { vendor: null };
658
+ },
659
+ onSetProjectLastVendor: function (slug, vendor) {
660
+ for (var i = 0; i < config.projects.length; i++) {
661
+ if (config.projects[i].slug === slug) {
662
+ if (config.projects[i].lastVendor === vendor) return { ok: true };
663
+ if (vendor) {
664
+ config.projects[i].lastVendor = vendor;
665
+ } else {
666
+ delete config.projects[i].lastVendor;
667
+ }
668
+ saveConfig(config);
669
+ return { ok: true };
670
+ }
671
+ }
672
+ return { ok: false, error: "Project not found" };
673
+ },
651
674
  onGetProjectMcpServers: function (slug) {
652
675
  for (var i = 0; i < config.projects.length; i++) {
653
676
  if (config.projects[i].slug === slug) {
@@ -968,21 +991,28 @@ var relay = createServer({
968
991
  },
969
992
  });
970
993
 
971
- var IDLE_MS = Number(process.env.CLAY_CODEX_IDLE_MS) || 5 * 60 * 1000;
972
- var REAP_MS = Number(process.env.CLAY_CODEX_REAPER_MS) || 60 * 1000;
994
+ // Reclaim idle out-of-process agent runtimes. Any adapter that owns a child
995
+ // process may implement the optional shutdownIfIdle(ms); adapters without one
996
+ // (Claude, which runs in-process or under sdk-worker) are simply skipped.
997
+ // The CLAY_CODEX_* names are kept as aliases for backwards compatibility.
998
+ var IDLE_MS = Number(process.env.CLAY_ADAPTER_IDLE_MS) || Number(process.env.CLAY_CODEX_IDLE_MS) || 5 * 60 * 1000;
999
+ var REAP_MS = Number(process.env.CLAY_ADAPTER_REAPER_MS) || Number(process.env.CLAY_CODEX_REAPER_MS) || 60 * 1000;
973
1000
  var reaperHandle = setInterval(function () {
974
1001
  if (!relay || typeof relay.forEachProject !== "function") return;
975
1002
  relay.forEachProject(function (ctx) {
976
- try {
977
- if (ctx && ctx.adapters && ctx.adapters.codex && typeof ctx.adapters.codex.shutdownIfIdle === "function") {
978
- var result = ctx.adapters.codex.shutdownIfIdle(IDLE_MS);
1003
+ if (!ctx || !ctx.adapters) return;
1004
+ Object.keys(ctx.adapters).forEach(function (vendor) {
1005
+ try {
1006
+ var adapter = ctx.adapters[vendor];
1007
+ if (!adapter || typeof adapter.shutdownIfIdle !== "function") return;
1008
+ var result = adapter.shutdownIfIdle(IDLE_MS);
979
1009
  if (result && typeof result.catch === "function") {
980
1010
  result.catch(function (e) {
981
- console.error("[daemon] Codex idle reclaim failed:", e && e.message ? e.message : e);
1011
+ console.error("[daemon] " + vendor + " idle reclaim failed:", e && e.message ? e.message : e);
982
1012
  });
983
1013
  }
984
- }
985
- } catch (e) {}
1014
+ } catch (e) {}
1015
+ });
986
1016
  });
987
1017
  }, REAP_MS);
988
1018
  if (reaperHandle && typeof reaperHandle.unref === "function") {
@@ -0,0 +1,22 @@
1
+ // Kiro-specific default values. Single source of truth — do not duplicate
2
+ // elsewhere. Consumed by the server-side vendor plumbing and sent to clients
3
+ // via the config state so the UI renders the right controls.
4
+
5
+ var KIRO_DEFAULTS = {
6
+ // Kiro CLI 2.18.1 exposes the next-generation agent as the v3 engine. Its
7
+ // general-purpose mode id is "vibe" (displayed as "Default").
8
+ engine: "v3",
9
+ mode: "vibe",
10
+ };
11
+
12
+ function getKiroConfig(sm) {
13
+ return {
14
+ engine: (sm && sm.kiroEngine) || KIRO_DEFAULTS.engine,
15
+ mode: (sm && sm.kiroMode) || KIRO_DEFAULTS.mode,
16
+ };
17
+ }
18
+
19
+ module.exports = {
20
+ KIRO_DEFAULTS: KIRO_DEFAULTS,
21
+ getKiroConfig: getKiroConfig,
22
+ };
@@ -4,6 +4,7 @@ var usersModule = require("./users");
4
4
  var userPresence = require("./user-presence");
5
5
  var emailAccounts = require("./email-accounts");
6
6
  var { getCodexConfig } = require("./codex-defaults");
7
+ var yoke = require("./yoke");
7
8
 
8
9
  /**
9
10
  * Attach connection/disconnection handlers to a project context.
@@ -132,7 +133,7 @@ function attachConnection(ctx) {
132
133
  var restoredActive = restoredState.active;
133
134
  var initialVendor = (restoredActive && restoredActive.vendor) || sm.defaultVendor || "claude";
134
135
  var initialModels = (sm.modelsByVendor && sm.modelsByVendor[initialVendor]) || sm.availableModels || [];
135
- sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked });
136
+ sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked, vendors: yoke.VENDOR_REGISTRY });
136
137
  // Update notifications are pushed on a scheduled interval (see
137
138
  // scheduleUpdateBroadcast). We no longer push on connect to avoid
138
139
  // re-triggering the banner on every page refresh.
@@ -143,6 +144,7 @@ function attachConnection(ctx) {
143
144
  sendTo(ws, { type: "model_info", model: sm.currentModel, models: initialModels, vendor: initialVendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
144
145
  }
145
146
  sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
147
+ sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
146
148
  sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
147
149
  sendTo(ws, { type: "term_list", terminals: tm.list() });
148
150
  // Context sources sent after session is resolved (per-session storage)
@@ -5,6 +5,7 @@
5
5
  var fs = require("fs");
6
6
  var path = require("path");
7
7
  var config = require("./config");
8
+ var yoke = require("./yoke");
8
9
 
9
10
  var NOTIF_FILE = path.join(config.CONFIG_DIR, "notifications.json");
10
11
  var REMINDER_INTERVAL = 60 * 60 * 1000; // 1 hour
@@ -21,7 +22,8 @@ function generateId() {
21
22
  var formatters = {
22
23
  auth_required: function (data) {
23
24
  var vendor = data.vendor || "claude";
24
- var title = data.title || ((vendor === "codex" ? "Codex" : "Claude Code") + " is not logged in");
25
+ var vendorInfo = yoke.getVendorInfo(vendor);
26
+ var title = data.title || (((vendorInfo && vendorInfo.displayName) || "Claude Code") + " is not logged in");
25
27
  return {
26
28
  type: "auth_required",
27
29
  title: title,
@@ -3,6 +3,12 @@ var path = require("path");
3
3
  var crypto = require("crypto");
4
4
  var { execFileSync } = require("child_process");
5
5
  var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
6
+ var yoke = require("./yoke");
7
+
8
+ function vendorSupportsTui(vendor) {
9
+ var info = yoke.getVendorInfo(vendor);
10
+ return !!(info && info.sessionModes.indexOf("tui") !== -1);
11
+ }
6
12
 
7
13
  // Format a user's answer to an ask_user_questions card as a plain user
8
14
  // message so the MCP path can feed it back to the agent on the next turn.
@@ -274,6 +280,8 @@ function attachSessions(ctx) {
274
280
  var sid = session.cliSessionId;
275
281
  var localId = session.localId;
276
282
  var resumeSkip = session.dangerouslySkipPermissions ? " --dangerously-skip-permissions" : "";
283
+ // Command construction is Claude-specific. Generalize this before any
284
+ // other vendor declares "tui" in the YOKE registry.
277
285
  var cmd = "claude --resume " + sid + resumeSkip + "; exit\n";
278
286
  var term = tm.create(80, 24, getOsUserInfoForWs(ws), ws, {
279
287
  initialInput: cmd,
@@ -346,7 +354,7 @@ function attachSessions(ctx) {
346
354
  // born-TUI session shows the same read-only + Resume view as a fresh click.
347
355
  function resolveSessionForView(session, ws) {
348
356
  if (!session) return;
349
- if (session.vendor && session.vendor !== "claude") { session.tuiSuspended = false; return; }
357
+ if (session.vendor && !vendorSupportsTui(session.vendor)) { session.tuiSuspended = false; return; }
350
358
  var pref = getClaudeOpenModeForWs(ws);
351
359
  // A LIVE runtime always wins over the viewer's claudeOpenMode pref:
352
360
  // another user (or this user in another tab) may be in the session right
@@ -424,13 +432,13 @@ function attachSessions(ctx) {
424
432
  if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
425
433
  if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
426
434
  if (msg.vendor) sessionOpts.vendor = msg.vendor;
427
- // Mode resolution: codex sessions are always GUI (no TUI adapter).
428
- // Claude sessions honor the explicit msg.mode if provided, otherwise
435
+ // Mode resolution: vendors without a TUI session mode are always GUI.
436
+ // TUI-capable sessions honor the explicit msg.mode if provided, otherwise
429
437
  // fall back to the user's claudeOpenMode preference. This is what
430
438
  // makes the sidebar's "Claude" icon button create the right kind of
431
439
  // session without the client needing to know the preference.
432
440
  var requestedMode;
433
- if (msg.vendor === "codex") {
441
+ if (msg.vendor && !vendorSupportsTui(msg.vendor)) {
434
442
  requestedMode = "gui";
435
443
  } else if (msg.mode === "tui" || msg.mode === "gui") {
436
444
  requestedMode = msg.mode;
@@ -495,6 +503,17 @@ function attachSessions(ctx) {
495
503
  newSess = sm.createSession(sessionOpts, ws);
496
504
  }
497
505
  ws._clayActiveSession = newSess.localId;
506
+ // Remember the vendor only when the client asked for one explicitly.
507
+ // new_session without a vendor (mate sidebar, notification banner,
508
+ // debate) falls through to the default adapter and must not clobber
509
+ // the user's remembered pick.
510
+ if (msg.vendor && sm.lastVendor !== msg.vendor) {
511
+ sm.lastVendor = msg.vendor;
512
+ if (typeof opts.onSetProjectLastVendor === "function") {
513
+ opts.onSetProjectLastVendor(slug, msg.vendor);
514
+ }
515
+ send({ type: "last_vendor", vendor: msg.vendor });
516
+ }
498
517
  // Apply project-level email defaults to new session
499
518
  if (typeof ctx._email === "object" && ctx._email.getEmailDefaults) {
500
519
  var emailDefaults = ctx._email.getEmailDefaults();
@@ -614,7 +633,7 @@ function attachSessions(ctx) {
614
633
  // for every viewer; only cold sessions follow the clicker's
615
634
  // claudeOpenMode pref. Nothing is spawned here.
616
635
  var xmTarget = sm.sessions.get(msg.id);
617
- if (xmTarget && (xmTarget.vendor === "claude" || !xmTarget.vendor)) {
636
+ if (xmTarget && (!xmTarget.vendor || vendorSupportsTui(xmTarget.vendor))) {
618
637
  // Single source of truth: live runtime wins (tui stays tui, gui stays
619
638
  // gui); only cold sessions follow the viewer's pref. No PTY is spawned
620
639
  // on switch - born-TUI resumes lazily via the Resume bar
@@ -667,7 +686,7 @@ function attachSessions(ctx) {
667
686
  if (msg.type === "resume_tui_session") {
668
687
  if (msg.id && sm.sessions.has(msg.id)) {
669
688
  var rtTarget = sm.sessions.get(msg.id);
670
- var rtOk = rtTarget && (rtTarget.vendor === "claude" || !rtTarget.vendor) &&
689
+ var rtOk = rtTarget && (!rtTarget.vendor || vendorSupportsTui(rtTarget.vendor)) &&
671
690
  rtTarget.cliSessionId && tm;
672
691
  if (rtOk) {
673
692
  if (usersModule.isMultiUser() && ws._clayUser &&
@@ -695,7 +714,7 @@ function attachSessions(ctx) {
695
714
  if (msg.type === "suspend_tui_session") {
696
715
  if (msg.id && sm.sessions.has(msg.id)) {
697
716
  var stTarget = sm.sessions.get(msg.id);
698
- var stOk = stTarget && (stTarget.vendor === "claude" || !stTarget.vendor);
717
+ var stOk = stTarget && (!stTarget.vendor || vendorSupportsTui(stTarget.vendor));
699
718
  if (stOk && (!usersModule.isMultiUser() || !ws._clayUser ||
700
719
  usersModule.canAccessSession(ws._clayUser.id, stTarget, { visibility: "public" }))) {
701
720
  if (tm) {
@@ -726,7 +745,7 @@ function attachSessions(ctx) {
726
745
  var tprId = msg.id;
727
746
  var tprSess = (tprId && sm.sessions.has(tprId)) ? sm.sessions.get(tprId) : null;
728
747
  if (!tprSess || !tprSess.cliSessionId || tprSess.mode !== "tui") return true;
729
- if (tprSess.vendor && tprSess.vendor !== "claude") return true;
748
+ if (tprSess.vendor && !vendorSupportsTui(tprSess.vendor)) return true;
730
749
  if (usersModule.isMultiUser() && ws._clayUser
731
750
  && !usersModule.canAccessSession(ws._clayUser.id, tprSess, { visibility: "public" })) {
732
751
  return true;
@@ -1073,7 +1092,7 @@ function attachSessions(ctx) {
1073
1092
 
1074
1093
  // Codex-specific settings (stored on sessionManager, passed to adapter via adapterOptions)
1075
1094
  if (msg.type === "set_codex_approval") {
1076
- sm.codexApproval = msg.approval || CODEX_DEFAULTS.approval;
1095
+ sm.codexApproval = getCodexConfig({ codexApproval: msg.approval }).approval;
1077
1096
  send(Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
1078
1097
  return true;
1079
1098
  }
package/lib/project.js CHANGED
@@ -166,7 +166,7 @@ function createProjectContext(opts) {
166
166
  var sessionTitleMigrationScheduled = false;
167
167
 
168
168
  // --- YOKE adapters (multi-vendor, lazy init) ---
169
- var _yokeState = yoke.createAdapters({ cwd: cwd, slug: slug });
169
+ var _yokeState = yoke.createAdapters({ cwd: cwd, slug: slug, osUsers: osUsers });
170
170
  var adapters = _yokeState.adapters;
171
171
  var defaultVendor = adapters.claude ? "claude" : Object.keys(adapters)[0] || "claude";
172
172
  var adapter = adapters[defaultVendor] || null;
@@ -428,6 +428,12 @@ function createProjectContext(opts) {
428
428
  var _srvEffort = typeof opts.onGetServerDefaultEffort === "function" ? opts.onGetServerDefaultEffort() : null;
429
429
  sm.currentEffort = (_projEffort && _projEffort.effort) || (_srvEffort && _srvEffort.effort) || "medium";
430
430
 
431
+ // Last vendor the user started a session with in this project. Seeds the
432
+ // sidebar's "New session" button so it defaults to whatever they used last
433
+ // instead of always launching Claude.
434
+ var _projLastVendor = typeof opts.onGetProjectLastVendor === "function" ? opts.onGetProjectLastVendor(slug) : null;
435
+ sm.lastVendor = (_projLastVendor && _projLastVendor.vendor) || null;
436
+
431
437
  var _projModel = typeof opts.onGetProjectDefaultModel === "function" ? opts.onGetProjectDefaultModel(slug) : null;
432
438
  var _srvModel = typeof opts.onGetServerDefaultModel === "function" ? opts.onGetServerDefaultModel() : null;
433
439
  sm._savedDefaultModel = (_projModel && _projModel.model) || (_srvModel && _srvModel.model) || null;
@@ -897,27 +903,36 @@ function createProjectContext(opts) {
897
903
  (async function() {
898
904
  if (msg.vendor) {
899
905
  try {
906
+ var modelLinuxUser = getLinuxUserForWs(ws);
900
907
  var vendorAdapter = adapters[msg.vendor] || null;
901
908
  if (!vendorAdapter) {
902
909
  vendorAdapter = await yoke.lazyCreateAdapter(adapters, msg.vendor, {
903
910
  cwd: cwd,
911
+ linuxUser: modelLinuxUser || undefined,
904
912
  clayPort: serverPort,
905
913
  clayTls: serverTls,
906
914
  clayAuthToken: serverAuthToken,
907
915
  slug: slug,
908
916
  });
909
- } else if ((!sm.modelsByVendor || !sm.modelsByVendor[msg.vendor]) && typeof vendorAdapter.init === "function") {
917
+ }
918
+ var needsReadyMetadata = !sm.capabilitiesByVendor || !sm.capabilitiesByVendor[msg.vendor]
919
+ || !sm.modelsByVendor || !sm.modelsByVendor[msg.vendor];
920
+ if (vendorAdapter && needsReadyMetadata && typeof vendorAdapter.init === "function") {
910
921
  // Init warms the adapter, but a slow/failed init must not block
911
922
  // model listing (e.g. Codex models are a fixed list). Keep going
912
923
  // to supportedModels() even if init throws.
913
924
  try {
914
- await vendorAdapter.init({
925
+ var readyResult = await vendorAdapter.init({
915
926
  cwd: cwd,
916
927
  clayPort: serverPort,
917
928
  clayTls: serverTls,
918
929
  clayAuthToken: serverAuthToken,
919
930
  slug: slug,
920
931
  });
932
+ sm.capabilitiesByVendor = sm.capabilitiesByVendor || {};
933
+ sm.capabilitiesByVendor[msg.vendor] = readyResult.capabilities || {};
934
+ sm.modelsByVendor = sm.modelsByVendor || {};
935
+ if (Array.isArray(readyResult.models)) sm.modelsByVendor[msg.vendor] = readyResult.models;
921
936
  } catch (e) {
922
937
  console.error("[project] " + msg.vendor + " init failed (continuing to model list):", e.message || e);
923
938
  }
@@ -949,7 +964,8 @@ function createProjectContext(opts) {
949
964
  }
950
965
  }
951
966
  }
952
- sendTo(ws, { type: "model_info", model: modelToSend, models: vendorModels, vendor: msg.vendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
967
+ var vendorCapabilities = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[msg.vendor]) || {};
968
+ sendTo(ws, { type: "model_info", model: modelToSend, models: vendorModels, vendor: msg.vendor, capabilities: vendorCapabilities, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
953
969
  })();
954
970
  return;
955
971
  }
@@ -1528,14 +1544,30 @@ function createProjectContext(opts) {
1528
1544
  fs.rmSync(tmpDir, { recursive: true, force: true });
1529
1545
  } catch (e) {}
1530
1546
 
1531
- var codexShutdown = Promise.resolve(true);
1532
- if (adapters && adapters.codex && typeof adapters.codex.shutdown === "function") {
1533
- codexShutdown = adapters.codex.shutdown().catch(function(err) {
1534
- console.error("[project] Codex shutdown failed for " + slug + ":", err && err.message ? err.message : err);
1535
- return false;
1547
+ // Shut down every adapter that owns a child process. shutdown() is
1548
+ // optional on the YOKE contract, so adapters without one are skipped.
1549
+ var shutdowns = [];
1550
+ if (adapters) {
1551
+ Object.keys(adapters).forEach(function(vendor) {
1552
+ var adapter = adapters[vendor];
1553
+ if (!adapter || typeof adapter.shutdown !== "function") return;
1554
+ // Shared adapter instances (e.g. Claude) are reused across projects,
1555
+ // so tearing one down here would kill other projects' sessions.
1556
+ if (adapter.shared) return;
1557
+ try {
1558
+ shutdowns.push(Promise.resolve(adapter.shutdown()).catch(function(err) {
1559
+ console.error("[project] " + vendor + " shutdown failed for " + slug + ":", err && err.message ? err.message : err);
1560
+ return false;
1561
+ }));
1562
+ } catch (err) {
1563
+ console.error("[project] " + vendor + " shutdown threw for " + slug + ":", err && err.message ? err.message : err);
1564
+ }
1536
1565
  });
1537
1566
  }
1538
- return codexShutdown;
1567
+ if (!shutdowns.length) return Promise.resolve(true);
1568
+ return Promise.all(shutdowns).then(function(results) {
1569
+ return results.every(function(r) { return r !== false; });
1570
+ });
1539
1571
  }
1540
1572
 
1541
1573
  // --- Status info ---
package/lib/public/app.js CHANGED
@@ -310,6 +310,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
310
310
  // panels
311
311
  currentModel: "",
312
312
  currentModels: [],
313
+ // Project's last-used vendor; seeds the sidebar's "New session" button.
314
+ lastVendor: "",
315
+ // Static adapter metadata sent before any vendor is initialized.
316
+ vendorInfo: {},
317
+ // How Claude sessions open: "gui" (default) or "tui". The server sends
318
+ // claude_open_mode_changed on connect; this seeds it beforehand so the
319
+ // new-session menu doesn't flash the TUI-only entry.
320
+ claudeOpenMode: "gui",
313
321
  currentMode: "default",
314
322
  currentEffort: "medium",
315
323
  currentBetas: [],
@@ -358,18 +358,16 @@
358
358
  background: rgba(var(--overlay-rgb), 0.06);
359
359
  }
360
360
 
361
- /* Vendor-split row: Claude + Codex side-by-side. Used inside
362
- renderMobileSessionsInto so users can pick the vendor on mobile, the
363
- same way the desktop sidebar already does. */
361
+ /* Split "New session" row: main action on the last-used vendor + a chevron
362
+ that expands .mobile-vendor-list below it. Mirrors the desktop sidebar. */
364
363
  .mobile-session-new-row {
365
364
  display: flex;
366
365
  gap: 6px;
367
366
  padding: 6px 8px 8px;
368
- border-bottom: 1px solid var(--border-subtle);
369
367
  }
370
368
 
371
- .mobile-session-new-row .mobile-session-new-vendor {
372
- flex: 1 1 0;
369
+ .mobile-session-new-row .mobile-session-new-main {
370
+ flex: 1 1 auto;
373
371
  width: auto;
374
372
  margin-bottom: 0;
375
373
  padding: 10px 12px;
@@ -377,15 +375,61 @@
377
375
  border-radius: 8px;
378
376
  background: rgba(var(--overlay-rgb), 0.03);
379
377
  color: var(--text);
380
- justify-content: center;
381
378
  gap: 8px;
382
379
  }
383
- .mobile-session-new-row .mobile-session-new-vendor:last-of-type {
380
+ .mobile-session-new-row .mobile-session-new-chevron {
381
+ flex: 0 0 auto;
382
+ width: 44px;
383
+ margin-bottom: 0;
384
+ padding: 10px 0;
385
+ border: 1px solid var(--border-subtle);
386
+ border-radius: 8px;
387
+ background: rgba(var(--overlay-rgb), 0.03);
388
+ color: var(--text-muted);
389
+ justify-content: center;
390
+ gap: 0;
391
+ }
392
+ .mobile-session-new-row .mobile-session-new-chevron.expanded svg {
393
+ transform: rotate(180deg);
394
+ }
395
+ .mobile-session-new-row .mobile-session-new-main:active,
396
+ .mobile-session-new-row .mobile-session-new-chevron:active {
397
+ background: rgba(var(--overlay-rgb), 0.1);
398
+ }
399
+
400
+ .mobile-vendor-list {
401
+ display: flex;
402
+ flex-direction: column;
403
+ gap: 4px;
404
+ padding: 0 8px 8px;
384
405
  border-bottom: 1px solid var(--border-subtle);
385
406
  }
386
- .mobile-session-new-row .mobile-session-new-vendor:active {
407
+ .mobile-vendor-list.hidden { display: none; }
408
+
409
+ .mobile-vendor-list .mobile-session-new-vendor {
410
+ width: 100%;
411
+ margin-bottom: 0;
412
+ padding: 10px 12px;
413
+ border: 1px solid var(--border-subtle);
414
+ border-radius: 8px;
415
+ background: rgba(var(--overlay-rgb), 0.03);
416
+ color: var(--text);
417
+ font-weight: 500;
418
+ gap: 8px;
419
+ }
420
+ .mobile-vendor-list .mobile-session-new-vendor.active { font-weight: 700; }
421
+ /* Uninstalled vendors stay tappable: the tap opens the vendor's homepage. */
422
+ .mobile-vendor-list .mobile-session-new-vendor.disabled { opacity: 0.45; }
423
+ .mobile-vendor-list .mobile-session-new-vendor:active {
387
424
  background: rgba(var(--overlay-rgb), 0.1);
388
425
  }
426
+ .mobile-vendor-note {
427
+ margin-left: auto;
428
+ font-size: 12px;
429
+ font-weight: 500;
430
+ color: var(--text-dimmer);
431
+ }
432
+
389
433
  .mobile-session-new-icon {
390
434
  width: 18px;
391
435
  height: 18px;
@@ -993,7 +993,7 @@
993
993
 
994
994
  .session-top-actions {
995
995
  display: grid;
996
- grid-template-columns: repeat(2, minmax(0, 1fr));
996
+ grid-template-columns: minmax(0, 1fr);
997
997
  gap: 2px;
998
998
  padding: 0 0 2px;
999
999
  }
@@ -1240,6 +1240,29 @@
1240
1240
  .session-ctx-item.session-ctx-delete { color: var(--error); }
1241
1241
  .session-ctx-item.session-ctx-delete:hover { background: var(--error-8); }
1242
1242
 
1243
+ /* --- New-session vendor picker --- */
1244
+ .session-new-menu { min-width: 200px; }
1245
+
1246
+ .session-ctx-sep {
1247
+ height: 1px;
1248
+ margin: 4px 0;
1249
+ background: var(--border);
1250
+ }
1251
+
1252
+ .session-new-vendor .session-new-vendor-name { flex: 1 1 auto; min-width: 0; }
1253
+ .session-new-vendor.active { color: var(--text); font-weight: 600; }
1254
+
1255
+ /* Uninstalled vendors stay clickable on purpose: the click opens the
1256
+ vendor's homepage instead of creating a session. */
1257
+ .session-new-vendor.disabled { opacity: 0.45; }
1258
+ .session-new-vendor.disabled:hover { opacity: 0.7; }
1259
+
1260
+ .session-new-vendor-note {
1261
+ font-size: 11px;
1262
+ color: var(--text-dimmer);
1263
+ white-space: nowrap;
1264
+ }
1265
+
1243
1266
  /* --- Session inline rename --- */
1244
1267
  .session-rename-input {
1245
1268
  width: 100%;
@@ -49,69 +49,10 @@ body.tui-suspended #tui-resume-bar {
49
49
  .tui-resume-hint { display: none; }
50
50
  }
51
51
 
52
- /* Policy notice that sits above the embedded xterm in fullscreen TUI
53
- sessions, explaining why this mode exists (post-2026-06-15 Agent SDK
54
- billing split). Thin so it doesn't eat terminal real estate. */
55
- .tui-policy-notice {
56
- display: flex;
57
- align-items: center;
58
- gap: 8px;
59
- flex-shrink: 0;
60
- padding: 6px 10px;
61
- margin-bottom: 4px;
62
- border-radius: 6px;
63
- background: rgba(80, 250, 123, 0.06);
64
- border: 1px solid rgba(80, 250, 123, 0.15);
65
- color: #b8b8b8;
66
- font-size: 11px;
67
- line-height: 1.3;
68
- font-family: "Roboto Mono", Menlo, Monaco, monospace;
69
- }
70
- .tui-policy-icon {
71
- color: #50fa7b;
72
- font-size: 9px;
73
- line-height: 1;
74
- flex-shrink: 0;
75
- }
76
- .tui-policy-text {
77
- flex: 1 1 auto;
78
- min-width: 0;
79
- }
80
- .tui-policy-learn-more {
81
- flex-shrink: 0;
82
- padding: 2px 8px;
83
- border: 1px solid rgba(80, 250, 123, 0.3);
84
- border-radius: 4px;
85
- background: transparent;
86
- color: #50fa7b;
87
- font: inherit;
88
- font-size: 10px;
89
- cursor: pointer;
90
- transition: background 0.15s, border-color 0.15s;
91
- }
92
- .tui-policy-learn-more:hover {
93
- background: rgba(80, 250, 123, 0.1);
94
- border-color: rgba(80, 250, 123, 0.5);
95
- }
96
- .tui-policy-dismiss {
97
- flex-shrink: 0;
98
- width: 18px;
99
- height: 18px;
100
- padding: 0;
101
- border: none;
102
- border-radius: 4px;
103
- background: transparent;
104
- color: #888;
105
- font-size: 14px;
106
- line-height: 1;
107
- cursor: pointer;
108
- }
109
- .tui-policy-dismiss:hover {
110
- background: rgba(255, 255, 255, 0.06);
111
- color: #ddd;
112
- }
113
-
114
- /* Policy info modal. Themed via CSS vars so it follows light/dark. */
52
+ /* Policy info modal. Themed via CSS vars so it follows light/dark.
53
+ The modal itself is gone; .tui-policy-modal-* rules stay because
54
+ What's New article bodies (lib/whats-new-content.js) still use the
55
+ -links class for their footer links. */
115
56
  .tui-policy-modal-backdrop {
116
57
  position: fixed;
117
58
  inset: 0;
@@ -513,6 +513,10 @@
513
513
  <img src="/codex-avatar.png" class="vendor-toggle-icon" alt="Codex">
514
514
  <span class="vendor-toggle-label">Codex</span>
515
515
  </button>
516
+ <button id="vendor-btn-kiro" class="vendor-toggle-btn" data-vendor="kiro">
517
+ <img src="/kiro-avatar.svg" class="vendor-toggle-icon" alt="Kiro">
518
+ <span class="vendor-toggle-label">Kiro CLI</span>
519
+ </button>
516
520
  </div>
517
521
  <div id="active-vendor-indicator" class="hidden" title="Session vendor">
518
522
  <img id="active-vendor-icon" alt="">
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128" role="img" aria-label="Kiro CLI">
2
+ <defs>
3
+ <linearGradient id="kiroBg" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#8b5cf6"/>
5
+ <stop offset="1" stop-color="#6d28d9"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="128" height="128" rx="28" fill="url(#kiroBg)"/>
9
+ <g fill="#ffffff">
10
+ <rect x="34" y="34" width="12" height="60" rx="3"/>
11
+ <rect x="52" y="60" width="42" height="12" rx="3" transform="rotate(-45 73 66)"/>
12
+ <rect x="52" y="60" width="42" height="12" rx="3" transform="rotate(45 73 66)"/>
13
+ </g>
14
+ </svg>