@unson/brainbase-mcp 0.10.9 → 0.10.10

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.9",
3
+ "version": "0.10.10",
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": "56b00ab769c11319de73f799e8be90a95a81b7f0"
339
+ "gitHead": "12af97cb3f02f9964c7bb7ca8ba7a5debdf29e11"
340
340
  }
@@ -933,6 +933,56 @@ button.bb-pkw-context-task-title:focus-visible { outline: 3px solid var(--bb-col
933
933
  gap: var(--bb-space-2);
934
934
  }
935
935
 
936
+ .bb-pkw-context-directory {
937
+ margin-block: var(--bb-space-5);
938
+ }
939
+
940
+ .bb-pkw-directory-grid {
941
+ display: grid;
942
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
943
+ gap: var(--bb-space-3);
944
+ }
945
+
946
+ .bb-pkw-directory-card {
947
+ display: grid;
948
+ align-content: start;
949
+ gap: var(--bb-space-2);
950
+ min-width: 0;
951
+ border: 1px solid var(--bb-color-border);
952
+ border-radius: var(--bb-radius-sm);
953
+ padding: var(--bb-space-4);
954
+ background: var(--bb-color-panel);
955
+ }
956
+
957
+ .bb-pkw-directory-card h4,
958
+ .bb-pkw-directory-card p {
959
+ margin: 0;
960
+ }
961
+
962
+ .bb-pkw-directory-count {
963
+ font-size: 16px;
964
+ font-weight: 720;
965
+ }
966
+
967
+ .bb-pkw-directory-card .bb-pkw-dense-action {
968
+ justify-self: start;
969
+ margin-top: var(--bb-space-2);
970
+ }
971
+
972
+ .bb-pkw-list-pager {
973
+ display: flex;
974
+ align-items: center;
975
+ justify-content: flex-end;
976
+ gap: var(--bb-space-3);
977
+ }
978
+
979
+ .bb-pkw-list-pager button:disabled {
980
+ color: var(--bb-color-muted);
981
+ cursor: default;
982
+ text-decoration: none;
983
+ opacity: .5;
984
+ }
985
+
936
986
  .bb-pkw-work-search,
937
987
  .bb-pkw-work-status,
938
988
  .bb-pkw-work-owner,
@@ -417,13 +417,6 @@ function renderProjectContextGroup(doc, item, onTaskOpen) {
417
417
  return group;
418
418
  }
419
419
 
420
- function projectContextInsertionAnchor(doc) {
421
- return makeElement(doc, 'span', {
422
- className: 'bb-pkw-context-anchor',
423
- attrs: { 'aria-hidden': 'true', hidden: 'true' },
424
- });
425
- }
426
-
427
420
  /** Renders supplementary context while preserving each section's own state. */
428
421
  export function renderProjectContext(doc, context, {
429
422
  sections: selectedSections = null,
@@ -462,12 +455,7 @@ export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
462
455
  className: 'bb-pkw-panel bb-pkw-overview bb-pkw-overview-loading',
463
456
  attrs: { 'aria-label': '概要' },
464
457
  });
465
- const contextSections = Array.isArray(context?.sections) ? context.sections : [];
466
- const readySections = contextSections.filter((item) => item?.state === 'ok' && Array.isArray(item.items) && item.items.length > 0);
467
- const deferredSections = contextSections.filter((item) => !readySections.includes(item));
468
- if (readySections.length > 0) {
469
- panel.append(renderProjectContext(doc, context, { sections: readySections, includeStatus: false }));
470
- }
458
+ panel.append(renderContextDirectory(doc, context));
471
459
  const columns = makeElement(doc, 'div', { className: 'bb-pkw-columns' });
472
460
  for (const title of ['目的と現在地', '関係者と役割']) {
473
461
  const item = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-skeleton-section', attrs: { 'aria-label': title, 'aria-busy': 'true' } });
@@ -475,15 +463,6 @@ export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
475
463
  columns.append(item);
476
464
  }
477
465
  panel.append(columns);
478
- for (const title of ['記録された判断', '関連する情報']) {
479
- const item = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-skeleton-section', attrs: { 'aria-label': title, 'aria-busy': 'true' } });
480
- item.append(makeElement(doc, 'h3', { text: title }), skeletonBars(doc, 2));
481
- panel.append(item);
482
- }
483
- const contextBlock = readySections.length > 0
484
- ? (deferredSections.length > 0 ? renderProjectContext(doc, context, { sections: deferredSections, includeHeading: false }) : null)
485
- : renderProjectContext(doc, context ?? { state: 'loading', sections: [] });
486
- if (contextBlock) panel.append(contextBlock);
487
466
  return panel;
488
467
  }
