clay-server 2.40.1-beta.1 → 2.41.0-beta.1
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 +1 -1
- package/lib/project-sessions.js +38 -0
- package/lib/public/index.html +3 -0
- package/lib/public/modules/app-messages.js +8 -1
- package/lib/public/modules/app-notifications.js +91 -2
- package/lib/public/modules/header-tui-font.js +1 -0
- package/lib/public/modules/session-tui-view.js +10 -1
- package/lib/public/modules/sidebar-projects.js +11 -3
- package/lib/public/modules/terminal-prefs.js +51 -4
- package/lib/public/modules/tui-grab.js +723 -0
- package/lib/server-global-ws.js +189 -0
- package/lib/server-settings.js +2 -2
- package/lib/server.js +40 -7
- package/lib/tui-transcript-index.js +125 -0
- package/lib/users-preferences.js +1 -1
- package/lib/ws-schema.js +2 -0
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -730,7 +730,7 @@ var relay = createServer({
|
|
|
730
730
|
},
|
|
731
731
|
onSetTerminalFont: function (family, size) {
|
|
732
732
|
var DEFAULT_FAMILY = "'SF Mono', Menlo, Monaco, 'Courier New', monospace";
|
|
733
|
-
var current = config.terminalFont || { family: DEFAULT_FAMILY, size:
|
|
733
|
+
var current = config.terminalFont || { family: DEFAULT_FAMILY, size: 14 };
|
|
734
734
|
var nextFamily = (typeof family === "string" && family.trim()) ? family.trim().slice(0, 200) : current.family;
|
|
735
735
|
var nextSize = current.size;
|
|
736
736
|
if (typeof size === "number" && size >= 9 && size <= 32) {
|
package/lib/project-sessions.js
CHANGED
|
@@ -176,6 +176,19 @@ function attachSessions(ctx) {
|
|
|
176
176
|
cliSessionId: s.cliSessionId || null,
|
|
177
177
|
});
|
|
178
178
|
} catch (e) {}
|
|
179
|
+
// Re-read the assistant text index and broadcast it so any client
|
|
180
|
+
// with this session in view can wire hover-to-grab onto the new
|
|
181
|
+
// message right away. The transcript is small enough that a full
|
|
182
|
+
// re-send beats maintaining a delta protocol.
|
|
183
|
+
try {
|
|
184
|
+
var newIndex = require("./tui-transcript-index").readAssistantIndex(cwd, s.cliSessionId);
|
|
185
|
+
send({
|
|
186
|
+
type: "tui_transcript_state",
|
|
187
|
+
id: s.localId,
|
|
188
|
+
cliSessionId: s.cliSessionId,
|
|
189
|
+
messages: newIndex.messages,
|
|
190
|
+
});
|
|
191
|
+
} catch (e) {}
|
|
179
192
|
},
|
|
180
193
|
});
|
|
181
194
|
session._titleWatcherStop = stop;
|
|
@@ -680,6 +693,31 @@ function attachSessions(ctx) {
|
|
|
680
693
|
return true;
|
|
681
694
|
}
|
|
682
695
|
|
|
696
|
+
// Client asks for the assistant text index of a Claude TUI session so
|
|
697
|
+
// it can wire hover-to-grab on the rendered terminal output. Only the
|
|
698
|
+
// raw markdown of assistant text messages is returned — no tool calls,
|
|
699
|
+
// no user prompts. Codex sessions skip the feature.
|
|
700
|
+
if (msg.type === "tui_transcript_request") {
|
|
701
|
+
var tprId = msg.id;
|
|
702
|
+
var tprSess = (tprId && sm.sessions.has(tprId)) ? sm.sessions.get(tprId) : null;
|
|
703
|
+
if (!tprSess || !tprSess.cliSessionId || tprSess.mode !== "tui") return true;
|
|
704
|
+
if (tprSess.vendor && tprSess.vendor !== "claude") return true;
|
|
705
|
+
if (usersModule.isMultiUser() && ws._clayUser
|
|
706
|
+
&& !usersModule.canAccessSession(ws._clayUser.id, tprSess, { visibility: "public" })) {
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
var tprIndex = require("./tui-transcript-index").readAssistantIndex(cwd, tprSess.cliSessionId);
|
|
711
|
+
sendTo(ws, {
|
|
712
|
+
type: "tui_transcript_state",
|
|
713
|
+
id: tprId,
|
|
714
|
+
cliSessionId: tprSess.cliSessionId,
|
|
715
|
+
messages: tprIndex.messages,
|
|
716
|
+
});
|
|
717
|
+
} catch (e) {}
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
|
|
683
721
|
if (msg.type === "set_mate_dm") {
|
|
684
722
|
// Only store mateDm on non-mate projects (main project presence).
|
|
685
723
|
// Mate projects should never hold mateDm to avoid circular restore loops.
|
package/lib/public/index.html
CHANGED
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
SF Mono is Apple-system-only (no Google Fonts entry) and
|
|
23
23
|
ui-monospace is a system keyword - both rely on the user's OS. -->
|
|
24
24
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Fira+Code:wght@400;500;700&family=Cascadia+Code:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500;700&family=Source+Code+Pro:wght@400;500;700&display=swap" rel="stylesheet">
|
|
25
|
+
<!-- D2 Coding isn't on Google Fonts and its CJK woff2 is ~1.4MB, so it
|
|
26
|
+
is lazy-loaded by terminal-prefs.js the first time the user selects
|
|
27
|
+
it (or boots with it already saved). See ensureD2CodingWebfont(). -->
|
|
25
28
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5/css/xterm.min.css">
|
|
26
29
|
<script>
|
|
27
30
|
(function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
|
|
@@ -28,6 +28,7 @@ import { isProjectSettingsOpen, handleInstructionsRead, handleInstructionsWrite,
|
|
|
28
28
|
import { updateSettingsModels, updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './server-settings.js';
|
|
29
29
|
import { handleTermList, handleTermCreated, sendTerminalCommand, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed } from './terminal.js';
|
|
30
30
|
import { attachTuiView, detachTuiView, setTuiSuspendedView, tuiHandleTermOutput, tuiHandleTermResized, tuiHandleTermExited, tuiHandleTermClosed } from './session-tui-view.js';
|
|
31
|
+
import { handleTuiTranscriptState } from './tui-grab.js';
|
|
31
32
|
import { tuiModalHandleTermOutput, tuiModalHandleTermResized, tuiModalHandleTermExited, tuiModalHandleTermClosed } from './tui-attention.js';
|
|
32
33
|
import { updateTerminalList, handleContextSourcesState, updateEmailAccountList, updateEmailUnreadCounts, handleEmailTestResult, handleEmailAddResult, handleEmailRemoveResult, handleEmailDefaults } from './context-sources.js';
|
|
33
34
|
import { refreshEmailSettings } from './user-settings.js';
|
|
@@ -421,6 +422,12 @@ export function processMessage(msg) {
|
|
|
421
422
|
}) });
|
|
422
423
|
break;
|
|
423
424
|
|
|
425
|
+
case "tui_transcript_state":
|
|
426
|
+
// Assistant text index for a Claude TUI session — drives the
|
|
427
|
+
// hover-to-grab overlay in lib/public/modules/tui-grab.js.
|
|
428
|
+
handleTuiTranscriptState(msg);
|
|
429
|
+
break;
|
|
430
|
+
|
|
424
431
|
case "model_info": {
|
|
425
432
|
// Drop stale model_info from a vendor that doesn't match the active
|
|
426
433
|
// session's vendor. On high-latency connections, the server's default-
|
|
@@ -616,7 +623,7 @@ export function processMessage(msg) {
|
|
|
616
623
|
// `claude` inside a real PTY. Mount or tear down before the rest of
|
|
617
624
|
// the chat-side bookkeeping runs so we don't waste work on hidden DOM.
|
|
618
625
|
if (_effectiveMode === "tui" && typeof _effectiveTerminalId === "number") {
|
|
619
|
-
attachTuiView(_effectiveTerminalId);
|
|
626
|
+
attachTuiView(_effectiveTerminalId, msg.id, msg.vendor || null);
|
|
620
627
|
} else {
|
|
621
628
|
detachTuiView();
|
|
622
629
|
}
|
|
@@ -7,17 +7,31 @@ import { escapeHtml } from './utils.js';
|
|
|
7
7
|
import { store } from './store.js';
|
|
8
8
|
import { getWs } from './ws-ref.js';
|
|
9
9
|
import { openDm } from './app-dm.js';
|
|
10
|
-
import { getCachedProjects } from './app-projects.js';
|
|
11
|
-
import { switchProject } from './app-projects.js';
|
|
10
|
+
import { getCachedProjects, switchProject, renderProjectList } from './app-projects.js';
|
|
12
11
|
import { mateAvatarUrl, userAvatarUrl } from './avatar.js';
|
|
13
12
|
import { openTerminal } from './terminal.js';
|
|
14
13
|
import { openTuiModal } from './tui-attention.js';
|
|
14
|
+
import { startUrgentBlink, stopUrgentBlink } from './app-favicon.js';
|
|
15
|
+
import { playDoneSound, isNotifSoundEnabled } from './notifications.js';
|
|
15
16
|
var notifications = [];
|
|
16
17
|
var unreadCount = 0;
|
|
17
18
|
var bannerContainer = null;
|
|
18
19
|
var bellBtn = null;
|
|
19
20
|
var badgeEl = null;
|
|
20
21
|
|
|
22
|
+
// --- Pending TUI attention tracking ---
|
|
23
|
+
// Mirrors the icon-shake / favicon-blink behavior the SDK side already gets
|
|
24
|
+
// from `pendingPermissions` on project status broadcasts. The notification
|
|
25
|
+
// center is our source of truth for unresolved tui_attention notifications:
|
|
26
|
+
// when one arrives we bump the slug's count and start the urgent favicon
|
|
27
|
+
// blink; when it's dismissed (the user opened the session, clicked the
|
|
28
|
+
// banner, or closed it) we decrement and stop the blink once nothing is
|
|
29
|
+
// left pending. `notif.id -> slug` lets us decrement without having to
|
|
30
|
+
// re-derive the slug from a dismissed notification.
|
|
31
|
+
var pendingTuiBySlug = Object.create(null);
|
|
32
|
+
var pendingTuiBySlugId = Object.create(null);
|
|
33
|
+
var pendingTuiTotal = 0;
|
|
34
|
+
|
|
21
35
|
// --- Update available banner state ---
|
|
22
36
|
// Server pushes update_available on an hourly boundary; dismissal is
|
|
23
37
|
// per-banner-instance and doesn't need to persist. The next server push
|
|
@@ -457,11 +471,61 @@ function dismissNotif(id) {
|
|
|
457
471
|
// Message handlers
|
|
458
472
|
// ========================================================
|
|
459
473
|
|
|
474
|
+
function isPendingTuiAttention(notif) {
|
|
475
|
+
return notif && notif.type === "tui_attention" && notif.slug;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function bumpTuiAttention(notif) {
|
|
479
|
+
if (!isPendingTuiAttention(notif)) return false;
|
|
480
|
+
if (pendingTuiBySlugId[notif.id]) return false;
|
|
481
|
+
pendingTuiBySlugId[notif.id] = notif.slug;
|
|
482
|
+
pendingTuiBySlug[notif.slug] = (pendingTuiBySlug[notif.slug] || 0) + 1;
|
|
483
|
+
pendingTuiTotal++;
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function dropTuiAttentionById(id) {
|
|
488
|
+
var slug = pendingTuiBySlugId[id];
|
|
489
|
+
if (!slug) return false;
|
|
490
|
+
delete pendingTuiBySlugId[id];
|
|
491
|
+
pendingTuiBySlug[slug] = (pendingTuiBySlug[slug] || 1) - 1;
|
|
492
|
+
if (pendingTuiBySlug[slug] <= 0) delete pendingTuiBySlug[slug];
|
|
493
|
+
pendingTuiTotal--;
|
|
494
|
+
if (pendingTuiTotal < 0) pendingTuiTotal = 0;
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function refreshTuiAttentionVisuals(prevTotal) {
|
|
499
|
+
if (pendingTuiTotal > 0 && prevTotal === 0) {
|
|
500
|
+
try { startUrgentBlink(); } catch (e) {}
|
|
501
|
+
} else if (pendingTuiTotal === 0 && prevTotal > 0) {
|
|
502
|
+
try { stopUrgentBlink(); } catch (e) {}
|
|
503
|
+
}
|
|
504
|
+
try { renderProjectList(); } catch (e) {}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function getPendingTuiAttention(slug) {
|
|
508
|
+
if (!slug) return 0;
|
|
509
|
+
return pendingTuiBySlug[slug] || 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
460
512
|
export function handleNotificationsState(msg) {
|
|
461
513
|
notifications = msg.notifications || [];
|
|
462
514
|
unreadCount = msg.unreadCount || 0;
|
|
463
515
|
updateBadge();
|
|
464
516
|
|
|
517
|
+
// Rebuild the pending-tui-attention map from the authoritative list. This
|
|
518
|
+
// also covers reconnect — pre-existing notifications get the icon shake
|
|
519
|
+
// and favicon blink restored without waiting for a fresh server event.
|
|
520
|
+
var prevTotal = pendingTuiTotal;
|
|
521
|
+
pendingTuiBySlug = Object.create(null);
|
|
522
|
+
pendingTuiBySlugId = Object.create(null);
|
|
523
|
+
pendingTuiTotal = 0;
|
|
524
|
+
for (var i = 0; i < notifications.length; i++) {
|
|
525
|
+
bumpTuiAttention(notifications[i]);
|
|
526
|
+
}
|
|
527
|
+
refreshTuiAttentionVisuals(prevTotal);
|
|
528
|
+
|
|
465
529
|
// Check for pending session navigation after project switch
|
|
466
530
|
try {
|
|
467
531
|
var pendingSession = sessionStorage.getItem("pending-notif-session");
|
|
@@ -490,6 +554,20 @@ export function handleNotificationCreated(msg) {
|
|
|
490
554
|
unreadCount = msg.unreadCount;
|
|
491
555
|
updateBadge();
|
|
492
556
|
|
|
557
|
+
// tui_attention drives the project icon shake and the favicon blink —
|
|
558
|
+
// same visual cues the SDK already produces from pendingPermissions /
|
|
559
|
+
// permission_request messages, so the user notices either kind without
|
|
560
|
+
// having the right tab focused. Ring the ding too, gated on document.hidden
|
|
561
|
+
// and the notif-sound toggle to match the SDK 'done' path — we don't want
|
|
562
|
+
// to chime while the user is actively watching the tab.
|
|
563
|
+
var prevTotal = pendingTuiTotal;
|
|
564
|
+
if (bumpTuiAttention(notif)) {
|
|
565
|
+
refreshTuiAttentionVisuals(prevTotal);
|
|
566
|
+
if (document.hidden && isNotifSoundEnabled()) {
|
|
567
|
+
try { playDoneSound(); } catch (e) {}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
493
571
|
// user_mention banners are persistent (no auto-dismiss). They go away when:
|
|
494
572
|
// 1. The user clicks the banner (which navigates to the session — see
|
|
495
573
|
// banner click handler in showBanner / navigateToNotification), OR
|
|
@@ -516,6 +594,12 @@ export function handleNotificationDismissed(msg) {
|
|
|
516
594
|
if (el) removeBanner(el);
|
|
517
595
|
}
|
|
518
596
|
}
|
|
597
|
+
var prevTotal = pendingTuiTotal;
|
|
598
|
+
var anyTuiDropped = false;
|
|
599
|
+
for (var j = 0; j < ids.length; j++) {
|
|
600
|
+
if (dropTuiAttentionById(ids[j])) anyTuiDropped = true;
|
|
601
|
+
}
|
|
602
|
+
if (anyTuiDropped) refreshTuiAttentionVisuals(prevTotal);
|
|
519
603
|
}
|
|
520
604
|
|
|
521
605
|
export function handleNotificationDismissedAll() {
|
|
@@ -523,6 +607,11 @@ export function handleNotificationDismissedAll() {
|
|
|
523
607
|
unreadCount = 0;
|
|
524
608
|
updateBadge();
|
|
525
609
|
if (bannerContainer) bannerContainer.innerHTML = "";
|
|
610
|
+
var prevTotal = pendingTuiTotal;
|
|
611
|
+
pendingTuiBySlug = Object.create(null);
|
|
612
|
+
pendingTuiBySlugId = Object.create(null);
|
|
613
|
+
pendingTuiTotal = 0;
|
|
614
|
+
if (prevTotal > 0) refreshTuiAttentionVisuals(prevTotal);
|
|
526
615
|
}
|
|
527
616
|
|
|
528
617
|
// ========================================================
|
|
@@ -21,6 +21,7 @@ var FONT_OPTIONS = [
|
|
|
21
21
|
{ label: "IBM Plex Mono", family: "'IBM Plex Mono', 'SF Mono', Menlo, monospace" },
|
|
22
22
|
{ label: "Source Code Pro", family: "'Source Code Pro', 'SF Mono', Menlo, monospace" },
|
|
23
23
|
{ label: "Roboto Mono", family: "'Roboto Mono', 'SF Mono', Menlo, monospace" },
|
|
24
|
+
{ label: "D2 Coding", family: "'D2 coding', 'D2Coding', 'SF Mono', Menlo, monospace" },
|
|
24
25
|
{ label: "System mono", family: "ui-monospace, monospace" },
|
|
25
26
|
];
|
|
26
27
|
|
|
@@ -19,6 +19,7 @@ import { store } from './store.js';
|
|
|
19
19
|
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
|
+
import { attachTuiGrab, detachTuiGrab } from './tui-grab.js';
|
|
22
23
|
import { openArticle as openWhatsNewArticle } from './whats-new-article.js';
|
|
23
24
|
import { createKeyToolbar, TERMINAL_TOOLBAR_HTML } from './terminal-toolbar.js';
|
|
24
25
|
import { refreshIcons, iconHtml } from './icons.js';
|
|
@@ -593,7 +594,7 @@ function teardownXterm() {
|
|
|
593
594
|
imeLastComposedAt = 0;
|
|
594
595
|
}
|
|
595
596
|
|
|
596
|
-
export function attachTuiView(terminalId) {
|
|
597
|
+
export function attachTuiView(terminalId, localId, vendor) {
|
|
597
598
|
if (typeof terminalId !== "number") return;
|
|
598
599
|
// Re-attaching to the same terminal: just refit and refocus.
|
|
599
600
|
if (currentTermId === terminalId && xterm) {
|
|
@@ -603,6 +604,9 @@ export function attachTuiView(terminalId) {
|
|
|
603
604
|
showHeaderTuiClose();
|
|
604
605
|
fitNow();
|
|
605
606
|
focusGuiComposer();
|
|
607
|
+
// Re-arm hover-grab against the same xterm in case the session
|
|
608
|
+
// metadata (localId / vendor) changed since the last attach.
|
|
609
|
+
if (localId) attachTuiGrab(xterm, xtermContainerEl, localId, { vendor: vendor });
|
|
606
610
|
return;
|
|
607
611
|
}
|
|
608
612
|
// Switching to a different TUI terminal: tear down the old one cleanly.
|
|
@@ -619,6 +623,10 @@ export function attachTuiView(terminalId) {
|
|
|
619
623
|
currentTermId = terminalId;
|
|
620
624
|
if (!xterm) xterm = createXterm();
|
|
621
625
|
if (!xterm) return;
|
|
626
|
+
// PC통신-style hover-and-click grab over the rendered output. Pulls
|
|
627
|
+
// its index from the on-disk JSONL transcript via tui_transcript_*
|
|
628
|
+
// messages; no-ops for Codex sessions.
|
|
629
|
+
if (localId) attachTuiGrab(xterm, xtermContainerEl, localId, { vendor: vendor });
|
|
622
630
|
|
|
623
631
|
// Subscribe to the terminal's output stream on the server. The server
|
|
624
632
|
// replays its scrollback buffer on attach so we never start blank.
|
|
@@ -653,6 +661,7 @@ export function detachTuiView() {
|
|
|
653
661
|
try { resizeObserver.disconnect(); } catch (e) {}
|
|
654
662
|
resizeObserver = null;
|
|
655
663
|
}
|
|
664
|
+
detachTuiGrab();
|
|
656
665
|
if (currentTermId != null) {
|
|
657
666
|
var ws = getWs();
|
|
658
667
|
if (ws && ws.readyState === 1) {
|
|
@@ -12,6 +12,7 @@ import { closeSidebar } from './sidebar.js';
|
|
|
12
12
|
import { showIconTooltip, hideIconTooltip, closeUserCtxMenu, getCurrentDmUserId } from './sidebar-mates.js';
|
|
13
13
|
import { switchProject, openAddProjectModal, getCachedProjects } from './app-projects.js';
|
|
14
14
|
import { showHomeHub } from './app-home-hub.js';
|
|
15
|
+
import { getPendingTuiAttention } from './app-notifications.js';
|
|
15
16
|
|
|
16
17
|
// --- Project state ---
|
|
17
18
|
var cachedProjectList = [];
|
|
@@ -1011,7 +1012,11 @@ function createIconItem(p, currentSlug) {
|
|
|
1011
1012
|
}
|
|
1012
1013
|
el.appendChild(projectBadge);
|
|
1013
1014
|
|
|
1014
|
-
|
|
1015
|
+
// TUI sessions don't go through pendingPermissions on the project status
|
|
1016
|
+
// broadcast — they surface via tui_attention notifications. We shake the
|
|
1017
|
+
// same icon for either, so the user notices regardless of which engine
|
|
1018
|
+
// is asking.
|
|
1019
|
+
if (!isActive && (p.pendingPermissions > 0 || getPendingTuiAttention(p.slug) > 0)) {
|
|
1015
1020
|
el.classList.add("has-pending-perm");
|
|
1016
1021
|
}
|
|
1017
1022
|
|
|
@@ -1312,7 +1317,7 @@ export function renderIconStrip(projects, currentSlug) {
|
|
|
1312
1317
|
});
|
|
1313
1318
|
}
|
|
1314
1319
|
|
|
1315
|
-
if (wt.pendingPermissions > 0
|
|
1320
|
+
if (!isWtActive && (wt.pendingPermissions > 0 || getPendingTuiAttention(wt.slug) > 0)) {
|
|
1316
1321
|
wtEl.classList.add("has-pending-perm");
|
|
1317
1322
|
}
|
|
1318
1323
|
|
|
@@ -1322,7 +1327,10 @@ export function renderIconStrip(projects, currentSlug) {
|
|
|
1322
1327
|
|
|
1323
1328
|
var hasWtPendingPerm = false;
|
|
1324
1329
|
for (var wpi2 = 0; wpi2 < worktrees.length; wpi2++) {
|
|
1325
|
-
if (worktrees[wpi2].pendingPermissions > 0
|
|
1330
|
+
if (worktrees[wpi2].pendingPermissions > 0 || getPendingTuiAttention(worktrees[wpi2].slug) > 0) {
|
|
1331
|
+
hasWtPendingPerm = true;
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1326
1334
|
}
|
|
1327
1335
|
if (hasWtPendingPerm) folder.classList.remove("collapsed");
|
|
1328
1336
|
|
|
@@ -10,12 +10,60 @@
|
|
|
10
10
|
// without round-trips.
|
|
11
11
|
|
|
12
12
|
var DEFAULT_FAMILY = "'SF Mono', Menlo, Monaco, 'Courier New', monospace";
|
|
13
|
-
var DEFAULT_SIZE =
|
|
13
|
+
var DEFAULT_SIZE = 14;
|
|
14
14
|
|
|
15
15
|
var currentFamily = DEFAULT_FAMILY;
|
|
16
16
|
var currentSize = DEFAULT_SIZE;
|
|
17
17
|
var listeners = [];
|
|
18
18
|
|
|
19
|
+
// D2 Coding ships ~1.4MB of CJK glyphs per weight, so we avoid loading
|
|
20
|
+
// it eagerly on every page. The link is injected on demand the first
|
|
21
|
+
// time the user actually picks the font (or boots with it saved). The
|
|
22
|
+
// @font-face in this CSS registers under the family 'D2 coding' and
|
|
23
|
+
// also picks up a locally-installed 'D2Coding' via local() fallback.
|
|
24
|
+
//
|
|
25
|
+
// xterm's WebGL renderer measures cell metrics synchronously when the
|
|
26
|
+
// font family option changes, so the first atlas build right after
|
|
27
|
+
// selection happens against the *fallback* (SF Mono for Latin, a system
|
|
28
|
+
// Hangul font for Korean). Each is measured with its own width, so
|
|
29
|
+
// Latin and Hangul cells fall out of the expected 1:2 ratio and lines
|
|
30
|
+
// stop lining up. After the woff2 actually arrives we replay the
|
|
31
|
+
// font-change notification so every listener rebuilds its atlas with
|
|
32
|
+
// the proper D2 Coding metrics for both scripts.
|
|
33
|
+
var d2CodingInjected = false;
|
|
34
|
+
function notifyFontListeners() {
|
|
35
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
36
|
+
try { listeners[i](currentFamily, currentSize); } catch (e) {}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function ensureD2CodingWebfont() {
|
|
40
|
+
if (d2CodingInjected) return;
|
|
41
|
+
d2CodingInjected = true;
|
|
42
|
+
if (typeof document === "undefined" || !document.head) return;
|
|
43
|
+
var link = document.createElement("link");
|
|
44
|
+
link.rel = "stylesheet";
|
|
45
|
+
link.href = "https://cdn.jsdelivr.net/gh/Joungkyun/font-d2coding/d2coding.css";
|
|
46
|
+
link.onload = function () {
|
|
47
|
+
if (document.fonts && typeof document.fonts.load === "function") {
|
|
48
|
+
Promise.all([
|
|
49
|
+
document.fonts.load('400 16px "D2 coding"'),
|
|
50
|
+
document.fonts.load('700 16px "D2 coding"'),
|
|
51
|
+
]).then(notifyFontListeners).catch(function () {});
|
|
52
|
+
} else {
|
|
53
|
+
notifyFontListeners();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
document.head.appendChild(link);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function maybeLazyLoadWebfont(family) {
|
|
60
|
+
if (typeof family !== "string") return;
|
|
61
|
+
var lower = family.toLowerCase();
|
|
62
|
+
if (lower.indexOf("d2 coding") !== -1 || lower.indexOf("d2coding") !== -1) {
|
|
63
|
+
ensureD2CodingWebfont();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
19
67
|
export function getTerminalFontFamily() {
|
|
20
68
|
return currentFamily || DEFAULT_FAMILY;
|
|
21
69
|
}
|
|
@@ -43,9 +91,8 @@ export function applyTerminalFont(family, size) {
|
|
|
43
91
|
changed = true;
|
|
44
92
|
}
|
|
45
93
|
if (!changed) return;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
94
|
+
maybeLazyLoadWebfont(currentFamily);
|
|
95
|
+
notifyFontListeners();
|
|
49
96
|
}
|
|
50
97
|
|
|
51
98
|
export function onTerminalFontChange(fn) {
|