claude-code-hub 1.35.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/features/voice/feature.json +1 -1
- package/package.json +2 -2
- package/packages/claude-hub-sdk/src/client.js +6 -2
- package/public/app.js +120 -3
- package/public/index.html +85 -0
- package/server.js +22 -2
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"listing": {
|
|
21
21
|
"tagline": "Hold a key and speak: start a session or send a prompt with no Enter press",
|
|
22
22
|
"description": "Hold Ctrl+Shift+Space, speak and let go. \"New session, fix the flaky test\" opens the New session dialog with that prompt, and anything else goes to the session on screen as a prompt and is submitted. Kanban's New session microphone uses the same speech to text. Hex does the speech to text on your machine; without it, the browser's speech recognition does it.",
|
|
23
|
-
"tags": ["voice"
|
|
23
|
+
"tags": ["voice"],
|
|
24
24
|
"screenshots": [
|
|
25
25
|
{
|
|
26
26
|
"url": "https://nikiforovall.blog/claude-code-hub/shots/extensions/voice-demo.webp",
|
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",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
} catch (_) {}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function connect({ standalone = {} } = {}) {
|
|
104
|
+
function connect({ standalone = {}, plainHostKeys = [] } = {}) {
|
|
105
105
|
if (hub) return hub;
|
|
106
106
|
const topics = new Map();
|
|
107
107
|
const activeFns = new Set();
|
|
@@ -275,7 +275,11 @@
|
|
|
275
275
|
const m = e.data;
|
|
276
276
|
if (!framed || e.source !== win.parent || win.parent === win.top) return;
|
|
277
277
|
if (m?.type !== 'hub:hostKeys' || !Array.isArray(m.keys)) return;
|
|
278
|
-
if (!hostKeys)
|
|
278
|
+
if (!hostKeys)
|
|
279
|
+
win.parent.postMessage(
|
|
280
|
+
{ type: 'hub:hostReady', ...(plainHostKeys.length && { plainKeys: plainHostKeys }) },
|
|
281
|
+
e.origin,
|
|
282
|
+
);
|
|
279
283
|
hostOrigin = e.origin;
|
|
280
284
|
hostKeys = new Set(m.keys.map((k) => keySigs(k)[0]));
|
|
281
285
|
}
|
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
|
|
@@ -1111,8 +1213,9 @@ function addIframe(id) {
|
|
|
1111
1213
|
document.getElementById('iframe-container').appendChild(iframes[id]);
|
|
1112
1214
|
}
|
|
1113
1215
|
|
|
1216
|
+
// A deferredStart app's first page load starts it, so its frame waits until the tab opens.
|
|
1114
1217
|
function buildIframes() {
|
|
1115
|
-
for (const id of tabIds()) addIframe(id);
|
|
1218
|
+
for (const id of tabIds()) if (!apps[id].deferredStart) addIframe(id);
|
|
1116
1219
|
}
|
|
1117
1220
|
|
|
1118
1221
|
const TAB_KEY = 'hub-tab';
|
|
@@ -1130,6 +1233,7 @@ function switchTab(appId) {
|
|
|
1130
1233
|
if (!iframes[appId]) addIframe(appId);
|
|
1131
1234
|
if (appId !== activeApp) previousApp = activeApp;
|
|
1132
1235
|
activeApp = appId;
|
|
1236
|
+
if (document.getElementById('attention-toast').dataset.app === appId) hideAttentionToast();
|
|
1133
1237
|
try {
|
|
1134
1238
|
localStorage.setItem(TAB_KEY, appId);
|
|
1135
1239
|
} catch {}
|
|
@@ -1656,6 +1760,19 @@ const APP_ICONS = {
|
|
|
1656
1760
|
['path', { d: 'M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3' }],
|
|
1657
1761
|
['path', { d: 'M12 12V8' }],
|
|
1658
1762
|
],
|
|
1763
|
+
'notebook-pen': [
|
|
1764
|
+
['path', { d: 'M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4' }],
|
|
1765
|
+
['path', { d: 'M2 6h4' }],
|
|
1766
|
+
['path', { d: 'M2 10h4' }],
|
|
1767
|
+
['path', { d: 'M2 14h4' }],
|
|
1768
|
+
['path', { d: 'M2 18h4' }],
|
|
1769
|
+
[
|
|
1770
|
+
'path',
|
|
1771
|
+
{
|
|
1772
|
+
d: 'M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z',
|
|
1773
|
+
},
|
|
1774
|
+
],
|
|
1775
|
+
],
|
|
1659
1776
|
puzzle: [
|
|
1660
1777
|
[
|
|
1661
1778
|
'path',
|
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;
|
|
@@ -397,6 +402,7 @@ async function restartApp(pool, app) {
|
|
|
397
402
|
const { id } = app;
|
|
398
403
|
pool.restarts[id] = [];
|
|
399
404
|
pool.stopped.delete(id);
|
|
405
|
+
pool.deferred.delete(id);
|
|
400
406
|
await killApp(pool, id);
|
|
401
407
|
if (pool.retired) return false;
|
|
402
408
|
const child = spawnOne(pool, app);
|
|
@@ -406,9 +412,21 @@ async function restartApp(pool, app) {
|
|
|
406
412
|
|
|
407
413
|
async function stopApp(pool, { id }) {
|
|
408
414
|
pool.stopped.add(id);
|
|
415
|
+
pool.deferred.delete(id);
|
|
409
416
|
await killApp(pool, id);
|
|
410
417
|
}
|
|
411
418
|
|
|
419
|
+
// A deferredStart app that nothing has started yet starts on its first page load: its tab, a pane,
|
|
420
|
+
// an action. One the user stopped stays stopped.
|
|
421
|
+
function startOnDemand(name) {
|
|
422
|
+
const pool = activePool();
|
|
423
|
+
if (!pool?.deferred.delete(name)) return;
|
|
424
|
+
console.log(`[${name}] starting on first open`);
|
|
425
|
+
pool.stopped.delete(name);
|
|
426
|
+
const target = ENABLED_APPS.find((a) => a.id === name);
|
|
427
|
+
spawnOne(pool, target);
|
|
428
|
+
}
|
|
429
|
+
|
|
412
430
|
// Out of pool.children before the kill, so the exit handler does not restart it.
|
|
413
431
|
async function killApp(pool, id) {
|
|
414
432
|
const old = pool.children.get(id);
|
|
@@ -448,7 +466,7 @@ async function ensurePool(dir) {
|
|
|
448
466
|
pool = pools.get(dir);
|
|
449
467
|
if (pool) return pool.ready;
|
|
450
468
|
const stopped = new Set(ENABLED_APPS.filter((a) => a.deferredStart).map((a) => a.id));
|
|
451
|
-
pool = { dir, children: new Map(), ports: {}, restarts: {}, stopped, retired: false };
|
|
469
|
+
pool = { dir, children: new Map(), ports: {}, restarts: {}, stopped, deferred: new Set(stopped), retired: false };
|
|
452
470
|
pools.set(dir, pool);
|
|
453
471
|
spawnChildren(pool);
|
|
454
472
|
const ready = [...pool.children.values()].map((c) => c.ready);
|
|
@@ -819,6 +837,7 @@ function proxyHandler(name) {
|
|
|
819
837
|
if (body === undefined) return;
|
|
820
838
|
// One deadline for the whole request, not one per attempt: a retry must not re-spend the wait.
|
|
821
839
|
const budget = { deadline: Date.now() + (isNavigation(req) ? NAV_PORT_WAIT_MS : API_PORT_WAIT_MS) };
|
|
840
|
+
if (isNavigation(req)) startOnDemand(name);
|
|
822
841
|
// One listener for the request, however many attempts it takes.
|
|
823
842
|
let current = null;
|
|
824
843
|
res.once('close', () => current?.destroy());
|
|
@@ -963,6 +982,7 @@ function appsConfig() {
|
|
|
963
982
|
publishes: a.publishes,
|
|
964
983
|
};
|
|
965
984
|
if (a.hidden) entry.hidden = true;
|
|
985
|
+
if (a.deferredStart) entry.deferredStart = true;
|
|
966
986
|
if (a.settings.length) entry.settings = appValues.get(a.id);
|
|
967
987
|
if (TERMINAL_TOKEN && a.id === TERMINAL_APP.id) entry.terminalToken = TERMINAL_TOKEN;
|
|
968
988
|
return [a.id, entry];
|
|
@@ -999,7 +1019,7 @@ app.get('/api/config', (_req, res) => {
|
|
|
999
1019
|
configDirSwitch: configDirSwitch(hubConfig),
|
|
1000
1020
|
voice: voice && { provider: voice.provider, auto: voice.auto, commands: voice.commands },
|
|
1001
1021
|
version: HUB_VERSION,
|
|
1002
|
-
onboarding: hubConfig.onboarding ?? null,
|
|
1022
|
+
onboarding: process.env.CLAUDE_HUB_NO_ONBOARDING ? 'skipped' : (hubConfig.onboarding ?? null),
|
|
1003
1023
|
});
|
|
1004
1024
|
});
|
|
1005
1025
|
|