489
468
 
@@ -520,6 +499,51 @@ function explicitSectionTotal(section) {
520
499
  return null;
521
500
  }
522
501
 
502
+ const CONTEXT_CATEGORIES = [
503
+ ['tasks', 'タスク'], ['decisions', '判断'], ['knowledge', '知識'], ['glossary', '用語集'],
504
+ ];
505
+
506
+ function contextSectionState(context, section) {
507
+ if (section) return section.state;
508
+ if (context?.state === 'failed') return 'failed';
509
+ if (context?.state === 'loading') return 'loading';
510
+ if (context?.state === 'unknown') return 'unknown';
511
+ return 'unavailable';
512
+ }
513
+
514
+ function contextCountLabel(context, section) {
515
+ if (contextSectionState(context, section) !== 'ok') return '件数未確認';
516
+ const total = explicitSectionTotal(section);
517
+ if (total === null) return `${section.items.length}件取得済み · 総数未確認`;
518
+ return `${total}件 · ${section.items.length}件取得済み${total > section.items.length ? ' · 続き未取得' : ''}`;
519
+ }
520
+
521
+ function renderContextDirectory(doc, context, actions = {}) {
522
+ const panel = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-context-directory', attrs: { 'aria-label': '項目別の一覧' } });
523
+ panel.append(makeElement(doc, 'h3', { text: '項目別に探す' }));
524
+ if (context?.state && context.state !== 'ok') panel.append(workspaceNotice(doc, {
525
+ label: context.state === 'loading' ? '読み込み中' : context.state === 'failed' ? '読み取り失敗' : '未確認',
526
+ text: context.message ?? '項目の取得状態を確認できません。',
527
+ tone: context.state === 'failed' ? 'danger' : 'info',
528
+ }));
529
+ const grid = makeElement(doc, 'div', { className: 'bb-pkw-directory-grid' });
530
+ for (const [id, title] of CONTEXT_CATEGORIES) {
531
+ const section = contextSectionById(context, [id]);
532
+ const state = contextSectionState(context, section);
533
+ const card = makeElement(doc, 'div', { className: 'bb-pkw-directory-card' });
534
+ card.append(makeElement(doc, 'h4', { text: title }));
535
+ card.append(makeElement(doc, 'p', { className: 'bb-pkw-directory-count', text: contextCountLabel(context, section) }));
536
+ const stateLabel = state === 'ok'
537
+ ? (explicitSectionTotal(section) === 0 && section.items.length === 0 ? '該当項目なし' : '取得済み')
538
+ : state === 'loading' ? '読み込み中' : state === 'failed' ? '読み取り失敗' : state === 'unavailable' ? '未接続' : '未確認';
539
+ card.append(makeElement(doc, 'small', { className: 'bb-pkw-muted', text: stateLabel }));
540
+ if (typeof actions.openSectionList === 'function') card.append(denseButton(doc, '一覧を見る', 'bb-pkw-dense-action', () => actions.openSectionList(id), { 'aria-label': `${title}の一覧を見る` }));
541
+ grid.append(card);
542
+ }
543
+ panel.append(grid);
544
+ return panel;
545
+ }
546
+
523
547
  function denseDate(value) {
524
548
  const text = textOrNull(value);
525
549
  if (!text) return '未確認';
@@ -569,14 +593,14 @@ function denseContextStateNotice(doc, section, fallback = 'この情報はまだ
569
593
  return workspaceNotice(doc, { label, text: section.message ?? fallback, tone, role: section.state === 'failed' ? 'alert' : 'status' });
570
594
  }
571
595
 
572
- function renderDenseItemRow(doc, item, { kind = 'task', onOpen, table = false } = {}) {
596
+ function renderDenseItemRow(doc, item, { kind = 'task', onOpen, table = false, includeSummary = true } = {}) {
573
597
  const row = makeElement(doc, 'li', { className: `bb-pkw-dense-item bb-pkw-dense-item-${kind}` });
574
598
  const head = makeElement(doc, 'div', { className: 'bb-pkw-dense-item-head' });
575
599
  const title = cleanName(item.title, item.name) ?? '名前不明';
576
600
  if (typeof onOpen === 'function' && item.id) head.append(denseButton(doc, title, 'bb-pkw-dense-link', () => onOpen(item.id), { 'data-item-id': item.id }));
577
601
  else head.append(makeElement(doc, 'strong', { text: title }));
578
602
  if (item.status) head.append(badge(doc, item.status, 'muted'));
579
- row.append(head, denseItemFacts(doc, item, { table, includeSummary: !table }));
603
+ row.append(head, denseItemFacts(doc, item, { table, includeSummary: !table && includeSummary }));
580
604
  return row;
581
605
  }
582
606
 
@@ -611,10 +635,10 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
611
635
  const tasks = contextTaskItems(context);
612
636
  const records = contextRecordItems(context);
613
637
  const graphRecords = [...(entities?.values?.() ?? [])].filter((entity) => !['project', 'person'].includes(entity.type));
614
- const participants = [...new Map((payload?.participants ?? [])
615
- .map((edge) => edge?.counterpart)
616
- .filter((person) => person?.id && (person.name || person.title))
617
- .map((person) => [person.id, person])).values()].slice(0, 6);
638
+ const participantEdges = [...new Map((payload?.participants ?? [])
639
+ .filter((edge) => edge?.counterpart?.id && (edge.counterpart.name || edge.counterpart.title))
640
+ .map((edge) => [edge.counterpart.id, edge])).values()];
641
+ const participants = participantEdges.slice(0, 6);
618
642
  const recentRecords = records.length > 0 ? sortDenseItems(records).slice(0, 4) : graphRecords.slice(0, 4);
619
643
  const updated = [...tasks, ...records]
620
644
  .filter((item) => item.updatedAt || item.eventAt)
@@ -633,7 +657,7 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
633
657
  denseMetric(doc, '仕事', taskMetric, taskNote),
634
658
  denseMetric(doc, '今日扱う仕事', taskSection?.state === 'ok' ? `${due.length}件` : '未確認', taskSection?.state === 'ok' ? '期限・更新が確認できる順' : 'タスクの状態を確認中'),
635
659
  denseMetric(doc, '前回からの変化', updated.length > 0 ? `${updated.length}件` : '未確認', updated.length > 0 ? '更新時点がある記録' : '更新時点が未確認'),
636
- denseMetric(doc, '関係者', Array.isArray(payload?.participants) && payload.participants.length > 0 ? `${payload.participants.length}人` : '未確認', 'Graphに記録された参加関係'),
660
+ denseMetric(doc, '関係者', participantEdges.length > 0 ? `${participantEdges.length}人` : '未確認', 'Graphに記録された参加関係'),
637
661
  );
638
662
  panel.append(metrics);
639
663
 
@@ -688,10 +712,13 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
688
712
  if (participants.length === 0) people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: 'Graphで確認できる関係者は未確認です。' }));
