clay-server 2.47.0-beta.1 → 2.47.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/daemon.js +38 -8
- package/lib/kiro-defaults.js +22 -0
- package/lib/project-connection.js +1 -0
- package/lib/project-notifications.js +1 -1
- package/lib/project-sessions.js +13 -2
- package/lib/project.js +31 -7
- package/lib/public/app.js +6 -0
- package/lib/public/css/mobile-nav.css +53 -9
- package/lib/public/css/sidebar.css +24 -1
- package/lib/public/css/tui-attention.css +4 -63
- package/lib/public/index.html +4 -0
- package/lib/public/kiro-avatar.svg +14 -0
- package/lib/public/modules/app-messages.js +11 -10
- package/lib/public/modules/app-panels.js +4 -1
- package/lib/public/modules/app-rendering.js +12 -0
- package/lib/public/modules/input.js +5 -3
- package/lib/public/modules/mate-sidebar.js +3 -3
- package/lib/public/modules/session-tui-view.js +4 -40
- package/lib/public/modules/sidebar-mates.js +1 -1
- package/lib/public/modules/sidebar-mobile.js +56 -23
- package/lib/public/modules/sidebar-sessions.js +126 -63
- package/lib/public/modules/tools.js +1 -1
- package/lib/sdk-bridge.js +50 -5
- package/lib/sdk-message-processor.js +4 -2
- package/lib/server.js +4 -0
- package/lib/users-preferences.js +3 -1
- package/lib/yoke/adapters/kiro.js +1156 -0
- package/lib/yoke/index.js +56 -4
- package/lib/yoke/kiro-acp-server.js +328 -0
- package/package.json +2 -1
|
@@ -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,
|
|
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
|
|
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
|
-
//
|
|
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">×</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
|
|
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
|
-
//
|
|
702
|
-
//
|
|
703
|
-
//
|
|
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
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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(
|
|
718
|
-
|
|
719
|
-
var
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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(
|
|
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();
|
|
@@ -12,6 +12,7 @@ import { dismissOverlayPanels, closeSidebar, updatePageTitle, spawnDustParticles
|
|
|
12
12
|
import { showConfirm } from './app-misc.js';
|
|
13
13
|
import { getUpcomingSchedules } from './scheduler.js';
|
|
14
14
|
import { refreshMobileChatSheet } from './sidebar-mobile.js';
|
|
15
|
+
import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES } from './app-rendering.js';
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
// --- Session state ---
|
|
@@ -292,85 +293,140 @@ function appendSessionCloseButton(el, session) {
|
|
|
292
293
|
el.appendChild(closeBtn);
|
|
293
294
|
}
|
|
294
295
|
|
|
296
|
+
// Resolve the vendor the "New session" button should launch: the project's
|
|
297
|
+
// last-used vendor when its CLI is still installed, else the first installed
|
|
298
|
+
// vendor, else Claude (so the button always has a sane label to render).
|
|
299
|
+
export function resolveDefaultVendor() {
|
|
300
|
+
var installed = store.get('installedVendors') || [];
|
|
301
|
+
var last = store.get('lastVendor') || "";
|
|
302
|
+
if (last && installed.indexOf(last) !== -1) return last;
|
|
303
|
+
for (var i = 0; i < VENDOR_ORDER.length; i++) {
|
|
304
|
+
if (installed.indexOf(VENDOR_ORDER[i]) !== -1) return VENDOR_ORDER[i];
|
|
305
|
+
}
|
|
306
|
+
return "claude";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Single entry point for session creation from the sidebar. Optimistically
|
|
310
|
+
// bind the composer to the requested vendor before the server round-trip.
|
|
311
|
+
// Otherwise vendorSelectionLocked from the previous session can reject the
|
|
312
|
+
// new config_state and leave a Kiro/Codex session rendered as Claude.
|
|
313
|
+
export function startNewSession(vendor, extra) {
|
|
314
|
+
if (!getWs() || !store.get('connected')) return;
|
|
315
|
+
var payload = { type: "new_session", vendor: vendor };
|
|
316
|
+
if (extra) {
|
|
317
|
+
var keys = Object.keys(extra);
|
|
318
|
+
for (var i = 0; i < keys.length; i++) payload[keys[i]] = extra[keys[i]];
|
|
319
|
+
}
|
|
320
|
+
getWs().send(JSON.stringify(payload));
|
|
321
|
+
store.set({
|
|
322
|
+
lastVendor: vendor,
|
|
323
|
+
currentVendor: vendor,
|
|
324
|
+
currentModel: "",
|
|
325
|
+
currentModels: [],
|
|
326
|
+
vendorSelectionLocked: true,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
295
330
|
function renderSessionTopActions() {
|
|
296
331
|
var wrap = document.createElement("div");
|
|
297
332
|
wrap.className = "session-top-actions";
|
|
298
333
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
var
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
334
|
+
// Single split button. The main half creates a session with the project's
|
|
335
|
+
// last-used vendor (for Claude the server applies the claudeOpenMode pref);
|
|
336
|
+
// the chevron opens the vendor picker plus alternate launch modes.
|
|
337
|
+
var defaultVendor = resolveDefaultVendor();
|
|
338
|
+
|
|
339
|
+
var cell = document.createElement("div");
|
|
340
|
+
cell.className = "session-top-action-split";
|
|
341
|
+
|
|
342
|
+
var mainBtn = document.createElement("button");
|
|
343
|
+
mainBtn.className = "session-top-action split-main";
|
|
344
|
+
mainBtn.type = "button";
|
|
345
|
+
mainBtn.title = "New " + (VENDOR_NAMES[defaultVendor] || defaultVendor) + " session";
|
|
346
|
+
mainBtn.innerHTML = '<img src="' + (VENDOR_AVATARS[defaultVendor] || VENDOR_AVATARS.claude) +
|
|
347
|
+
'" class="session-top-action-icon" alt=""><span>New session</span>';
|
|
348
|
+
mainBtn.addEventListener("click", function () {
|
|
349
|
+
startNewSession(defaultVendor);
|
|
314
350
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
var
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
351
|
+
cell.appendChild(mainBtn);
|
|
352
|
+
|
|
353
|
+
var chevron = document.createElement("button");
|
|
354
|
+
chevron.className = "session-top-action split-chevron";
|
|
355
|
+
chevron.type = "button";
|
|
356
|
+
chevron.title = "Choose a vendor or launch mode";
|
|
357
|
+
chevron.setAttribute("aria-label", "Choose a vendor or launch mode");
|
|
358
|
+
chevron.innerHTML = iconHtml("chevron-down");
|
|
359
|
+
chevron.addEventListener("click", function (e) {
|
|
324
360
|
e.preventDefault();
|
|
325
361
|
e.stopPropagation();
|
|
326
362
|
if (sessionCtxMenu) { closeSessionCtxMenu(); return; }
|
|
327
|
-
|
|
363
|
+
showNewSessionMenu(chevron, defaultVendor);
|
|
328
364
|
});
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
wrap.appendChild(claudeCell);
|
|
332
|
-
|
|
333
|
-
// Codex: always GUI (no TUI adapter for Codex).
|
|
334
|
-
var codexBtn = document.createElement("button");
|
|
335
|
-
codexBtn.className = "session-top-action";
|
|
336
|
-
codexBtn.type = "button";
|
|
337
|
-
codexBtn.title = "New Codex session";
|
|
338
|
-
codexBtn.innerHTML = '<img src="/codex-avatar.png" class="session-top-action-icon" alt=""><span>Codex</span>';
|
|
339
|
-
codexBtn.addEventListener("click", function () {
|
|
340
|
-
if (getWs() && store.get('connected')) {
|
|
341
|
-
getWs().send(JSON.stringify({ type: "new_session", vendor: "codex" }));
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
wrap.appendChild(codexBtn);
|
|
365
|
+
cell.appendChild(chevron);
|
|
345
366
|
|
|
367
|
+
wrap.appendChild(cell);
|
|
346
368
|
return wrap;
|
|
347
369
|
}
|
|
348
370
|
|
|
349
|
-
// Dropdown anchored to the
|
|
350
|
-
//
|
|
351
|
-
function
|
|
371
|
+
// Dropdown anchored to the split-button chevron. Reuses the session-ctx-menu
|
|
372
|
+
// element/var so the global document click handler closes it.
|
|
373
|
+
function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
352
374
|
closeSessionCtxMenu();
|
|
353
375
|
|
|
354
376
|
var menu = document.createElement("div");
|
|
355
|
-
menu.className = "session-ctx-menu";
|
|
377
|
+
menu.className = "session-ctx-menu session-new-menu";
|
|
378
|
+
var installed = store.get('installedVendors') || [];
|
|
379
|
+
|
|
380
|
+
// Every known vendor is listed. Uninstalled ones stay visible but disabled,
|
|
381
|
+
// and clicking one opens the vendor's homepage so the user can install it.
|
|
382
|
+
for (var i = 0; i < VENDOR_ORDER.length; i++) {
|
|
383
|
+
(function (vendor) {
|
|
384
|
+
var isInstalled = installed.indexOf(vendor) !== -1;
|
|
385
|
+
var name = VENDOR_NAMES[vendor] || vendor;
|
|
386
|
+
var item = document.createElement("button");
|
|
387
|
+
item.className = "session-ctx-item session-new-vendor";
|
|
388
|
+
if (!isInstalled) item.classList.add("disabled");
|
|
389
|
+
if (vendor === defaultVendor) item.classList.add("active");
|
|
390
|
+
item.type = "button";
|
|
391
|
+
item.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="session-top-action-icon" alt="">' +
|
|
392
|
+
'<span class="session-new-vendor-name">' + name + '</span>' +
|
|
393
|
+
(isInstalled ? "" : '<span class="session-new-vendor-note">Not installed</span>' + iconHtml("external-link"));
|
|
394
|
+
item.title = isInstalled
|
|
395
|
+
? "New " + name + " session"
|
|
396
|
+
: name + " is not installed - open " + VENDOR_HOMEPAGES[vendor];
|
|
397
|
+
item.addEventListener("click", function (e) {
|
|
398
|
+
e.stopPropagation();
|
|
399
|
+
closeSessionCtxMenu();
|
|
400
|
+
if (!isInstalled) {
|
|
401
|
+
window.open(VENDOR_HOMEPAGES[vendor], "_blank", "noopener");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
startNewSession(vendor);
|
|
405
|
+
});
|
|
406
|
+
menu.appendChild(item);
|
|
407
|
+
})(VENDOR_ORDER[i]);
|
|
408
|
+
}
|
|
356
409
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
410
|
+
// Claude-only launch mode: a bare TUI shell with permission prompts off.
|
|
411
|
+
// Hidden in GUI mode (the default) because the item only ever produces a
|
|
412
|
+
// terminal session, which contradicts what the rest of the menu creates.
|
|
413
|
+
if (installed.indexOf("claude") !== -1 && store.get('claudeOpenMode') === "tui") {
|
|
414
|
+
var sep = document.createElement("div");
|
|
415
|
+
sep.className = "session-ctx-sep";
|
|
416
|
+
menu.appendChild(sep);
|
|
417
|
+
|
|
418
|
+
var skipItem = document.createElement("button");
|
|
419
|
+
skipItem.className = "session-ctx-item";
|
|
420
|
+
skipItem.type = "button";
|
|
421
|
+
skipItem.innerHTML = iconHtml("shield-off") + " <span>Skip permissions (TUI)</span>";
|
|
422
|
+
skipItem.title = "Start a terminal session with --dangerously-skip-permissions";
|
|
423
|
+
skipItem.addEventListener("click", function (e) {
|
|
424
|
+
e.stopPropagation();
|
|
425
|
+
closeSessionCtxMenu();
|
|
426
|
+
startNewSession("claude", { mode: "tui", dangerouslySkipPermissions: true });
|
|
427
|
+
});
|
|
428
|
+
menu.appendChild(skipItem);
|
|
429
|
+
}
|
|
374
430
|
|
|
375
431
|
document.body.appendChild(menu);
|
|
376
432
|
sessionCtxMenu = menu;
|
|
@@ -528,6 +584,14 @@ export function initSidebarSessions() {
|
|
|
528
584
|
syncHeaderSearchUi();
|
|
529
585
|
}
|
|
530
586
|
|
|
587
|
+
// The "New session" split button renders from lastVendor + installedVendors,
|
|
588
|
+
// so a late-arriving vendor detection or a vendor switch has to repaint it.
|
|
589
|
+
store.subscribe(function (state, prev) {
|
|
590
|
+
if (state.lastVendor !== prev.lastVendor || state.installedVendors !== prev.installedVendors) {
|
|
591
|
+
renderSessionList(null);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
531
595
|
// --- Resume session picker ---
|
|
532
596
|
// --- Schedule countdown timer ---
|
|
533
597
|
startCountdownTimer();
|
|
@@ -1411,4 +1475,3 @@ function relativeTime(isoString) {
|
|
|
1411
1475
|
if (days < 30) return days + "d ago";
|
|
1412
1476
|
return new Date(isoString).toLocaleDateString();
|
|
1413
1477
|
}
|
|
1414
|
-
|
|
@@ -814,7 +814,7 @@ function resolvePermissionIdentity(mateId, vendor) {
|
|
|
814
814
|
}
|
|
815
815
|
}
|
|
816
816
|
// Project chat: use vendor name and avatar
|
|
817
|
-
var vendorAvatars = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png" };
|
|
817
|
+
var vendorAvatars = { claude: "/claude-code-avatar.png", codex: "/codex-avatar.png", kiro: "/kiro-avatar.svg" };
|
|
818
818
|
var vendorName = (vendor && VENDOR_NAMES[vendor]) || VENDOR_NAMES.claude;
|
|
819
819
|
return {
|
|
820
820
|
name: vendorName,
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -5,6 +5,7 @@ var execSync = require("child_process").execSync;
|
|
|
5
5
|
var execFileSync = require("child_process").execFileSync;
|
|
6
6
|
var usersModule = require("./users");
|
|
7
7
|
var { getCodexConfig } = require("./codex-defaults");
|
|
8
|
+
var { getKiroConfig } = require("./kiro-defaults");
|
|
8
9
|
var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
|
|
9
10
|
var { isSafeBashSegment } = require("./safe-bash-commands");
|
|
10
11
|
var { createMessageQueue } = require("./sdk-message-queue");
|
|
@@ -151,6 +152,7 @@ function createSDKBridge(opts) {
|
|
|
151
152
|
function getLoginCommand(vendor) {
|
|
152
153
|
if (vendor === "codex") return "codex login --device-auth";
|
|
153
154
|
if (vendor === "claude") return "claude login";
|
|
155
|
+
if (vendor === "kiro") return "kiro-cli login";
|
|
154
156
|
return (vendor || "claude") + " login";
|
|
155
157
|
}
|
|
156
158
|
|
|
@@ -799,7 +801,7 @@ function createSDKBridge(opts) {
|
|
|
799
801
|
onProcessingChanged();
|
|
800
802
|
send({ type: "status", processing: false });
|
|
801
803
|
sendAndRecord(session, { type: "thinking_stop" });
|
|
802
|
-
var interruptMsg = (session.vendor
|
|
804
|
+
var interruptMsg = (session.vendor && session.vendor !== "claude")
|
|
803
805
|
? "\u25a0 Conversation interrupted - tell the model what to do differently."
|
|
804
806
|
: "Interrupted \u00b7 What should Claude do instead?";
|
|
805
807
|
sendAndRecord(session, { type: "info", text: interruptMsg });
|
|
@@ -813,7 +815,7 @@ function createSDKBridge(opts) {
|
|
|
813
815
|
if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
|
|
814
816
|
if (!session.destroying) {
|
|
815
817
|
sendAndRecord(session, { type: "thinking_stop" });
|
|
816
|
-
var interruptMsg2 = (session.vendor
|
|
818
|
+
var interruptMsg2 = (session.vendor && session.vendor !== "claude")
|
|
817
819
|
? "\u25a0 Conversation interrupted - tell the model what to do differently."
|
|
818
820
|
: "Interrupted \u00b7 What should Claude do instead?";
|
|
819
821
|
sendAndRecord(session, { type: "info", text: interruptMsg2 });
|
|
@@ -867,7 +869,8 @@ function createSDKBridge(opts) {
|
|
|
867
869
|
var canAutoLogin = !usersModule.isMultiUser()
|
|
868
870
|
|| !!authLinuxUser
|
|
869
871
|
|| (authUser && authUser.role === "admin");
|
|
870
|
-
var
|
|
872
|
+
var authVendorLabel = session.vendor === "codex" ? "Codex" : (session.vendor === "kiro" ? "Kiro CLI" : "Claude Code");
|
|
873
|
+
var authTitle = authVendorLabel + " is not logged in.";
|
|
871
874
|
var authMsg = {
|
|
872
875
|
type: "auth_required",
|
|
873
876
|
text: authTitle,
|
|
@@ -1067,6 +1070,7 @@ function createSDKBridge(opts) {
|
|
|
1067
1070
|
async function startQuery(session, text, images, linuxUser) {
|
|
1068
1071
|
async function ensureVendorReady(vendor) {
|
|
1069
1072
|
if (!vendor) return null;
|
|
1073
|
+
if (linuxUser && vendor === "kiro") return null;
|
|
1070
1074
|
var vendorAdapter = adapters[vendor] || null;
|
|
1071
1075
|
if (!vendorAdapter) {
|
|
1072
1076
|
var yoke = require("./yoke");
|
|
@@ -1091,7 +1095,7 @@ function createSDKBridge(opts) {
|
|
|
1091
1095
|
});
|
|
1092
1096
|
}
|
|
1093
1097
|
if (vendorAdapter) {
|
|
1094
|
-
sm.availableVendors =
|
|
1098
|
+
sm.availableVendors = getAvailableVendors(linuxUser);
|
|
1095
1099
|
sm.modelsByVendor = sm.modelsByVendor || {};
|
|
1096
1100
|
if (!sm.modelsByVendor[vendor] && typeof vendorAdapter.supportedModels === "function") {
|
|
1097
1101
|
sm.modelsByVendor[vendor] = await vendorAdapter.supportedModels();
|
|
@@ -1100,6 +1104,20 @@ function createSDKBridge(opts) {
|
|
|
1100
1104
|
return vendorAdapter;
|
|
1101
1105
|
}
|
|
1102
1106
|
|
|
1107
|
+
// Kiro ACP currently spawns as the daemon user. Running it for an
|
|
1108
|
+
// OS-isolated session would expose the daemon user's HOME and Kiro
|
|
1109
|
+
// credentials to another user, so refuse until the adapter has a
|
|
1110
|
+
// per-user worker path.
|
|
1111
|
+
if (linuxUser && session.vendor === "kiro") {
|
|
1112
|
+
console.error("[sdk-bridge] Refusing Kiro for OS-isolated user " + linuxUser + ": per-user ACP spawning is not implemented");
|
|
1113
|
+
sendAndRecord(session, {
|
|
1114
|
+
type: "error",
|
|
1115
|
+
text: "Kiro is not available for OS-isolated users yet.",
|
|
1116
|
+
});
|
|
1117
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1103
1121
|
// If vendor is set but adapter not ready, try lazy creation (user may have logged in)
|
|
1104
1122
|
if (session.vendor && !adapters[session.vendor]) {
|
|
1105
1123
|
var lazyAdapter = await ensureVendorReady(session.vendor);
|
|
@@ -1292,6 +1310,7 @@ function createSDKBridge(opts) {
|
|
|
1292
1310
|
}
|
|
1293
1311
|
|
|
1294
1312
|
var codexConfig = getCodexConfig(sm);
|
|
1313
|
+
var kiroConfig = getKiroConfig(sm);
|
|
1295
1314
|
var mergedMcpServers = mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined;
|
|
1296
1315
|
|
|
1297
1316
|
// Derive an explicit session title for fresh queries so the SDK records
|
|
@@ -1346,6 +1365,14 @@ function createSDKBridge(opts) {
|
|
|
1346
1365
|
sandboxMode: codexConfig.sandbox,
|
|
1347
1366
|
webSearchMode: codexConfig.webSearch,
|
|
1348
1367
|
},
|
|
1368
|
+
KIRO: {
|
|
1369
|
+
engine: kiroConfig.engine,
|
|
1370
|
+
mode: kiroConfig.mode,
|
|
1371
|
+
// Kiro reads its native ~/.kiro/settings/mcp.json. Clay-managed MCP
|
|
1372
|
+
// servers are intentionally not forwarded until the ACP entry shape
|
|
1373
|
+
// and duplicate-registration behavior are verified against CLI 3.x.
|
|
1374
|
+
mcpServers: [],
|
|
1375
|
+
},
|
|
1349
1376
|
},
|
|
1350
1377
|
};
|
|
1351
1378
|
|
|
@@ -1502,9 +1529,27 @@ function createSDKBridge(opts) {
|
|
|
1502
1529
|
} catch (e) {}
|
|
1503
1530
|
if ((codexBin && fs.existsSync(codexBin)) || tryLookup("codex")) result.push("codex");
|
|
1504
1531
|
|
|
1532
|
+
// Kiro has no per-user ACP spawn path yet. Do not advertise the daemon's
|
|
1533
|
+
// binary to an OS-isolated user, even if that user also has Kiro installed.
|
|
1534
|
+
if (!linuxUser) {
|
|
1535
|
+
var kiroBin = null;
|
|
1536
|
+
try {
|
|
1537
|
+
kiroBin = require("./yoke/kiro-acp-server").findKiroPath();
|
|
1538
|
+
} catch (e) {}
|
|
1539
|
+
if ((kiroBin && fs.existsSync(kiroBin)) || tryLookup("kiro-cli")) result.push("kiro");
|
|
1540
|
+
} else {
|
|
1541
|
+
console.log("[sdk-bridge] Kiro hidden for OS-isolated user " + linuxUser + ": per-user ACP spawning is not implemented");
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1505
1544
|
return result;
|
|
1506
1545
|
}
|
|
1507
1546
|
|
|
1547
|
+
function getAvailableVendors(linuxUser) {
|
|
1548
|
+
return Object.keys(adapters).filter(function(vendor) {
|
|
1549
|
+
return !(linuxUser && vendor === "kiro");
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1508
1553
|
// SDK warmup: initialize all available adapters and collect models.
|
|
1509
1554
|
// The default adapter is initialized first for slash_commands and skills.
|
|
1510
1555
|
// Passes linuxUser to adapter for worker-based warmup when OS isolation is needed.
|
|
@@ -1568,7 +1613,7 @@ function createSDKBridge(opts) {
|
|
|
1568
1613
|
|
|
1569
1614
|
// Detect installed vendors per-user (binary existence check)
|
|
1570
1615
|
sm.installedVendors = detectInstalledVendors(linuxUser);
|
|
1571
|
-
sm.availableVendors =
|
|
1616
|
+
sm.availableVendors = getAvailableVendors(linuxUser);
|
|
1572
1617
|
|
|
1573
1618
|
// Send initial state to client
|
|
1574
1619
|
send({
|
|
@@ -50,10 +50,12 @@ function attachMessageProcessor(ctx) {
|
|
|
50
50
|
var canAutoLogin = !usersModule.isMultiUser()
|
|
51
51
|
|| !!authLinuxUser
|
|
52
52
|
|| (authUser && authUser.role === "admin");
|
|
53
|
-
var authTitle = session.vendor === "codex"
|
|
53
|
+
var authTitle = session.vendor === "codex"
|
|
54
|
+
? "Codex is not logged in."
|
|
55
|
+
: (session.vendor === "kiro" ? "Kiro CLI is not logged in." : "Claude Code is not logged in.");
|
|
54
56
|
var loginCommand = session.vendor === "codex"
|
|
55
57
|
? "codex login --device-auth"
|
|
56
|
-
: "claude login";
|
|
58
|
+
: (session.vendor === "kiro" ? "kiro-cli login" : "claude login");
|
|
57
59
|
var _nmLogin = getNotificationsModule();
|
|
58
60
|
sendAndRecord(session, {
|
|
59
61
|
type: "auth_required",
|
package/lib/server.js
CHANGED
|
@@ -148,6 +148,8 @@ function createServer(opts) {
|
|
|
148
148
|
var onSetServerDefaultMode = opts.onSetServerDefaultMode || null;
|
|
149
149
|
var onGetProjectDefaultMode = opts.onGetProjectDefaultMode || null;
|
|
150
150
|
var onSetProjectDefaultMode = opts.onSetProjectDefaultMode || null;
|
|
151
|
+
var onGetProjectLastVendor = opts.onGetProjectLastVendor || null;
|
|
152
|
+
var onSetProjectLastVendor = opts.onSetProjectLastVendor || null;
|
|
151
153
|
var onGetProjectMcpServers = opts.onGetProjectMcpServers || null;
|
|
152
154
|
var onSetProjectMcpServers = opts.onSetProjectMcpServers || null;
|
|
153
155
|
var onGetDaemonConfig = opts.onGetDaemonConfig || null;
|
|
@@ -1253,6 +1255,8 @@ function createServer(opts) {
|
|
|
1253
1255
|
onSetServerDefaultMode: onSetServerDefaultMode,
|
|
1254
1256
|
onGetProjectDefaultMode: onGetProjectDefaultMode,
|
|
1255
1257
|
onSetProjectDefaultMode: onSetProjectDefaultMode,
|
|
1258
|
+
onGetProjectLastVendor: onGetProjectLastVendor,
|
|
1259
|
+
onSetProjectLastVendor: onSetProjectLastVendor,
|
|
1256
1260
|
onGetProjectMcpServers: onGetProjectMcpServers,
|
|
1257
1261
|
onSetProjectMcpServers: onSetProjectMcpServers,
|
|
1258
1262
|
onGetDaemonConfig: onGetDaemonConfig,
|
package/lib/users-preferences.js
CHANGED
|
@@ -274,7 +274,9 @@ function attachPreferences(deps) {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
function setClaudeOpenMode(userId, mode) {
|
|
277
|
-
|
|
277
|
+
// GUI is the default in every other resolution path, so an unrecognized
|
|
278
|
+
// value must land on GUI too. Only an explicit "tui" opts into terminal.
|
|
279
|
+
var normalized = (mode === "tui") ? "tui" : "gui";
|
|
278
280
|
var data = loadUsers();
|
|
279
281
|
for (var i = 0; i < data.users.length; i++) {
|
|
280
282
|
if (data.users[i].id === userId) {
|