clay-server 4.0.0-beta.1 → 4.0.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.
- package/lib/background-task-timing.js +72 -0
- package/lib/project-connection.js +4 -0
- package/lib/project-vendor-login.js +397 -0
- package/lib/project.js +20 -0
- package/lib/public/css/input.css +73 -25
- package/lib/public/modules/app-messages.js +24 -17
- package/lib/public/modules/app-notifications.js +4 -117
- package/lib/public/modules/background-tasks-ui.js +109 -15
- package/lib/public/modules/pane-bridge.js +9 -0
- package/lib/public/modules/split-view.js +7 -0
- package/lib/public/modules/tui-attention.js +10 -0
- package/lib/public/modules/vendor-login.js +287 -0
- package/lib/sdk-message-processor.js +10 -2
- package/lib/tools-registry.js +11 -1
- package/lib/ws-schema.js +7 -0
- package/lib/yoke/adapters/claude.js +6 -1
- package/lib/yoke/adapters/codex.js +3 -4
- package/lib/yoke/codex-app-server.js +20 -2
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/package.json +2 -2
|
@@ -37,7 +37,7 @@ import { updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeep
|
|
|
37
37
|
import { handleTermList, handleTermCreated, sendTerminalCommand, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed } from './terminal.js';
|
|
38
38
|
import { attachTuiView, detachTuiView, setTuiSuspendedView, tuiHandleTermOutput, tuiHandleTermResized, tuiHandleTermExited, tuiHandleTermClosed } from './session-tui-view.js';
|
|
39
39
|
import { handleTuiTranscriptState } from './tui-grab.js';
|
|
40
|
-
import { tuiModalHandleTermOutput, tuiModalHandleTermResized, tuiModalHandleTermExited, tuiModalHandleTermClosed
|
|
40
|
+
import { tuiModalHandleTermOutput, tuiModalHandleTermResized, tuiModalHandleTermExited, tuiModalHandleTermClosed } from './tui-attention.js';
|
|
41
41
|
import { updateTerminalList, handleContextSourcesState, updateEmailAccountList, updateEmailUnreadCounts, handleEmailTestResult, handleEmailAddResult, handleEmailRemoveResult } from './context-sources.js';
|
|
42
42
|
import { refreshEmailSettings } from './user-settings.js';
|
|
43
43
|
import { handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, handleNoteWritten } from './sticky-notes.js';
|
|
@@ -72,7 +72,8 @@ import { handleRemoteCursorMove, handleRemoteCursorLeave, handleRemoteSelection,
|
|
|
72
72
|
import { showLoopBanner, updateLoopBanner, updateLoopInputVisibility, showRalphApprovalBar, updateRalphApprovalStatus, openRalphPreviewModal, showExecModal, updateExecModalStatus } from './app-loop-ui.js';
|
|
73
73
|
import { showDebateSticky, showDebateConcludeConfirm, showDebateUserFloor, exitDebateFloorMode, exitDebateConcludeMode, exitDebateEndedMode, updateDebateRound, renderDebateUserFloorDone } from './app-debate-ui.js';
|
|
74
74
|
import { handleSkillInstallWs } from './app-skills-install.js';
|
|
75
|
-
import { handleNotificationsState, handleNotificationCreated, handleNotificationDismissed, handleNotificationDismissedAll, showUpdateBanner
|
|
75
|
+
import { handleNotificationsState, handleNotificationCreated, handleNotificationDismissed, handleNotificationDismissedAll, showUpdateBanner } from './app-notifications.js';
|
|
76
|
+
import { autoStartLoginIfNeeded, handleVendorLoginReady, handleVendorLoginState, handleVendorLoginError, handleAuthRefreshed } from './vendor-login.js';
|
|
76
77
|
import { handleDebatePreparing, handleDebateBriefReady, renderDebateBriefReady, handleDebateStarted, renderDebateStarted, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, renderDebateCommentInjected, handleDebateResumed, handleDebateEnded, renderDebateEnded, handleDebateError, isDebateActive, renderMcpDebateProposal, renderDebateUserResume } from './debate.js';
|
|
77
78
|
import { handleMentionStart, handleMentionActivity, handleMentionStream, handleMentionDone, handleMentionError, renderMentionUser, renderMentionResponse, renderUserMention } from './mention.js';
|
|
78
79
|
|
|
@@ -1299,11 +1300,28 @@ export function processMessage(msg) {
|
|
|
1299
1300
|
appendDelta((msg.text || "Authentication required.") + "\n");
|
|
1300
1301
|
setStatus("connected");
|
|
1301
1302
|
if (!store.get('loopActive')) enableMainInput();
|
|
1302
|
-
//
|
|
1303
|
-
//
|
|
1303
|
+
// Open the login flow for this vendor. The server keeps exactly one
|
|
1304
|
+
// login terminal per vendor, so repeat auth_required events (other
|
|
1305
|
+
// sessions, split panes, a 401 retry burst) do not stack up.
|
|
1304
1306
|
autoStartLoginIfNeeded(msg);
|
|
1305
1307
|
break;
|
|
1306
1308
|
|
|
1309
|
+
case "vendor_login_ready":
|
|
1310
|
+
handleVendorLoginReady(msg);
|
|
1311
|
+
break;
|
|
1312
|
+
|
|
1313
|
+
case "vendor_login_state":
|
|
1314
|
+
handleVendorLoginState(msg);
|
|
1315
|
+
break;
|
|
1316
|
+
|
|
1317
|
+
case "vendor_login_error":
|
|
1318
|
+
handleVendorLoginError(msg);
|
|
1319
|
+
break;
|
|
1320
|
+
|
|
1321
|
+
case "auth_refreshed":
|
|
1322
|
+
handleAuthRefreshed(msg);
|
|
1323
|
+
break;
|
|
1324
|
+
|
|
1307
1325
|
case "rate_limit":
|
|
1308
1326
|
handleRateLimitEvent(msg);
|
|
1309
1327
|
break;
|
|
@@ -1492,19 +1510,8 @@ export function processMessage(msg) {
|
|
|
1492
1510
|
break;
|
|
1493
1511
|
|
|
1494
1512
|
case "term_created":
|
|
1495
|
-
// Login
|
|
1496
|
-
//
|
|
1497
|
-
// so the login URL is visible) instead of the sidebar terminal.
|
|
1498
|
-
if (store.get('pendingLoginModal')) {
|
|
1499
|
-
var _lm = store.get('pendingLoginModal');
|
|
1500
|
-
store.set({ pendingLoginModal: null });
|
|
1501
|
-
openTuiModal(msg.id, _lm.slug, {
|
|
1502
|
-
sessionTitle: (VENDOR_NAMES[_lm.vendor] || "Claude Code") + " login",
|
|
1503
|
-
projectName: _lm.slug,
|
|
1504
|
-
compact: true,
|
|
1505
|
-
});
|
|
1506
|
-
break;
|
|
1507
|
-
}
|
|
1513
|
+
// Login terminals never come through here: they are created by the
|
|
1514
|
+
// server-owned flow and announced with vendor_login_ready instead.
|
|
1508
1515
|
handleTermCreated(msg);
|
|
1509
1516
|
if (store.get('pendingTermCommand')) {
|
|
1510
1517
|
var cmd = store.get('pendingTermCommand');
|
|
@@ -10,8 +10,8 @@ import { showHomeHub } from './app-home-hub.js';
|
|
|
10
10
|
import { openHomeChat } from './home-mate-chat.js';
|
|
11
11
|
import { getCachedProjects, switchProject, renderProjectList } from './app-projects.js';
|
|
12
12
|
import { mateAvatarUrl, userAvatarUrl } from './avatar.js';
|
|
13
|
-
import { openTerminal } from './terminal.js';
|
|
14
13
|
import { openTuiModal } from './tui-attention.js';
|
|
14
|
+
import { requestVendorLogin } from './vendor-login.js';
|
|
15
15
|
import { startUrgentBlink, stopUrgentBlink } from './app-favicon.js';
|
|
16
16
|
import { playDoneSound, isNotifSoundEnabled } from './notifications.js';
|
|
17
17
|
var notifications = [];
|
|
@@ -47,7 +47,6 @@ var pendingTuiTotal = 0;
|
|
|
47
47
|
// (next hour) acts as a fresh ping.
|
|
48
48
|
var pendingUpdateMsg = null;
|
|
49
49
|
var activeAuthRequiredMsg = null;
|
|
50
|
-
var authReminderVisible = false;
|
|
51
50
|
|
|
52
51
|
// ========================================================
|
|
53
52
|
// Init
|
|
@@ -91,7 +90,6 @@ function showAllBanners() {
|
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
if (activeAuthRequiredMsg) showAuthRequiredBanner(activeAuthRequiredMsg);
|
|
94
|
-
if (authReminderVisible) showLoginReminderBanner();
|
|
95
93
|
|
|
96
94
|
// Check if any banner actually got rendered (update/auth banners can be suppressed)
|
|
97
95
|
var hasVisibleBanner = bannerContainer.children.length > 0;
|
|
@@ -266,8 +264,9 @@ function showBanner(notif, autoDismissMs) {
|
|
|
266
264
|
removeBanner(banner);
|
|
267
265
|
dismissNotif(notif.id);
|
|
268
266
|
var authMeta = notif.meta || {};
|
|
269
|
-
|
|
270
|
-
|
|
267
|
+
// Deliberate click: re-attach to a live login terminal instead of
|
|
268
|
+
// spawning a second one (the server dedups per vendor).
|
|
269
|
+
requestVendorLogin(authMeta.vendor || "claude", { sessionId: notif.sessionId || null });
|
|
271
270
|
});
|
|
272
271
|
}
|
|
273
272
|
}
|
|
@@ -325,118 +324,6 @@ function showBanner(notif, autoDismissMs) {
|
|
|
325
324
|
}
|
|
326
325
|
}
|
|
327
326
|
|
|
328
|
-
// Open the login terminal as a modal dialog (the same modal used for TUI
|
|
329
|
-
// attention), running the vendor login command. The terminal is created with
|
|
330
|
-
// the command as its initial input, so when the modal attaches it replays the
|
|
331
|
-
// scrollback and the user sees the login URL / prompts immediately.
|
|
332
|
-
// Falls back to the sidebar terminal if the current project slug is unknown
|
|
333
|
-
// (the modal needs a slug to open its own WS to the project).
|
|
334
|
-
function currentProjectSlug() {
|
|
335
|
-
var slug = store.get('currentSlug') || "";
|
|
336
|
-
if (slug) return slug;
|
|
337
|
-
// Fallback: derive from the URL (/p/<slug>/...).
|
|
338
|
-
try {
|
|
339
|
-
var m = (window.location.pathname || "").match(/^\/p\/([a-z0-9_-]+)/);
|
|
340
|
-
if (m) return m[1];
|
|
341
|
-
} catch (e) {}
|
|
342
|
-
return "";
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function startLoginInModal(loginCommand, vendor) {
|
|
346
|
-
if (authReminderVisible) return;
|
|
347
|
-
var ws = getWs();
|
|
348
|
-
if (!ws || ws.readyState !== 1) return;
|
|
349
|
-
var cmd = loginCommand || getVendorLoginCommand(vendor);
|
|
350
|
-
var slug = currentProjectSlug();
|
|
351
|
-
if (!slug) { startLoginCommand(cmd); return; }
|
|
352
|
-
store.set({ pendingLoginModal: { slug: slug, vendor: vendor || "claude" } });
|
|
353
|
-
ws.send(JSON.stringify({
|
|
354
|
-
type: "term_create",
|
|
355
|
-
cols: 100,
|
|
356
|
-
rows: 30,
|
|
357
|
-
initialCommand: cmd + "\n",
|
|
358
|
-
}));
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Sidebar-terminal fallback (used when no slug is available for the modal).
|
|
362
|
-
function startLoginCommand(loginCommand) {
|
|
363
|
-
if (authReminderVisible) return;
|
|
364
|
-
var ws = getWs();
|
|
365
|
-
if (!ws || ws.readyState !== 1) return;
|
|
366
|
-
var termCommand = (loginCommand || "claude login") + "\n";
|
|
367
|
-
store.set({ pendingTermCommand: termCommand });
|
|
368
|
-
ws.send(JSON.stringify({ type: "term_create", cols: 80, rows: 24 }));
|
|
369
|
-
openTerminal();
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Auto-open the login modal and run the login command when the server reports
|
|
373
|
-
// the vendor isn't logged in. The popup terminal runs as the connected user's
|
|
374
|
-
// own OS identity (term_create -> getOsUserInfoForWs), so login always targets
|
|
375
|
-
// the right account — we pop it unconditionally rather than gating on
|
|
376
|
-
// canAutoLogin. Idempotent: startLoginInModal is guarded by authReminderVisible
|
|
377
|
-
// so repeat auth_required events no-op.
|
|
378
|
-
export function autoStartLoginIfNeeded(msg) {
|
|
379
|
-
if (!msg) return false;
|
|
380
|
-
if (authReminderVisible) return false;
|
|
381
|
-
var vendor = msg.vendor || "claude";
|
|
382
|
-
var cmd = msg.loginCommand
|
|
383
|
-
|| getVendorLoginCommand(vendor);
|
|
384
|
-
startLoginInModal(cmd, vendor);
|
|
385
|
-
showLoginReminderBanner();
|
|
386
|
-
return true;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function showLoginReminderBanner() {
|
|
390
|
-
if (!bannerContainer) return;
|
|
391
|
-
authReminderVisible = true;
|
|
392
|
-
var existing = bannerContainer.querySelector('[data-auth-reminder="true"]');
|
|
393
|
-
if (existing) removeBanner(existing);
|
|
394
|
-
var banner = document.createElement("div");
|
|
395
|
-
banner.className = "notif-banner notif-banner-update";
|
|
396
|
-
banner.setAttribute("data-notif-id", "_auth_reminder");
|
|
397
|
-
banner.setAttribute("data-auth-reminder", "true");
|
|
398
|
-
banner.innerHTML =
|
|
399
|
-
'<div class="notif-banner-icon">' + iconHtml("check-circle") + '</div>' +
|
|
400
|
-
'<div class="notif-banner-body">' +
|
|
401
|
-
'<div class="notif-banner-project">CLAY</div>' +
|
|
402
|
-
'<div class="notif-banner-title">Login started</div>' +
|
|
403
|
-
'<div class="notif-banner-text">After the login completes, open a new session so Clay picks up the fresh auth state.</div>' +
|
|
404
|
-
'<div class="notif-banner-actions">' +
|
|
405
|
-
'<button class="notif-banner-new-session notif-banner-update-now">Open new session</button>' +
|
|
406
|
-
'</div>' +
|
|
407
|
-
'</div>' +
|
|
408
|
-
'<button class="notif-banner-close">' + iconHtml("x") + '</button>';
|
|
409
|
-
|
|
410
|
-
bannerContainer.appendChild(banner);
|
|
411
|
-
refreshIcons();
|
|
412
|
-
|
|
413
|
-
requestAnimationFrame(function () {
|
|
414
|
-
banner.classList.add("show");
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
var newSessionBtn = banner.querySelector(".notif-banner-new-session");
|
|
418
|
-
if (newSessionBtn) {
|
|
419
|
-
newSessionBtn.addEventListener("click", function (e) {
|
|
420
|
-
e.stopPropagation();
|
|
421
|
-
authReminderVisible = false;
|
|
422
|
-
var ws = getWs();
|
|
423
|
-
if (ws && ws.readyState === 1) {
|
|
424
|
-
ws.send(JSON.stringify({ type: "new_session" }));
|
|
425
|
-
}
|
|
426
|
-
removeBanner(banner);
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
var closeBtn = banner.querySelector(".notif-banner-close");
|
|
431
|
-
if (closeBtn) {
|
|
432
|
-
closeBtn.addEventListener("click", function (e) {
|
|
433
|
-
e.stopPropagation();
|
|
434
|
-
authReminderVisible = false;
|
|
435
|
-
removeBanner(banner);
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
327
|
export function showAuthRequiredBanner(msg) {
|
|
441
328
|
if (!bannerContainer) return;
|
|
442
329
|
var vendor = (msg && (msg.vendor || (msg.meta && msg.meta.vendor))) || "claude";
|
|
@@ -1,55 +1,149 @@
|
|
|
1
|
+
// background-tasks-ui.js - Background task status line above the composer.
|
|
2
|
+
//
|
|
3
|
+
// Collapsed it reads as a quiet status line, not a button demanding attention:
|
|
4
|
+
// a muted activity indicator, a count, and a chevron. Expanding lists each task
|
|
5
|
+
// with its elapsed time and a Stop control.
|
|
6
|
+
//
|
|
7
|
+
// Elapsed time comes from `started_at` (epoch ms) which the server stamps in
|
|
8
|
+
// lib/background-task-timing.js. The ticker only runs while the list is
|
|
9
|
+
// expanded, so a collapsed bar costs nothing.
|
|
10
|
+
|
|
1
11
|
import { store } from './store.js';
|
|
2
12
|
import { getWs } from './ws-ref.js';
|
|
3
13
|
import { escapeHtml } from './utils.js';
|
|
14
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
4
15
|
|
|
5
16
|
var expanded = false;
|
|
17
|
+
var elapsedTimer = null;
|
|
18
|
+
|
|
19
|
+
// Lucide icon per vendor-neutral task type (see normalizeBackgroundTasks in
|
|
20
|
+
// yoke/adapters/claude.js for where these types are assigned).
|
|
21
|
+
var TASK_TYPE_ICONS = { shell: "terminal", agent: "sparkles", other: "circle-dot" };
|
|
6
22
|
|
|
7
23
|
export function initBackgroundTasksUi() {
|
|
8
|
-
store.subscribe(function(state, prev) {
|
|
24
|
+
store.subscribe(function (state, prev) {
|
|
9
25
|
if (state.activeBackgroundTasks !== prev.activeBackgroundTasks) renderBackgroundTasks();
|
|
10
26
|
});
|
|
11
27
|
renderBackgroundTasks();
|
|
12
28
|
}
|
|
13
29
|
|
|
30
|
+
// "45s", "2m 14s", "1h 03m" - short enough to sit in a row without wrapping.
|
|
31
|
+
export function formatElapsed(startedAt, now) {
|
|
32
|
+
if (typeof startedAt !== "number" || !isFinite(startedAt) || startedAt <= 0) return "";
|
|
33
|
+
var current = typeof now === "number" ? now : Date.now();
|
|
34
|
+
var totalSeconds = Math.floor((current - startedAt) / 1000);
|
|
35
|
+
if (totalSeconds < 0) totalSeconds = 0;
|
|
36
|
+
if (totalSeconds < 60) return totalSeconds + "s";
|
|
37
|
+
var minutes = Math.floor(totalSeconds / 60);
|
|
38
|
+
var seconds = totalSeconds % 60;
|
|
39
|
+
if (minutes < 60) return minutes + "m " + pad(seconds) + "s";
|
|
40
|
+
var hours = Math.floor(minutes / 60);
|
|
41
|
+
return hours + "h " + pad(minutes % 60) + "m";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pad(value) {
|
|
45
|
+
return value < 10 ? "0" + value : String(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stopElapsedTimer() {
|
|
49
|
+
if (!elapsedTimer) return;
|
|
50
|
+
clearInterval(elapsedTimer);
|
|
51
|
+
elapsedTimer = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Ticks at most once a second and only touches the time cells, so expanding
|
|
55
|
+
// the list never re-renders (and never steals focus from) the rows.
|
|
56
|
+
function startElapsedTimer() {
|
|
57
|
+
stopElapsedTimer();
|
|
58
|
+
elapsedTimer = setInterval(updateElapsedCells, 1000);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function updateElapsedCells() {
|
|
62
|
+
var bar = document.getElementById('background-tasks-bar');
|
|
63
|
+
if (!bar || !expanded) {
|
|
64
|
+
stopElapsedTimer();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
var now = Date.now();
|
|
68
|
+
var cells = bar.querySelectorAll('.background-task-elapsed');
|
|
69
|
+
for (var i = 0; i < cells.length; i++) {
|
|
70
|
+
var startedAt = Number(cells[i].dataset.startedAt);
|
|
71
|
+
cells[i].textContent = formatElapsed(startedAt, now);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function activityDotsHtml() {
|
|
76
|
+
return '<span class="background-tasks-activity" aria-hidden="true"><i></i><i></i><i></i></span>';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function taskRowHtml(task, now) {
|
|
80
|
+
var taskType = task.task_type || 'other';
|
|
81
|
+
var icon = TASK_TYPE_ICONS[taskType] || TASK_TYPE_ICONS.other;
|
|
82
|
+
var description = task.description || 'Background task';
|
|
83
|
+
var startedAt = typeof task.started_at === 'number' ? task.started_at : 0;
|
|
84
|
+
var elapsed = formatElapsed(startedAt, now);
|
|
85
|
+
return '<div class="background-task-row">' +
|
|
86
|
+
'<span class="background-task-icon" title="' + escapeHtml(taskType) + '">' + iconHtml(icon) + '</span>' +
|
|
87
|
+
'<span class="background-task-description" title="' + escapeHtml(description) + '">' +
|
|
88
|
+
escapeHtml(description) + '</span>' +
|
|
89
|
+
'<span class="background-task-meta">' +
|
|
90
|
+
'<span class="background-task-type">' + escapeHtml(taskType) + '</span>' +
|
|
91
|
+
'<span class="background-task-elapsed" data-started-at="' + startedAt + '">' + escapeHtml(elapsed) + '</span>' +
|
|
92
|
+
'</span>' +
|
|
93
|
+
'<button type="button" class="background-task-stop" data-task-id="' + escapeHtml(task.task_id || '') + '" ' +
|
|
94
|
+
'aria-label="Stop ' + escapeHtml(description) + '">Stop</button>' +
|
|
95
|
+
'</div>';
|
|
96
|
+
}
|
|
97
|
+
|
|
14
98
|
function renderBackgroundTasks() {
|
|
15
99
|
var tasks = store.get('activeBackgroundTasks') || [];
|
|
16
100
|
var existing = document.getElementById('background-tasks-bar');
|
|
17
101
|
if (tasks.length === 0) {
|
|
18
102
|
if (existing) existing.remove();
|
|
19
103
|
expanded = false;
|
|
104
|
+
stopElapsedTimer();
|
|
20
105
|
return;
|
|
21
106
|
}
|
|
22
107
|
var inputArea = document.getElementById('input-area');
|
|
23
108
|
if (!inputArea || !inputArea.parentNode) return;
|
|
109
|
+
|
|
24
110
|
var bar = existing || document.createElement('div');
|
|
25
111
|
bar.id = 'background-tasks-bar';
|
|
26
|
-
bar.className = 'background-tasks-bar';
|
|
112
|
+
bar.className = 'background-tasks-bar' + (expanded ? ' expanded' : '');
|
|
113
|
+
|
|
27
114
|
var taskLabel = tasks.length === 1 ? 'background task' : 'background tasks';
|
|
28
|
-
var
|
|
29
|
-
|
|
30
|
-
'
|
|
115
|
+
var summary = tasks.length + ' ' + taskLabel;
|
|
116
|
+
var html = '<button type="button" class="background-tasks-toggle" aria-expanded="' + expanded + '"' +
|
|
117
|
+
' aria-controls="background-tasks-list" aria-label="' + summary + ', ' +
|
|
118
|
+
(expanded ? 'collapse' : 'expand') + ' details">' +
|
|
119
|
+
activityDotsHtml() +
|
|
120
|
+
'<span class="background-tasks-summary">' + summary + '</span>' +
|
|
121
|
+
'<span class="background-tasks-chevron" aria-hidden="true">' + iconHtml('chevron-right') + '</span>' +
|
|
122
|
+
'</button>';
|
|
123
|
+
|
|
31
124
|
if (expanded) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
html += '<div class="background-task-row"><span class="background-task-description" title="' + escapeHtml(task.description || '') + '">' +
|
|
36
|
-
escapeHtml(task.description || 'Background task') + '</span><span class="background-task-type">' +
|
|
37
|
-
escapeHtml(task.task_type || 'other') + '</span><button type="button" class="background-task-stop" data-task-id="' +
|
|
38
|
-
escapeHtml(task.task_id || '') + '">Stop</button></div>';
|
|
39
|
-
}
|
|
125
|
+
var now = Date.now();
|
|
126
|
+
html += '<div class="background-tasks-list" id="background-tasks-list">';
|
|
127
|
+
for (var i = 0; i < tasks.length; i++) html += taskRowHtml(tasks[i], now);
|
|
40
128
|
html += '</div>';
|
|
41
129
|
}
|
|
42
130
|
bar.innerHTML = html;
|
|
43
131
|
if (!existing) inputArea.parentNode.insertBefore(bar, inputArea);
|
|
44
|
-
|
|
132
|
+
refreshIcons();
|
|
133
|
+
|
|
134
|
+
bar.querySelector('.background-tasks-toggle').addEventListener('click', function () {
|
|
45
135
|
expanded = !expanded;
|
|
46
136
|
renderBackgroundTasks();
|
|
47
137
|
});
|
|
138
|
+
|
|
48
139
|
var stopButtons = bar.querySelectorAll('.background-task-stop');
|
|
49
140
|
for (var j = 0; j < stopButtons.length; j++) {
|
|
50
|
-
stopButtons[j].addEventListener('click', function() {
|
|
141
|
+
stopButtons[j].addEventListener('click', function () {
|
|
51
142
|
var ws = getWs();
|
|
52
143
|
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'stop_task', taskId: this.dataset.taskId }));
|
|
53
144
|
});
|
|
54
145
|
}
|
|
146
|
+
|
|
147
|
+
if (expanded) startElapsedTimer();
|
|
148
|
+
else stopElapsedTimer();
|
|
55
149
|
}
|
|
@@ -23,6 +23,15 @@ export function reportPaneContext(data) {
|
|
|
23
23
|
}, window.location.origin);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// A pane has no banner surface and no login modal of its own, so both panes
|
|
27
|
+
// hitting auth_required would otherwise race to start their own login flow.
|
|
28
|
+
// Hand the event to the parent shell, which owns the single flow.
|
|
29
|
+
export function forwardPaneAuthRequired(message) {
|
|
30
|
+
if (!store.get('paneMode') || window.parent === window) return false;
|
|
31
|
+
window.parent.postMessage({ type: "clay-pane-auth-required", message: message }, window.location.origin);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
export function forwardPaneMarkdownPresentation(message) {
|
|
27
36
|
if (!store.get('paneMode') || window.parent === window) return false;
|
|
28
37
|
window.parent.postMessage({ type: "clay-pane-present-markdown", message: message }, window.location.origin);
|
|
@@ -11,6 +11,7 @@ import { groupedSessionIds, findSplitGroup } from './split-group-helpers.js';
|
|
|
11
11
|
import { showConfirm } from './app-misc.js';
|
|
12
12
|
import { syncPairChrome } from './split-pair-ui.js';
|
|
13
13
|
import { presentMarkdownEdit } from './filebrowser.js';
|
|
14
|
+
import { autoStartLoginIfNeeded } from './vendor-login.js';
|
|
14
15
|
|
|
15
16
|
var host = null;
|
|
16
17
|
var nativeApp = null;
|
|
@@ -276,6 +277,12 @@ function handlePaneMessage(event) {
|
|
|
276
277
|
if (present) presentMarkdownEdit(present);
|
|
277
278
|
return;
|
|
278
279
|
}
|
|
280
|
+
// Both panes can report auth_required for their own sessions. The shell runs
|
|
281
|
+
// one login flow for the project; repeat events are no-ops there.
|
|
282
|
+
if (msg.type === "clay-pane-auth-required") {
|
|
283
|
+
if (msg.message) autoStartLoginIfNeeded(msg.message);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
279
286
|
if (msg.type !== "clay-pane-context") return;
|
|
280
287
|
var frames = host.querySelectorAll(".split-pane-frame");
|
|
281
288
|
for (var i = 0; i < frames.length; i++) {
|
|
@@ -29,6 +29,10 @@ var modalWs = null;
|
|
|
29
29
|
var modalResizeObserver = null;
|
|
30
30
|
var modalKeyHandler = null;
|
|
31
31
|
var modalResizeDebounce = null;
|
|
32
|
+
// Optional per-open callback fired once the modal is torn down, whatever
|
|
33
|
+
// dismissed it (Esc, backdrop click, term_closed). The login flow uses it to
|
|
34
|
+
// cancel a login the user walked away from.
|
|
35
|
+
var modalCloseHook = null;
|
|
32
36
|
// Debounced fit+redraw for the modal xterm. Same rationale as
|
|
33
37
|
// session-tui-view.js: collapses rapid resize events into a single
|
|
34
38
|
// SIGWINCH so claude can redraw cleanly without mid-resize corruption.
|
|
@@ -139,6 +143,7 @@ export function openTuiModal(terminalId, sourceSlug, info) {
|
|
|
139
143
|
modalTerminalId = terminalId;
|
|
140
144
|
modalSourceSlug = sourceSlug;
|
|
141
145
|
var infoObj = info || {};
|
|
146
|
+
modalCloseHook = typeof infoObj.onClose === "function" ? infoObj.onClose : null;
|
|
142
147
|
setModalBreadcrumb({
|
|
143
148
|
projectIcon: infoObj.projectIcon || null,
|
|
144
149
|
projectName: infoObj.projectName || sourceSlug,
|
|
@@ -232,6 +237,8 @@ export function openTuiModal(terminalId, sourceSlug, info) {
|
|
|
232
237
|
|
|
233
238
|
export function closeTuiModal() {
|
|
234
239
|
if (!modalEl) return;
|
|
240
|
+
var closeHook = modalCloseHook;
|
|
241
|
+
modalCloseHook = null;
|
|
235
242
|
if (modalTerminalId != null && modalWs && modalWs.readyState === 1) {
|
|
236
243
|
try { modalWs.send(JSON.stringify({ type: "term_detach", id: modalTerminalId })); } catch (e) {}
|
|
237
244
|
}
|
|
@@ -247,6 +254,9 @@ export function closeTuiModal() {
|
|
|
247
254
|
document.removeEventListener("keydown", modalKeyHandler);
|
|
248
255
|
modalKeyHandler = null;
|
|
249
256
|
}
|
|
257
|
+
if (closeHook) {
|
|
258
|
+
try { closeHook(); } catch (e) {}
|
|
259
|
+
}
|
|
250
260
|
}
|
|
251
261
|
|
|
252
262
|
export function isTuiModalOpen() {
|