689
713
  else {
690
714
  const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list' });
691
- for (const person of participants) {
715
+ for (const edge of participants) {
716
+ const person = edge.counterpart;
692
717
  const row = makeElement(doc, 'li', { className: 'bb-pkw-dense-item' });
693
- row.append(makeElement(doc, 'strong', { text: cleanName(person.name, person.title) }));
694
- if (person.role) row.append(makeElement(doc, 'span', { className: 'bb-pkw-muted', text: person.role }));
718
+ row.append(recordLink(doc, cleanName(person.name, person.title), person.id, actions.openGraphEntity, 'bb-pkw-dense-link'));
719
+ row.append(badge(doc, relationLabel(edge.relation), edge.relation === 'accountable_for' ? 'accent' : 'muted'));
720
+ const facts = [textOrNull(edge.role) ? `役割: ${edge.role}` : null, textOrNull(edge.context) ? `文脈: ${edge.context}` : null].filter(Boolean);
721
+ if (facts.length > 0) row.append(makeElement(doc, 'span', { className: 'bb-pkw-muted', text: facts.join(' · ') }));
695
722
  list.append(row);
696
723
  }
697
724
  people.append(list);
@@ -701,7 +728,7 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
701
728
  return panel;
702
729
  }
703
730
 
704
- function renderWorkList(doc, context, actions = {}) {
731
+ function renderWorkList(doc, context, actions = {}, state = {}) {
705
732
  const section = contextTaskSection(context);
706
733
  const allItems = contextTaskItems(context);
707
734
  const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-work-list', attrs: { role: 'tabpanel', 'aria-label': '仕事一覧' } });
@@ -726,10 +753,19 @@ function renderWorkList(doc, context, actions = {}) {
726
753
  makeElement(doc, 'option', { text: '期限未確認', attrs: { value: 'missing' } }),
727
754
  );
728
755
  controls.append(search, status, owner, due);
756
+ search.value = state.query ?? '';
757
+ status.value = state.status ?? '';
758
+ owner.value = state.owner ?? '';
759
+ due.value = state.due ?? '';
729
760
  panel.append(controls);
730
761
  const count = makeElement(doc, 'p', { className: 'bb-pkw-work-count' });
731
762
  const rows = makeElement(doc, 'div', { className: 'bb-pkw-work-rows', attrs: { role: 'list', 'aria-label': '仕事の行' } });
732
- panel.append(count, rows);
763
+ const pager = makeElement(doc, 'nav', { className: 'bb-pkw-list-pager', attrs: { 'aria-label': '仕事一覧のページ' } });
764
+ const previous = denseButton(doc, '前へ', 'bb-pkw-dense-action', () => { state.page -= 1; renderRows(); });
765
+ const next = denseButton(doc, '次へ', 'bb-pkw-dense-action', () => { state.page += 1; renderRows(); });
766
+ const pageLabel = makeElement(doc, 'span', { className: 'bb-pkw-muted' });
767
+ pager.append(previous, pageLabel, next);
768
+ panel.append(count, rows, pager);
733
769
  const renderRows = () => {
734
770
  const query = String(search.value ?? '').trim().toLocaleLowerCase();
735
771
  const selectedStatus = String(status.value ?? '');
@@ -744,8 +780,14 @@ function renderWorkList(doc, context, actions = {}) {
744
780
  return [item.title, item.summary, item.owner, item.assignee, item.status].filter(Boolean).some((value) => String(value).toLocaleLowerCase().includes(query));
745
781
  });
746
782
  const total = explicitSectionTotal(section);
747
- const display = visible.slice(0, 16);
748
- count.textContent = `表示 ${display.length}件${display.length < visible.length ? `(条件一致 ${visible.length}件の先頭)` : ''} / 総数 ${total === null ? '未確認' : `${total}件`}`;
783
+ state.page = Math.max(0, Math.min(state.page ?? 0, Math.max(0, Math.ceil(visible.length / 16) - 1)));
784
+ const display = visible.slice(state.page * 16, (state.page + 1) * 16);
785
+ const start = display.length > 0 ? state.page * 16 + 1 : 0;
786
+ const end = state.page * 16 + display.length;
787
+ count.textContent = `取得済み ${allItems.length}件 · 条件一致 ${visible.length}件 · 表示 ${start}–${end}件 · 総数 ${section?.state === 'ok' && total !== null ? `${total}件${total > allItems.length ? '(続き未取得)' : ''}` : '未確認'}`;
788
+ pageLabel.textContent = `${state.page + 1}ページ目`;
789
+ previous.disabled = state.page === 0;
790
+ next.disabled = (state.page + 1) * 16 >= visible.length;
749
791
  rows.replaceChildren();
750
792
  if (display.length === 0) {
751
793
  rows.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: section?.state === 'ok' ? '条件に一致する仕事はありません。' : '仕事の行は未確認です。' }));
@@ -760,10 +802,71 @@ function renderWorkList(doc, context, actions = {}) {
760
802
  rows.append(row);
761
803
  }
762
804
  };
