claude-code-hub 1.35.0 → 1.36.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.
@@ -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", "experimental"],
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.35.0",
3
+ "version": "1.36.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.32.0",
50
+ "claude-code-kanban": "7.33.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) win.parent.postMessage({ type: 'hub:hostReady' }, e.origin);
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
@@ -1111,8 +1111,9 @@ function addIframe(id) {
1111
1111
  document.getElementById('iframe-container').appendChild(iframes[id]);
1112
1112
  }
1113
1113
 
1114
+ // A deferredStart app's first page load starts it, so its frame waits until the tab opens.
1114
1115
  function buildIframes() {
1115
- for (const id of tabIds()) addIframe(id);
1116
+ for (const id of tabIds()) if (!apps[id].deferredStart) addIframe(id);
1116
1117
  }
1117
1118
 
1118
1119
  const TAB_KEY = 'hub-tab';
@@ -1656,6 +1657,19 @@ const APP_ICONS = {
1656
1657
  ['path', { d: 'M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3' }],
1657
1658
  ['path', { d: 'M12 12V8' }],
1658
1659
  ],
1660
+ 'notebook-pen': [
1661
+ ['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' }],
1662
+ ['path', { d: 'M2 6h4' }],
1663
+ ['path', { d: 'M2 10h4' }],
1664
+ ['path', { d: 'M2 14h4' }],
1665
+ ['path', { d: 'M2 18h4' }],
1666
+ [
1667
+ 'path',
1668
+ {
1669
+ 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',
1670
+ },
1671
+ ],
1672
+ ],
1659
1673
  puzzle: [
1660
1674
  [
1661
1675
  'path',
package/server.js CHANGED
@@ -397,6 +397,7 @@ async function restartApp(pool, app) {
397
397
  const { id } = app;
398
398
  pool.restarts[id] = [];
399
399
  pool.stopped.delete(id);
400
+ pool.deferred.delete(id);
400
401
  await killApp(pool, id);
401
402
  if (pool.retired) return false;
402
403
  const child = spawnOne(pool, app);
@@ -406,9 +407,21 @@ async function restartApp(pool, app) {
406
407
 
407
408
  async function stopApp(pool, { id }) {
408
409
  pool.stopped.add(id);
410
+ pool.deferred.delete(id);
409
411
  await killApp(pool, id);
410
412
  }
411
413
 
414
+ // A deferredStart app that nothing has started yet starts on its first page load: its tab, a pane,
415
+ // an action. One the user stopped stays stopped.
416
+ function startOnDemand(name) {
417
+ const pool = activePool();
418
+ if (!pool?.deferred.delete(name)) return;
419
+ console.log(`[${name}] starting on first open`);
420
+ pool.stopped.delete(name);
421
+ const target = ENABLED_APPS.find((a) => a.id === name);
422
+ spawnOne(pool, target);
423
+ }
424
+
412
425
  // Out of pool.children before the kill, so the exit handler does not restart it.
413
426
  async function killApp(pool, id) {
414
427
  const old = pool.children.get(id);
@@ -448,7 +461,7 @@ async function ensurePool(dir) {
448
461
  pool = pools.get(dir);
449
462
  if (pool) return pool.ready;
450
463
  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 };
464
+ pool = { dir, children: new Map(), ports: {}, restarts: {}, stopped, deferred: new Set(stopped), retired: false };
452
465
  pools.set(dir, pool);
453
466
  spawnChildren(pool);
454
467
  const ready = [...pool.children.values()].map((c) => c.ready);
@@ -819,6 +832,7 @@ function proxyHandler(name) {
819
832
  if (body === undefined) return;
820
833
  // One deadline for the whole request, not one per attempt: a retry must not re-spend the wait.
821
834
  const budget = { deadline: Date.now() + (isNavigation(req) ? NAV_PORT_WAIT_MS : API_PORT_WAIT_MS) };
835
+ if (isNavigation(req)) startOnDemand(name);
822
836
  // One listener for the request, however many attempts it takes.
823
837
  let current = null;
824
838
  res.once('close', () => current?.destroy());
@@ -963,6 +977,7 @@ function appsConfig() {
963
977
  publishes: a.publishes,
964
978
  };
965
979
  if (a.hidden) entry.hidden = true;
980
+ if (a.deferredStart) entry.deferredStart = true;
966
981
  if (a.settings.length) entry.settings = appValues.get(a.id);
967
982
  if (TERMINAL_TOKEN && a.id === TERMINAL_APP.id) entry.terminalToken = TERMINAL_TOKEN;
968
983
  return [a.id, entry];
@@ -999,7 +1014,7 @@ app.get('/api/config', (_req, res) => {
999
1014
  configDirSwitch: configDirSwitch(hubConfig),
1000
1015
  voice: voice && { provider: voice.provider, auto: voice.auto, commands: voice.commands },
1001
1016
  version: HUB_VERSION,
1002
- onboarding: hubConfig.onboarding ?? null,
1017
+ onboarding: process.env.CLAUDE_HUB_NO_ONBOARDING ? 'skipped' : (hubConfig.onboarding ?? null),
1003
1018
  });
1004
1019
  });
1005
1020