@unson/brainbase-mcp 0.10.2 → 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -336,5 +336,5 @@
336
336
  "engines": {
337
337
  "node": ">=20"
338
338
  },
339
- "gitHead": "d8334b5e94cdc643f5de90ccfc883a5e67fdd9af"
339
+ "gitHead": "8abcea403a82666c8c4616d4023fb28a10af761a"
340
340
  }
@@ -54,7 +54,12 @@ import {
54
54
  graphHostEmptyNotice,
55
55
  } from './graph-view-shared.js';
56
56
  import { graphOwnShareSource, ownProjectDetail, ownShareSection, ownShareStateNotice } from './graph-own-share.js';
57
- import { createProjectKnowledgeWorkspace, normalizeProjectContext } from './project-workspace.js';
57
+ import {
58
+ createProjectKnowledgeWorkspace,
59
+ normalizeProjectContext,
60
+ renderProjectContext,
61
+ renderProjectKnowledgeSkeleton,
62
+ } from './project-workspace.js';
58
63
  import { mountProjectGraph as defaultMountProjectGraph } from './project-graph.js';
59
64
  import {
60
65
  workspaceActions,
@@ -211,9 +216,11 @@ function normalizeProjectSummary(value) {
211
216
  * labelled 「引き継いだ自分の分(組織には未反映)」 unless `label` says otherwise. They are never
212
217
  * added to the ledger, the metrics or the rail, and a failed read shows only in that section.
213
218
  * Without it nothing of the kind is drawn or read.
214
- * @param {(projectId: string) => Promise<object>} [options.loadProjectContext]
215
- * Optional read-only supplementary sections (tasks/knowledge). A failed
216
- * context read is kept separate from the Graph detail read.
219
+ * @param {(projectId: string, options?: { onProgress?: (snapshot: object) => void }) => Promise<object>} [options.loadProjectContext]
220
+ * Optional read-only supplementary sections (tasks/knowledge). The host may
221
+ * call `onProgress` with a complete `{ sections: [...] }` snapshot as each
222
+ * section resolves; a failed context read is kept separate from the Graph
223
+ * detail read.
217
224
  * @param {Array<{ id: string, name?: string }> | (() => Array<{ id: string, name?: string }>)} [options.projectSummaries]
218
225
  * Optional host-known project identities. These are used only to draw the
219
226
  * selected project's early loading shell before the Graph list is read.
@@ -330,7 +337,10 @@ export function createGraphProjectsView({
330
337
  const project = projectSummaryFor(target);
331
338
  const loading = makeElement(doc, 'section', {
332
339
  className: 'bb-pkw bb-pkw-loading',
333
- attrs: { 'aria-label': 'プロジェクトの知識', 'data-project-id': target },
340
+ attrs: {
341
+ 'aria-label': 'プロジェクトの知識',
342
+ 'data-project-id': target,
343
+ },
334
344
  });
335
345
  loading.append(makeElement(doc, 'div', { className: 'bb-pkw-kicker', text: 'プロジェクトの記録' }));
336
346
  const header = makeElement(doc, 'header', { className: 'bb-pkw-header' });
@@ -344,6 +354,12 @@ export function createGraphProjectsView({
344
354
  loadingText: 'プロジェクトの記録を読み込んでいます。',
345
355
  });
346
356
  if (notice) loading.append(notice);
357
+ if (readState?.state === 'loading') {
358
+ loading.append(renderProjectKnowledgeSkeleton(doc, { context: state.context?.projectId === target ? state.context : null }));
359
+ } else if (state.context?.projectId === target) {
360
+ const contextBlock = renderProjectContext(doc, state.context);
361
+ if (contextBlock) loading.append(contextBlock);
362
+ }
347
363
  return loading;
348
364
  }
349
365
 
@@ -403,16 +419,27 @@ export function createGraphProjectsView({
403
419
  const request = ++contextRequest;
404
420
  state.context = normalizeProjectContext(id, { state: 'loading' });
405
421
  controller.render();
422
+ const applyContext = (snapshot) => {
423
+ const normalized = normalizeProjectContext(id, snapshot);
424
+ if (viewDestroyed || request !== contextRequest || state.selectedId !== id) return false;
425
+ state.context = normalized;
426
+ // Context progress must not rebuild the workspace: rebuilding destroys
427
+ // Sigma's viewport and resets the user's active tab/selection. Detail
428
+ // loading still uses the shell render so the project name is immediate.
429
+ if (knowledgeWorkspace && state.detail?.id === id && state.detail.state === 'ok') {
430
+ knowledgeWorkspace.setContext?.(normalized);
431
+ } else {
432
+ controller.render();
433
+ }
434
+ return true;
435
+ };
406
436
  let result;
407
437
  try {
408
- result = await loadProjectContext(id);
438
+ result = await loadProjectContext(id, { onProgress: (snapshot) => { applyContext(snapshot); } });
409
439
  } catch (error) {
410
440
  result = { state: 'failed', message: `補足情報を読み取れませんでした(${failureText(error)})。` };
411
441
  }
412
- const normalized = normalizeProjectContext(id, result);
413
- if (viewDestroyed || request !== contextRequest || state.selectedId !== id) return state.context;
414
- state.context = normalized;
415
- controller.render();
442
+ applyContext(result);
416
443
  return state.context;
417
444
  }
418
445
 
@@ -267,6 +267,44 @@
267
267
  padding-top: var(--bb-space-3);
268
268
  }
269
269
 
270
+ .bb-pkw-skeleton-section {
271
+ min-height: 118px;
272
+ }
273
+
274
+ .bb-pkw-skeleton-stack {
275
+ display: grid;
276
+ gap: var(--bb-space-2);
277
+ width: min(100%, 360px);
278
+ }
279
+
280
+ .bb-pkw-skeleton-bar {
281
+ display: block;
282
+ width: 100%;
283
+ height: 10px;
284
+ border-radius: 999px;
285
+ background: color-mix(in srgb, var(--bb-color-border-strong) 65%, var(--bb-color-panel));
286
+ animation: bb-pkw-skeleton-pulse 1.35s ease-in-out infinite;
287
+ }
288
+
289
+ .bb-pkw-skeleton-bar-1 { width: 92%; }
290
+ .bb-pkw-skeleton-bar-2 { width: 68%; }
291
+ .bb-pkw-skeleton-bar-3 { width: 82%; }
292
+
293
+ .bb-pkw-context-group-loading {
294
+ min-height: 72px;
295
+ }
296
+
297
+ @keyframes bb-pkw-skeleton-pulse {
298
+ 0%, 100% { opacity: .48; }
299
+ 50% { opacity: 1; }
300
+ }
301
+
302
+ @media (prefers-reduced-motion: reduce) {
303
+ .bb-pkw-skeleton-bar {
304
+ animation: none;
305
+ }
306
+ }
307
+
270
308
  .bb-pkw-context-heading,
271
309
  .bb-pkw-graph-intro,
272
310
  .bb-pkw-selected-head,
@@ -95,7 +95,18 @@ export function normalizeProjectContext(projectId, result) {
95
95
  if (result?.state === 'failed' || result?.state === 'error') {
96
96
  return { projectId: id, state: 'failed', message: firstText(result.message, result.reason) ?? '補足情報を読み取れませんでした。', sections: [] };
97
97
  }
98
- if (result?.state === 'loading') return { projectId: id, state: 'loading', sections: [] };
98
+ if (result?.state === 'loading') {
99
+ const partial = result?.payload && isRecord(result.payload) ? result.payload : result;
100
+ const rawSections = Array.isArray(partial.sections) ? partial.sections : [];
101
+ const normalized = {
102
+ projectId: id,
103
+ state: 'loading',
104
+ sections: rawSections.map(normalizeContextSection).filter(Boolean),
105
+ };
106
+ const message = firstText(result.message, result.reason);
107
+ if (message) normalized.message = message;
108
+ return normalized;
109
+ }
99
110
  if (result?.state === 'unavailable') {
100
111
  return { projectId: id, state: 'unavailable', message: firstText(result.message, result.reason) ?? '補足情報は接続されていません。', sections: [] };
101
112
  }
@@ -107,7 +118,7 @@ export function normalizeProjectContext(projectId, result) {
107
118
  if (sections.length === 0 && payload.sections.length > 0) {
108
119
  return { projectId: id, state: 'unavailable', message: '補足情報の形式を確認できません。', sections: [] };
109
120
  }
110
- return { projectId: id, state: 'ok', sections };
121
+ return { projectId: id, state: sections.some((section) => section.state === 'loading') ? 'loading' : 'ok', sections };
111
122
  }
112
123
 
113
124
  function addEntity(map, candidate) {
@@ -292,30 +303,66 @@ function renderEntityRelations(doc, edges, direction, entity, asOf, onSelect) {
292
303
  return list.children.length > 0 ? list : null;
293
304
  }
294
305
 
295
- function renderContext(doc, context) {
306
+ function skeletonBars(doc, count = 3) {
307
+ const stack = makeElement(doc, 'div', { className: 'bb-pkw-skeleton-stack', attrs: { 'aria-hidden': 'true' } });
308
+ for (let index = 0; index < count; index += 1) {
309
+ stack.append(makeElement(doc, 'span', {
310
+ className: `bb-pkw-skeleton-bar bb-pkw-skeleton-bar-${(index % 3) + 1}`,
311
+ attrs: { 'aria-hidden': 'true' },
312
+ }));
313
+ }
314
+ return stack;
315
+ }
316
+
317
+ function contextSectionSkeleton(doc, { title = null, message = null } = {}) {
318
+ const group = makeElement(doc, 'section', {
319
+ className: 'bb-pkw-context-group bb-pkw-context-group-loading',
320
+ attrs: { 'aria-label': title ?? '補足情報', 'aria-busy': 'true' },
321
+ });
322
+ if (title) group.append(makeElement(doc, 'h4', { text: title }));
323
+ if (message) group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: message }));
324
+ group.append(skeletonBars(doc));
325
+ return group;
326
+ }
327
+
328
+ /** Renders supplementary context while preserving each section's own state. */
329
+ export function renderProjectContext(doc, context) {
296
330
  if (!context) return null;
297
- const section = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-context', attrs: { 'aria-label': '補足情報' } });
331
+ const sections = Array.isArray(context.sections) ? context.sections : [];
332
+ const section = makeElement(doc, 'section', {
333
+ className: 'bb-pkw-section bb-pkw-context',
334
+ attrs: { 'aria-label': '補足情報' },
335
+ });
298
336
  section.append(makeElement(doc, 'h3', { text: '補足情報' }));
299
- if (context.state === 'loading') {
300
- section.append(workspaceNotice(doc, { label: '読み込み中', text: 'タスクや知識の補足情報を読み込んでいます。', tone: 'info', role: 'status' }));
301
- return section;
302
- }
303
337
  if (context.state !== 'ok') {
304
- section.append(workspaceNotice(doc, { label: context.state === 'failed' ? '読み取り失敗' : '未接続', text: context.message ?? '補足情報を確認できません。', tone: context.state === 'failed' ? 'danger' : 'warning' }));
305
- return section;
338
+ if (context.state === 'loading') {
339
+ section.append(workspaceNotice(doc, { label: '読み込み中', text: context.message ?? '補足情報を読み込んでいます。', tone: 'info', role: 'status' }));
340
+ if (sections.length === 0) section.append(contextSectionSkeleton(doc));
341
+ } else {
342
+ section.append(workspaceNotice(doc, { label: context.state === 'failed' ? '読み取り失敗' : '未接続', text: context.message ?? '補足情報を確認できません。', tone: context.state === 'failed' ? 'danger' : 'warning' }));
343
+ }
306
344
  }
307
- if (context.sections.length === 0) {
345
+ if (context.state === 'ok' && sections.length === 0) {
308
346
  section.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '補足情報のセクションはありません。' }));
309
347
  return section;
310
348
  }
311
- for (const item of context.sections) {
312
- const group = makeElement(doc, 'section', { className: 'bb-pkw-context-group', attrs: { 'aria-label': item.title } });
349
+ for (const item of sections) {
350
+ const group = makeElement(doc, 'section', {
351
+ className: `bb-pkw-context-group${item.state === 'loading' ? ' bb-pkw-context-group-loading' : ''}`,
352
+ attrs: { 'aria-label': item.title, ...(item.state === 'loading' ? { 'aria-busy': 'true' } : {}) },
353
+ });
313
354
  const heading = makeElement(doc, 'div', { className: 'bb-pkw-context-heading' });
314
355
  heading.append(makeElement(doc, 'h4', { text: item.title }), badge(doc, item.state === 'ok' ? '利用可能' : item.state === 'loading' ? '読み込み中' : item.state === 'failed' ? '読み取り失敗' : '未接続', item.state === 'ok' ? 'success' : item.state === 'failed' ? 'danger' : 'muted'));
315
356
  group.append(heading);
316
- if (item.message) group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: item.message }));
357
+ if (item.message && !['failed', 'unavailable'].includes(item.state)) group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: item.message }));
317
358
  if (item.asOf) group.append(makeElement(doc, 'small', { className: 'bb-pkw-muted', text: `読み取った時点: ${formatDateTime(item.asOf)}` }));