763
- search.addEventListener('input', renderRows);
764
- status.addEventListener('change', renderRows);
765
- owner.addEventListener('change', renderRows);
766
- due.addEventListener('change', renderRows);
805
+ const resetRows = () => {
806
+ Object.assign(state, { query: search.value, status: status.value, owner: owner.value, due: due.value, page: 0 });
807
+ renderRows();
808
+ };
809
+ search.addEventListener('input', resetRows);
810
+ status.addEventListener('change', resetRows);
811
+ owner.addEventListener('change', resetRows);
812
+ due.addEventListener('change', resetRows);
813
+ renderRows();
814
+ return panel;
815
+ }
816
+
817
+ function renderRecordList(doc, context, sectionId, actions = {}, state = {}) {
818
+ const section = contextSectionById(context, [sectionId]);
819
+ const title = CONTEXT_CATEGORIES.find(([id]) => id === sectionId)?.[1] ?? section?.title ?? '記録';
820
+ const items = Array.isArray(section?.items) ? section.items : [];
821
+ const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-record-list', attrs: { role: 'tabpanel', 'aria-label': `${title}一覧` } });
822
+ const heading = makeElement(doc, 'div', { className: 'bb-pkw-route-heading' });
823
+ heading.append(denseButton(doc, '← 概要', 'bb-pkw-back', actions.openOverview), makeElement(doc, 'h3', { text: `${title}一覧` }));
824
+ panel.append(heading);
825
+ const notice = denseContextStateNotice(doc, section ?? (context?.state !== 'ok' ? context : { state: 'unknown' }), 'この区分を読み取れませんでした。');
826
+ if (notice) panel.append(notice);
827
+ const controls = makeElement(doc, 'div', { className: 'bb-pkw-work-controls', attrs: { 'aria-label': `${title}の絞り込み` } });
828
+ const search = makeElement(doc, 'input', { className: 'bb-pkw-work-search', attrs: { type: 'search', name: 'record-search', placeholder: `${title}を検索`, 'aria-label': `${title}を検索` } });
829
+ const status = makeElement(doc, 'select', { className: 'bb-pkw-work-status', attrs: { name: 'record-status', 'aria-label': '状態で絞り込む' } });
830
+ status.append(makeElement(doc, 'option', { text: 'すべての状態', attrs: { value: '' } }));
831
+ for (const value of new Set(items.map((item) => item.status).filter(Boolean))) status.append(makeElement(doc, 'option', { text: value, attrs: { value } }));
832
+ controls.append(search, status);
833
+ search.value = state.query ?? '';
834
+ status.value = state.status ?? '';
835
+ panel.append(controls);
836
+ const count = makeElement(doc, 'p', { className: 'bb-pkw-work-count' });
837
+ const rows = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list', attrs: { 'aria-label': `${title}の行` } });
838
+ const pager = makeElement(doc, 'nav', { className: 'bb-pkw-list-pager', attrs: { 'aria-label': `${title}一覧のページ` } });
839
+ const previous = denseButton(doc, '前へ', 'bb-pkw-dense-action', () => { state.page -= 1; renderRows(); });
840
+ const next = denseButton(doc, '次へ', 'bb-pkw-dense-action', () => { state.page += 1; renderRows(); });
841
+ const pageLabel = makeElement(doc, 'span', { className: 'bb-pkw-muted' });
842
+ pager.append(previous, pageLabel, next);
843
+ panel.append(count, rows, pager);
844
+ const renderRows = () => {
845
+ const query = String(search.value ?? '').trim().toLocaleLowerCase();
846
+ const selectedStatus = String(status.value ?? '');
847
+ const visible = items.filter((item) => {
848
+ if (selectedStatus && item.status !== selectedStatus) return false;
849
+ return !query || [item.title, item.summary, item.status, item.owner, item.body].filter(Boolean).some((value) => String(value).toLocaleLowerCase().includes(query));
850
+ });
851
+ state.page = Math.max(0, Math.min(state.page ?? 0, Math.max(0, Math.ceil(visible.length / 16) - 1)));
852
+ const display = visible.slice(state.page * 16, (state.page + 1) * 16);
853
+ const start = display.length > 0 ? state.page * 16 + 1 : 0;
854
+ const end = state.page * 16 + display.length;
855
+ const total = explicitSectionTotal(section);
856
+ count.textContent = `取得済み ${items.length}件 · 条件一致 ${visible.length}件 · 表示 ${start}–${end}件 · 総数 ${section?.state === 'ok' && total !== null ? `${total}件${total > items.length ? '(続き未取得)' : ''}` : '未確認'}`;
857
+ pageLabel.textContent = `${state.page + 1}ページ目`;
858
+ previous.disabled = state.page === 0;
859
+ next.disabled = (state.page + 1) * 16 >= visible.length;
860
+ rows.replaceChildren();
861
+ if (visible.length === 0) {
862
+ rows.append(makeElement(doc, 'li', { className: 'bb-pkw-muted', text: section?.state === 'ok' ? '条件に一致する記録はありません。' : '記録の行は未確認です。' }));
863
+ return;
864
+ }
865
+ for (const item of display) rows.append(renderDenseItemRow(doc, item, { kind: 'record', onOpen: actions.openRecordDetail, includeSummary: false }));
866
+ };
867
+ const resetRows = () => { Object.assign(state, { query: search.value, status: status.value, page: 0 }); renderRows(); };
868
+ search.addEventListener('input', resetRows);
869
+ status.addEventListener('change', resetRows);
767
870
  renderRows();
768
871
  return panel;
769
872
  }
