claude-code-hub 1.36.0 → 1.37.0
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/package.json +2 -2
- package/public/app.js +105 -2
- package/public/index.html +85 -0
- package/server.js +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-hub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "Unified hub for Claude Code tools — Marketplace + Kanban in one PWA",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"homepage": "https://github.com/NikiforovAll/claude-code-hub#readme",
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"claude-code-cost": "1.6.2",
|
|
50
|
-
"claude-code-kanban": "7.
|
|
50
|
+
"claude-code-kanban": "7.34.0",
|
|
51
51
|
"claude-code-marketplace": "1.7.3",
|
|
52
52
|
"claude-code-memory-explorer": "1.3.6",
|
|
53
53
|
"express": "^4.21.0",
|
package/public/app.js
CHANGED
|
@@ -249,6 +249,7 @@ function onPublish(appId, { topic, payload }) {
|
|
|
249
249
|
? { ...payload, encoded: encodeProjectPath(payload.project), projectName: basename(payload.project) }
|
|
250
250
|
: payload;
|
|
251
251
|
published.set(topic, { from: appId, payload: event });
|
|
252
|
+
if (topic === 'attention.changed') onAttention(appId, event);
|
|
252
253
|
for (const id of Object.keys(iframes)) {
|
|
253
254
|
if (id === appId) continue;
|
|
254
255
|
if (id === activeApp) sendTopic(id, topic, event);
|
|
@@ -317,6 +318,7 @@ async function init() {
|
|
|
317
318
|
bindSetup();
|
|
318
319
|
registerSW();
|
|
319
320
|
bindErrorToast();
|
|
321
|
+
bindAttentionToast();
|
|
320
322
|
showUpdateToast();
|
|
321
323
|
if (!config.onboarding) maybeShowSetup();
|
|
322
324
|
reloadOnHubRestart();
|
|
@@ -1010,8 +1012,108 @@ function bindErrorToast() {
|
|
|
1010
1012
|
// The hub has no visible chrome, so the window/tab title is the only place the active config dir
|
|
1011
1013
|
// shows. Only a non-default dir is named, and by basename alone — the PWA window prepends the
|
|
1012
1014
|
// manifest name, so a product name here would read as "Claude Code Hub - .claude-eom · Hub".
|
|
1013
|
-
function applyTitle(dir) {
|
|
1014
|
-
|
|
1015
|
+
function applyTitle(dir = titleDir) {
|
|
1016
|
+
titleDir = dir;
|
|
1017
|
+
const name = dir && dir !== defaultConfigDir ? basename(dir) : 'Claude Code Hub';
|
|
1018
|
+
const n = attentionCount();
|
|
1019
|
+
document.title = n ? `(${n}) ${name}` : name;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// attention.changed lists the sessions whose terminal reports blocked. With no tab strip, the
|
|
1023
|
+
// count goes in the window title and the installed app's taskbar badge. A session that newly
|
|
1024
|
+
// blocks while its app is off screen gets a toast, at most once per ATTENTION_GAP_MS each,
|
|
1025
|
+
// and none from the first payload, so a reload does not toast what was already waiting. The
|
|
1026
|
+
// toast goes when its app comes on screen or the session stops waiting.
|
|
1027
|
+
const ATTENTION_KINDS = { permission: 'Permission', question: 'Question', auth: 'Sign-in' };
|
|
1028
|
+
const ATTENTION_ICONS = {
|
|
1029
|
+
permission: [['path', { d: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }]],
|
|
1030
|
+
question: [
|
|
1031
|
+
['circle', { cx: 12, cy: 12, r: 10 }],
|
|
1032
|
+
['path', { d: 'M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3' }],
|
|
1033
|
+
['path', { d: 'M12 17h.01' }],
|
|
1034
|
+
],
|
|
1035
|
+
auth: [
|
|
1036
|
+
['circle', { cx: 7.5, cy: 15.5, r: 5.5 }],
|
|
1037
|
+
['path', { d: 'm21 2-9.6 9.6' }],
|
|
1038
|
+
['path', { d: 'm15.5 7.5 3 3L22 7l-3-3' }],
|
|
1039
|
+
],
|
|
1040
|
+
other: [
|
|
1041
|
+
['path', { d: 'M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9' }],
|
|
1042
|
+
['path', { d: 'M10.3 21a1.94 1.94 0 0 0 3.4 0' }],
|
|
1043
|
+
],
|
|
1044
|
+
};
|
|
1045
|
+
const ATTENTION_TOAST_MS = 8000;
|
|
1046
|
+
const ATTENTION_GAP_MS = 30_000;
|
|
1047
|
+
let titleDir = null;
|
|
1048
|
+
const attention = new Map();
|
|
1049
|
+
const attentionToasted = new Map();
|
|
1050
|
+
let attentionTimer = 0;
|
|
1051
|
+
|
|
1052
|
+
const attentionCount = () => [...attention.values()].reduce((n, list) => n + list.length, 0);
|
|
1053
|
+
|
|
1054
|
+
function onAttention(appId, payload) {
|
|
1055
|
+
const list = Array.isArray(payload?.blocked) ? payload.blocked : [];
|
|
1056
|
+
const blocked = list
|
|
1057
|
+
.filter((b) => isPlainObject(b) && typeof b.id === 'string' && b.id.length <= 64 && typeof b.name === 'string')
|
|
1058
|
+
.slice(0, 20)
|
|
1059
|
+
.map((b) => ({
|
|
1060
|
+
id: b.id,
|
|
1061
|
+
name: b.name.slice(0, 120),
|
|
1062
|
+
kind: Object.hasOwn(ATTENTION_KINDS, b.kind) ? b.kind : null,
|
|
1063
|
+
}));
|
|
1064
|
+
const seeded = attention.has(appId);
|
|
1065
|
+
const before = new Set(attention.get(appId)?.map((b) => b.id));
|
|
1066
|
+
attention.set(appId, blocked);
|
|
1067
|
+
applyTitle();
|
|
1068
|
+
const total = attentionCount();
|
|
1069
|
+
if (total) navigator.setAppBadge?.(total).catch(() => {});
|
|
1070
|
+
else navigator.clearAppBadge?.().catch(() => {});
|
|
1071
|
+
const now = Date.now();
|
|
1072
|
+
for (const [id, at] of attentionToasted) if (now - at > ATTENTION_GAP_MS) attentionToasted.delete(id);
|
|
1073
|
+
const fresh = blocked.find((b) => !before.has(b.id) && !attentionToasted.has(b.id));
|
|
1074
|
+
const toast = document.getElementById('attention-toast');
|
|
1075
|
+
if (toast.dataset.app === appId && !blocked.some((b) => b.id === toast.dataset.session)) hideAttentionToast();
|
|
1076
|
+
if (!seeded || !fresh || appId === activeApp) return;
|
|
1077
|
+
attentionToasted.set(fresh.id, now);
|
|
1078
|
+
showAttentionToast(appId, fresh, blocked.length - 1);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function hideAttentionToast() {
|
|
1082
|
+
clearTimeout(attentionTimer);
|
|
1083
|
+
const toast = document.getElementById('attention-toast');
|
|
1084
|
+
if (!toast.hidden) hideToast(toast);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function showAttentionToast(appId, b, others) {
|
|
1088
|
+
const toast = document.getElementById('attention-toast');
|
|
1089
|
+
document
|
|
1090
|
+
.getElementById('attention-icon')
|
|
1091
|
+
.replaceChildren(lucideSvg(ATTENTION_ICONS[b.kind] ?? ATTENTION_ICONS.other));
|
|
1092
|
+
document.getElementById('attention-title').textContent = b.name;
|
|
1093
|
+
document.getElementById('attention-kind').textContent = ATTENTION_KINDS[b.kind] ?? 'Needs you';
|
|
1094
|
+
document.getElementById('attention-detail').textContent = `waiting in ${apps[appId]?.name ?? appId}`;
|
|
1095
|
+
document.getElementById('attention-more').textContent = others > 0 ? `+${others} more waiting` : '';
|
|
1096
|
+
toast.dataset.app = appId;
|
|
1097
|
+
toast.dataset.session = b.id;
|
|
1098
|
+
const timer = document.getElementById('attention-timer');
|
|
1099
|
+
timer.style.animation = 'none';
|
|
1100
|
+
void timer.offsetWidth;
|
|
1101
|
+
timer.style.animation = '';
|
|
1102
|
+
clearTimeout(attentionTimer);
|
|
1103
|
+
attentionTimer = setTimeout(hideAttentionToast, ATTENTION_TOAST_MS);
|
|
1104
|
+
showToast(toast);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function bindAttentionToast() {
|
|
1108
|
+
const toast = document.getElementById('attention-toast');
|
|
1109
|
+
toast.style.setProperty('--attention-ms', `${ATTENTION_TOAST_MS}ms`);
|
|
1110
|
+
toast.addEventListener('mouseenter', () => clearTimeout(attentionTimer));
|
|
1111
|
+
toast.addEventListener('mouseleave', () => (attentionTimer = setTimeout(hideAttentionToast, ATTENTION_TOAST_MS / 2)));
|
|
1112
|
+
document.getElementById('attention-close').addEventListener('click', hideAttentionToast);
|
|
1113
|
+
document.getElementById('attention-open').addEventListener('click', () => {
|
|
1114
|
+
hideAttentionToast();
|
|
1115
|
+
onInvoke({ id: 'attention', action: 'session.open', params: { session: toast.dataset.session } }, () => {});
|
|
1116
|
+
});
|
|
1015
1117
|
}
|
|
1016
1118
|
|
|
1017
1119
|
// After a config-dir switch every child is a new process, possibly on a new port. Reloading each
|
|
@@ -1131,6 +1233,7 @@ function switchTab(appId) {
|
|
|
1131
1233
|
if (!iframes[appId]) addIframe(appId);
|
|
1132
1234
|
if (appId !== activeApp) previousApp = activeApp;
|
|
1133
1235
|
activeApp = appId;
|
|
1236
|
+
if (document.getElementById('attention-toast').dataset.app === appId) hideAttentionToast();
|
|
1134
1237
|
try {
|
|
1135
1238
|
localStorage.setItem(TAB_KEY, appId);
|
|
1136
1239
|
} catch {}
|
package/public/index.html
CHANGED
|
@@ -654,6 +654,77 @@
|
|
|
654
654
|
}
|
|
655
655
|
|
|
656
656
|
.update-toast[hidden] { display: none; }
|
|
657
|
+
.attention-toast {
|
|
658
|
+
width: min(380px, calc(100vw - 32px));
|
|
659
|
+
padding: 14px 14px 16px;
|
|
660
|
+
gap: 12px;
|
|
661
|
+
overflow: hidden;
|
|
662
|
+
border-left-color: var(--warn);
|
|
663
|
+
animation: attention-in 0.22s ease-out;
|
|
664
|
+
}
|
|
665
|
+
.attention-icon {
|
|
666
|
+
flex: none;
|
|
667
|
+
display: grid;
|
|
668
|
+
place-items: center;
|
|
669
|
+
width: 34px;
|
|
670
|
+
height: 34px;
|
|
671
|
+
border-radius: 50%;
|
|
672
|
+
background: color-mix(in srgb, var(--warn) 16%, transparent);
|
|
673
|
+
color: var(--warn);
|
|
674
|
+
}
|
|
675
|
+
.attention-icon svg { width: 18px; height: 18px; }
|
|
676
|
+
.attention-name {
|
|
677
|
+
font: 600 14px/1.35 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
678
|
+
overflow: hidden;
|
|
679
|
+
white-space: nowrap;
|
|
680
|
+
text-overflow: ellipsis;
|
|
681
|
+
}
|
|
682
|
+
.attention-meta {
|
|
683
|
+
display: flex;
|
|
684
|
+
align-items: center;
|
|
685
|
+
gap: 6px;
|
|
686
|
+
margin-top: 3px;
|
|
687
|
+
color: var(--text-dim);
|
|
688
|
+
font-size: 12px;
|
|
689
|
+
}
|
|
690
|
+
.attention-kind {
|
|
691
|
+
padding: 0 7px;
|
|
692
|
+
border: 1px solid currentColor;
|
|
693
|
+
border-radius: 9px;
|
|
694
|
+
color: var(--warn);
|
|
695
|
+
font-size: 11px;
|
|
696
|
+
font-weight: 600;
|
|
697
|
+
line-height: 16px;
|
|
698
|
+
}
|
|
699
|
+
.attention-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
|
|
700
|
+
.attention-open {
|
|
701
|
+
padding: 5px 12px;
|
|
702
|
+
border: none;
|
|
703
|
+
border-radius: 6px;
|
|
704
|
+
background: var(--accent);
|
|
705
|
+
color: #fff;
|
|
706
|
+
font: 600 12px system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
707
|
+
cursor: pointer;
|
|
708
|
+
}
|
|
709
|
+
.attention-open:hover { filter: brightness(1.08); }
|
|
710
|
+
.attention-more { color: var(--text-dim); font-size: 12px; }
|
|
711
|
+
.attention-timer {
|
|
712
|
+
position: absolute;
|
|
713
|
+
left: 0;
|
|
714
|
+
right: 0;
|
|
715
|
+
bottom: 0;
|
|
716
|
+
height: 2px;
|
|
717
|
+
background: var(--warn);
|
|
718
|
+
opacity: 0.5;
|
|
719
|
+
transform-origin: left;
|
|
720
|
+
animation: attention-timer var(--attention-ms, 8s) linear forwards;
|
|
721
|
+
}
|
|
722
|
+
.attention-toast:hover .attention-timer { animation-play-state: paused; }
|
|
723
|
+
@keyframes attention-in { from { opacity: 0; transform: translateY(8px); } }
|
|
724
|
+
@keyframes attention-timer { to { transform: scaleX(0); } }
|
|
725
|
+
@media (prefers-reduced-motion: reduce) {
|
|
726
|
+
.attention-toast, .attention-timer { animation: none; }
|
|
727
|
+
}
|
|
657
728
|
.voice-pill {
|
|
658
729
|
position: fixed;
|
|
659
730
|
left: 50%;
|
|
@@ -873,6 +944,20 @@
|
|
|
873
944
|
<button class="update-toast-close" id="update-close" type="button" aria-label="Close">×</button>
|
|
874
945
|
</div>
|
|
875
946
|
|
|
947
|
+
<div class="update-toast attention-toast" id="attention-toast" role="status" hidden>
|
|
948
|
+
<span class="attention-icon" id="attention-icon" aria-hidden="true"></span>
|
|
949
|
+
<div class="update-toast-body">
|
|
950
|
+
<div class="attention-name" id="attention-title"></div>
|
|
951
|
+
<div class="attention-meta"><span class="attention-kind" id="attention-kind"></span><span id="attention-detail"></span></div>
|
|
952
|
+
<div class="attention-actions">
|
|
953
|
+
<button class="attention-open" id="attention-open" type="button">Open session</button>
|
|
954
|
+
<span class="attention-more" id="attention-more"></span>
|
|
955
|
+
</div>
|
|
956
|
+
</div>
|
|
957
|
+
<button class="update-toast-close" id="attention-close" type="button" aria-label="Close">×</button>
|
|
958
|
+
<span class="attention-timer" id="attention-timer"></span>
|
|
959
|
+
</div>
|
|
960
|
+
|
|
876
961
|
<div class="update-toast error-toast" id="error-toast" role="alert" hidden>
|
|
877
962
|
<div class="update-toast-body">
|
|
878
963
|
<div class="update-toast-title" id="error-title"></div>
|
package/server.js
CHANGED
|
@@ -330,6 +330,11 @@ function childEnv(pool, name) {
|
|
|
330
330
|
const mods = appModEnv(MODS, name);
|
|
331
331
|
if (mods) env.HUB_MODS = mods;
|
|
332
332
|
else delete env.HUB_MODS;
|
|
333
|
+
// A setting the app's server needs before any page connects, such as cck's terminal host for
|
|
334
|
+
// the terminals it restores at start. A change later reaches the app through its pages.
|
|
335
|
+
const settings = appValues.get(name);
|
|
336
|
+
if (settings) env.HUB_APP_SETTINGS = JSON.stringify(settings);
|
|
337
|
+
else delete env.HUB_APP_SETTINGS;
|
|
333
338
|
if (TERMINAL.enabled && name === TERMINAL_APP.id) {
|
|
334
339
|
env.CCK_TERMINAL = JSON.stringify(TERMINAL);
|
|
335
340
|
env.CCK_TERMINAL_TOKEN = TERMINAL_TOKEN;
|