318
- if (item.items.length > 0) {
359
+ if (item.state === 'loading') {
360
+ group.append(skeletonBars(doc));
361
+ } else if (item.state === 'failed') {
362
+ group.append(workspaceNotice(doc, { label: '読み取り失敗', text: item.message ?? 'このセクションを読み取れませんでした。', tone: 'danger', role: 'alert' }));
363
+ } else if (item.state === 'unavailable') {
364
+ group.append(workspaceNotice(doc, { label: '未接続', text: item.message ?? 'このセクションは接続されていません。', tone: 'warning' }));
365
+ } else if (item.items.length > 0) {
319
366
  const list = makeElement(doc, 'ul', { className: 'bb-pkw-context-list' });
320
367
  for (const entry of item.items) {
321
368
  const row = makeElement(doc, 'li');
@@ -334,7 +381,30 @@ function renderContext(doc, context) {
334
381
  return section;
335
382
  }
336
383
 
337
- function renderOverview(doc, detail, context, edges, entities, onSelect) {
384
+ /** Structural shell used while the Graph detail is loading; it contains no inferred values. */
385
+ export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
386
+ const panel = makeElement(doc, 'div', {
387
+ className: 'bb-pkw-panel bb-pkw-overview bb-pkw-overview-loading',
388
+ attrs: { 'aria-label': '概要' },
389
+ });
390
+ const columns = makeElement(doc, 'div', { className: 'bb-pkw-columns' });
391
+ for (const title of ['目的と現在地', '関係者と役割']) {
392
+ const item = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-skeleton-section', attrs: { 'aria-label': title, 'aria-busy': 'true' } });
393
+ item.append(makeElement(doc, 'h3', { text: title }), skeletonBars(doc));
394
+ columns.append(item);
395
+ }
396
+ panel.append(columns);
397
+ for (const title of ['記録された判断', '関連する情報']) {
398
+ const item = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-skeleton-section', attrs: { 'aria-label': title, 'aria-busy': 'true' } });
399
+ item.append(makeElement(doc, 'h3', { text: title }), skeletonBars(doc, 2));
400
+ panel.append(item);
401
+ }
402
+ const contextBlock = renderProjectContext(doc, context ?? { state: 'loading', sections: [] });
403
+ if (contextBlock) panel.append(contextBlock);
404
+ return panel;
405
+ }
406
+
407
+ function renderOverview(doc, detail, context, edges, entities, onSelect, onContext) {
338
408
  const payload = projectDetailPayload(detail);
339
409
  const project = payload?.project;
340
410
  const panel = makeElement(doc, 'div', { className: 'bb-pkw-panel bb-pkw-overview', attrs: { role: 'tabpanel', 'aria-label': '概要' } });
@@ -423,8 +493,11 @@ function renderOverview(doc, detail, context, edges, entities, onSelect) {
423
493
  missing.append(list);
424
494
  panel.append(missing);
425
495
  }
426
- const contextBlock = renderContext(doc, context);
427
- if (contextBlock) panel.append(contextBlock);
496
+ const contextBlock = renderProjectContext(doc, context);
497
+ if (contextBlock) {
498
+ panel.append(contextBlock);
499
+ if (typeof onContext === 'function') onContext(contextBlock);
500
+ }
428
501
  return panel;
429
502
  }
430
503
 
@@ -463,10 +536,13 @@ export function createProjectKnowledgeWorkspace({
463
536
  let entityReadRequest = 0;
464
537
  let graphSearch = '';
465
538
  let graphType = '';
539
+ let currentContext = context;
466
540
  // Keep direct references as well as the DOM lookup. The local test harness
467
541
  // intentionally has no querySelector implementation, and a host can replace
468
542
  // the panel while a detail read is still in flight.
469
543
  let selectedPanel = null;
544
+ let overviewPanel = null;
545
+ let contextBlock = null;
470
546
 
471
547
  const root = makeElement(doc, 'section', { className: 'bb-pkw', attrs: { 'data-contract-version': PROJECT_WORKSPACE_CONTRACT_VERSION, 'aria-label': 'プロジェクトの知識' } });
472
548
  const body = makeElement(doc, 'div', { className: 'bb-pkw-body' });
@@ -698,15 +774,42 @@ export function createProjectKnowledgeWorkspace({
698
774
  graphHandle = null;
699
775
  }
700
776
 
777
+ function replaceContextBlock(nextContext) {
778
+ currentContext = nextContext;
779
+ if (destroyed) return;
780
+ const nextBlock = renderProjectContext(doc, currentContext);
781
+ if (!nextBlock) {
782
+ if (contextBlock?.parentNode?.removeChild) contextBlock.parentNode.removeChild(contextBlock);
783
+ contextBlock = null;
784
+ return;
785
+ }
786
+ if (contextBlock?.parentNode && typeof contextBlock.parentNode.replaceChild === 'function') {
787
+ contextBlock.parentNode.replaceChild(nextBlock, contextBlock);
788
+ contextBlock = nextBlock;
789
+ return;
790
+ }
791
+ if (contextBlock?.replaceWith) {
792
+ contextBlock.replaceWith(nextBlock);
793
+ contextBlock = nextBlock;
794
+ return;
795
+ }
796
+ if (overviewPanel && !contextBlock) {
797
+ overviewPanel.append(nextBlock);
798
+ contextBlock = nextBlock;
799
+ }
800
+ }
801
+
701
802
  function render() {
702
803
  if (destroyed) return;
703
804
  if (typeof onTabChange === 'function') onTabChange(activeTab);
704
805
  teardownGraph();
705
806
  tabs.replaceChildren(tabButton('概要', 'overview'), tabButton('情報を探す', 'graph'));
706
807
  panels.replaceChildren();
707
- const overview = renderOverview(doc, payload, context, overviewEdges, overviewEntities, selectAndExplore);
808
+ contextBlock = null;
809
+ const overview = renderOverview(doc, payload, currentContext, overviewEdges, overviewEntities, selectAndExplore, (node) => { contextBlock = node; });
708
810
  overview.id = 'bb-pkw-panel-overview';
709
811
  overview.hidden = activeTab !== 'overview';
812
+ overviewPanel = overview;
710
813
  panels.append(overview);
711
814
  const graph = renderGraphPanel();
712
815
  graph.hidden = activeTab !== 'graph';
@@ -721,7 +824,7 @@ export function createProjectKnowledgeWorkspace({
721
824
  }
722
825
 
723
826
  render();
724
- return Object.freeze({ element: root, destroy, select, setTab });
827
+ return Object.freeze({ element: root, destroy, select, setTab, setContext: replaceContextBlock });
725
828
  }
726
829
 
727
830
  export default createProjectKnowledgeWorkspace;