@@ -824,7 +927,7 @@ function renderWorkDetail(doc, item, context, actions = {}) {
824
927
  function renderRecordDetail(doc, record, context, actions = {}) {
825
928
  const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-detail', attrs: { role: 'tabpanel', 'aria-label': '記録詳細' } });
826
929
  const heading = makeElement(doc, 'div', { className: 'bb-pkw-route-heading' });
827
- heading.append(denseButton(doc, '← 概要', 'bb-pkw-back', actions.openOverview), makeElement(doc, 'div', { className: 'bb-pkw-route-title' }));
930
+ heading.append(denseButton(doc, actions.recordSectionTitle ? `← ${actions.recordSectionTitle}一覧` : '← 概要', 'bb-pkw-back', actions.recordSectionTitle ? () => actions.openSectionList?.(actions.recordSectionId) : actions.openOverview), makeElement(doc, 'div', { className: 'bb-pkw-route-title' }));
828
931
  heading.children[1].append(makeElement(doc, 'h3', { text: cleanName(record?.title, record?.name) ?? '記録詳細' }), record?.kind || record?.type ? badge(doc, typeLabel(record.kind ?? record.type), 'accent') : null);
829
932
  panel.append(heading);
830
933
  if (!record) {
@@ -859,12 +962,12 @@ function renderRecordDetail(doc, record, context, actions = {}) {
859
962
  return panel;
860
963
  }
861
964
 
862
- function renderOverview(doc, detail, context, edges, entities, onSelect, onContext, actions = {}) {
965
+ function renderOverview(doc, detail, context, entities, actions = {}) {
863
966
  const payload = projectDetailPayload(detail);
864
967
  const project = payload?.project;
865
968
  const panel = makeElement(doc, 'div', { className: 'bb-pkw-panel bb-pkw-overview', attrs: { role: 'tabpanel', 'aria-label': '概要' } });
866
969
  panel.append(renderDenseOverview(doc, detail, context, entities, actions));
867
- const columns = makeElement(doc, 'div', { className: 'bb-pkw-columns' });
970
+ panel.append(renderContextDirectory(doc, context, actions));
868
971
  const summary = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '目的と現在地' } });
869
972
  summary.append(makeElement(doc, 'h3', { text: '目的と現在地' }), workspaceDefinition(doc, [
870
973
  ['目的', textOrNull(project?.goal)],
@@ -873,83 +976,7 @@ function renderOverview(doc, detail, context, edges, entities, onSelect, onConte
873
976
  ['出典', sourceLabel(payload?.source)],
874
977
  ['読み取った時点', payload?.asOf ? formatDateTime(payload.asOf) : null],
875
978
  ]));
876
- columns.append(summary);
877
-
878
- const people = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '関係者' } });
879
- people.append(makeElement(doc, 'h3', { text: '関係者と役割' }));
880
- const participantEdges = Array.isArray(payload?.participants) ? payload.participants : [];
881
- if (participantEdges.length === 0) {
882
- people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '関係者の記録はありません。' }));
883
- } else {
884
- const list = makeElement(doc, 'ul', { className: 'bb-pkw-people-list' });
885
- for (const edge of participantEdges) {
886
- const item = makeElement(doc, 'li');
887
- const counterpart = edge.counterpart ?? {};
888
- item.append(recordLink(doc, cleanName(counterpart.name, counterpart.id) ?? '名前不明', cleanId(counterpart.id), onSelect), badge(doc, relationLabel(edge.relation), edge.relation === 'accountable_for' ? 'accent' : 'muted'));
889
- const facts = [textOrNull(edge.role) ? `役割: ${edge.role}` : null, textOrNull(edge.context) ? `文脈: ${edge.context}` : null].filter(Boolean);
890
- if (facts.length > 0) item.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: facts.join(' · ') }));
891
- list.append(item);
892
- }
893
- people.append(list);
894
- }
895
- columns.append(people);
896
- panel.append(columns);
897
-
898
- const contextBlock = renderProjectContext(doc, context, { onTaskOpen: actions.openWorkDetail });
899
- if (contextBlock) {
900
- panel.append(contextBlock);
901
- if (typeof onContext === 'function') onContext(contextBlock, null);
902
- } else {
903
- const contextAnchor = projectContextInsertionAnchor(doc);
904
- panel.append(contextAnchor);
905
- if (typeof onContext === 'function') onContext(null, contextAnchor);
906
- }
907
-
908
- const decisions = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '記録された判断' } });
909
- decisions.append(makeElement(doc, 'h3', { text: '記録された判断' }));
910
- const decisionEdges = edges.filter((edge) => entities.get(edge.source)?.type === 'decision' || entities.get(edge.target)?.type === 'decision');
911
- if (decisionEdges.length === 0) decisions.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: 'このプロジェクトに結びつく判断の記録はありません。' }));
912
- else {
913
- const list = makeElement(doc, 'ul', { className: 'bb-pkw-decision-list' });
914
- for (const edge of decisionEdges) {
915
- const decisionId = entities.get(edge.source)?.type === 'decision' ? edge.source : edge.target;
916
- const decision = entities.get(decisionId);
917
- const item = makeElement(doc, 'li');
918
- item.append(recordLink(doc, decision?.name ?? decisionId, decisionId, onSelect), badge(doc, relationLabel(edge.relation), 'muted'));
919
- if (decision?.summary) item.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: decision.summary }));
920
- list.append(item);
921
- }
922
- decisions.append(list);
923
- }
924
- panel.append(decisions);
925
-
926
- const objects = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '関連する情報' } });
927
- objects.append(makeElement(doc, 'h3', { text: '関連する情報' }));
928
- const groups = new Map();
929
- for (const entity of entities.values()) {
930
- if (entity.id === project?.id || entity.type === 'person' || entity.type === 'decision') continue;
931
- if (!groups.has(entity.type)) groups.set(entity.type, []);
932
- groups.get(entity.type).push(entity);
933
- }
934
- if (groups.size === 0) objects.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '関連する情報の記録はありません。' }));
935
- else {
936
- const grid = makeElement(doc, 'div', { className: 'bb-pkw-info-grid' });
937
- for (const [type, records] of groups) {
938
- const group = makeElement(doc, 'section', { className: 'bb-pkw-object', attrs: { 'aria-label': typeLabel(type) } });
939
- group.append(makeElement(doc, 'h4', { text: typeLabel(type) }));
940
- const list = makeElement(doc, 'ul');
941
- for (const record of records) {
942
- const item = makeElement(doc, 'li');
943
- item.append(recordLink(doc, record.name, record.id, onSelect));
944
- list.append(item);
945
- }
946
- group.append(list);
947
- grid.append(group);
948
- }
949
- objects.append(grid);
950
- }
951
- panel.append(objects);
952
-
979
+ panel.append(summary);
953
980
  const unknowns = projectKnowledgeUnknowns(detail);
954
981
  if (unknowns.length > 0) {
955
982
  const missing = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-unknown', attrs: { 'aria-label': '未記録・未確認' } });
@@ -1000,13 +1027,16 @@ export function createProjectKnowledgeWorkspace({
1000
1027
  let currentContext = context;
1001
1028
  let activeView = 'overview';
1002
1029
  let selectedContextId = null;
1030
+ let selectedSectionId = null;
1031
+ const listViewStates = new Map();
1032
+ const listViewState = (id) => {
1033
+ if (!listViewStates.has(id)) listViewStates.set(id, {});
1034
+ return listViewStates.get(id);
1035
+ };
1003
1036
  // Keep direct references as well as the DOM lookup. The local test harness
1004
1037
  // intentionally has no querySelector implementation, and a host can replace
1005
1038
  // the panel while a detail read is still in flight.
1006
1039
  let selectedPanel = null;
1007
- let overviewPanel = null;
1008
- let contextBlock = null;
1009
- let contextAnchor = null;
1010
1040
 
1011
1041
  const root = makeElement(doc, 'section', {
1012
1042
  className: 'bb-pkw',
@@ -1052,6 +1082,7 @@ export function createProjectKnowledgeWorkspace({
1052
1082
  activeTab = 'overview';
1053
1083
  activeView = 'overview';
1054
1084
  selectedContextId = null;
1085
+ selectedSectionId = null;
1055
1086
  render();
1056
1087
  }
1057
1088
 
@@ -1060,6 +1091,17 @@ export function createProjectKnowledgeWorkspace({
1060
1091
  activeTab = 'overview';
1061
1092
  activeView = 'work-list';
1062
1093
  selectedContextId = null;
1094
+ selectedSectionId = 'tasks';
1095
+ render();
1096
+ }
1097
+
1098
+ function openSectionList(id) {
1099
+ if (destroyed) return;
1100
+ if (id === 'tasks') return openWorkList();
1101
+ activeTab = 'overview';
1102
+ activeView = 'record-list';
1103
+ selectedSectionId = id;
1104
+ selectedContextId = null;
1063
1105
  render();
1064
1106
  }
1065
1107
 
@@ -1076,6 +1118,8 @@ export function createProjectKnowledgeWorkspace({
1076
1118
  activeTab = 'overview';
1077
1119
  activeView = 'record-detail';
1078
1120
  selectedContextId = cleanId(id);
1121
+ const sourceSection = currentContext?.sections?.find((section) => section.id !== contextTaskSection(currentContext)?.id && section.items?.some((item) => item.id === selectedContextId));
1122
+ selectedSectionId = sourceSection?.id ?? null;
1079
1123
  render();
1080
1124
  }
1081
1125
 
@@ -1290,39 +1334,17 @@ export function createProjectKnowledgeWorkspace({
1290
1334
 
1291
1335
  function replaceContextBlock(nextContext) {
1292
1336
  currentContext = nextContext;
1293
- if (destroyed) return;
1294
- if (activeTab === 'overview' && activeView !== 'overview') {
1295
- render();
1296
- return;
1297
- }
1298
- const nextBlock = renderProjectContext(doc, currentContext, { onTaskOpen: openWorkDetail });
1299
- if (!nextBlock) {
1300
- if (contextBlock?.parentNode && typeof contextBlock.parentNode.replaceChild === 'function') {
1301
- const nextAnchor = projectContextInsertionAnchor(doc);
1302
- contextBlock.parentNode.replaceChild(nextAnchor, contextBlock);
1303
- contextBlock = null;
1304
- contextAnchor = nextAnchor;
1305
- } else if (contextBlock?.parentNode?.removeChild) {
1306
- contextBlock.parentNode.removeChild(contextBlock);
1307
- contextBlock = null;
1308
- }
1309
- return;
1310
- }
1311
- if (contextBlock?.parentNode && typeof contextBlock.parentNode.replaceChild === 'function') {
1312
- contextBlock.parentNode.replaceChild(nextBlock, contextBlock);
1313
- contextBlock = nextBlock;
1314
- return;
1315
- }
1316
- if (contextAnchor?.parentNode && typeof contextAnchor.parentNode.replaceChild === 'function') {
1317
- contextAnchor.parentNode.replaceChild(nextBlock, contextAnchor);
1318
- contextBlock = nextBlock;
1319
- contextAnchor = null;
1320
- return;
1321
- }
1322
- if (contextBlock?.replaceWith) {
1323
- contextBlock.replaceWith(nextBlock);
1324
- contextBlock = nextBlock;
1325
- return;
1337
+ if (destroyed || activeTab !== 'overview') return;
1338
+ const focused = doc.activeElement;
1339
+ const controlName = root.contains?.(focused) && focused?.closest?.('.bb-pkw-work-controls') ? focused.getAttribute?.('name') : null;
1340
+ const restoreFocus = ['work-search', 'work-status', 'work-owner', 'work-due', 'record-search', 'record-status'].includes(controlName);
1341
+ const selectionStart = restoreFocus && focused?.tagName === 'INPUT' ? focused.selectionStart : null;
1342
+ const selectionEnd = restoreFocus && focused?.tagName === 'INPUT' ? focused.selectionEnd : null;
1343
+ render();
1344
+ if (restoreFocus) {
1345
+ const replacement = root.querySelector?.(`[name="${controlName}"]`);
1346
+ replacement?.focus?.();
1347
+ if (selectionStart !== null && selectionEnd !== null && typeof replacement?.setSelectionRange === 'function') replacement.setSelectionRange(selectionStart, selectionEnd);
1326
1348
  }
1327
1349
  }
1328
1350
 
@@ -1332,20 +1354,18 @@ export function createProjectKnowledgeWorkspace({
1332
1354
  teardownGraph();
1333
1355
  tabs.replaceChildren(tabButton('概要', 'overview'), tabButton('情報を探す', 'graph'));
1334
1356
  panels.replaceChildren();
1335
- contextBlock = null;
1336
- contextAnchor = null;
1337
- const actions = { openOverview, openWorkList, openWorkDetail, openRecordDetail };
1357
+ const actions = { openOverview, openWorkList, openWorkDetail, openRecordDetail, openSectionList, openGraphEntity: selectAndExplore,
1358
+ recordSectionId: selectedSectionId,
1359
+ recordSectionTitle: CONTEXT_CATEGORIES.find(([id]) => id === selectedSectionId)?.[1] ?? contextSectionById(currentContext, [selectedSectionId])?.title,
1360
+ };
1338
1361
  let overview;
1339
- if (activeView === 'work-list') overview = renderWorkList(doc, currentContext, actions);
1362
+ if (activeView === 'work-list') overview = renderWorkList(doc, currentContext, actions, listViewState('tasks'));
1363
+ else if (activeView === 'record-list') overview = renderRecordList(doc, currentContext, selectedSectionId, actions, listViewState(selectedSectionId));
1340
1364
  else if (activeView === 'work-detail') overview = renderWorkDetail(doc, findContextItem(selectedContextId, 'task'), currentContext, actions);
1341
1365
  else if (activeView === 'record-detail') overview = renderRecordDetail(doc, findContextItem(selectedContextId, 'record') ?? entities.get(selectedContextId), currentContext, actions);
1342
- else overview = renderOverview(doc, payload, currentContext, overviewEdges, overviewEntities, selectAndExplore, (node, anchor) => {
1343
- contextBlock = node;
1344
- contextAnchor = anchor;
1345
- }, actions);
1366
+ else overview = renderOverview(doc, payload, currentContext, overviewEntities, actions);
1346
1367
  overview.id = 'bb-pkw-panel-overview';
1347
1368
  overview.hidden = activeTab !== 'overview';
1348
- overviewPanel = overview;
1349
1369
  panels.append(overview);
1350
1370
  const graph = renderGraphPanel();
1351
1371
  graph.hidden = activeTab !== 'graph';