@unson/brainbase-mcp 0.10.5 → 0.10.7

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.5",
3
+ "version": "0.10.7",
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": "8cfad04bef7bf934165d96b979e9e5d52dd837dc"
339
+ "gitHead": "9b53aa137dac5126a8bd43b7a58b21b135244c88"
340
340
  }
@@ -27,6 +27,12 @@
27
27
  --bb-color-action-hover: #2a5540;
28
28
  --bb-color-action-text: #ffffff;
29
29
  --bb-color-focus: #20a37e;
30
+ --bb-color-project-ink: #172d4f;
31
+ --bb-color-project-muted: #566b87;
32
+ --bb-color-project-border: #dce4ee;
33
+ --bb-color-project-border-strong: #c8d4e2;
34
+ --bb-color-project-accent: #1261ad;
35
+ --bb-color-project-accent-strong: #194f8a;
30
36
  --bb-color-success: #087d62;
31
37
  --bb-color-success-soft: #e6f2ec;
32
38
  --bb-color-warning: #8a631d;
@@ -250,6 +250,7 @@ export function createGraphProjectsView({
250
250
  emptyNotice,
251
251
  pageActions,
252
252
  extraMetrics,
253
+ showProjectLedger = true,
253
254
  renderRailExtensions,
254
255
  selectedId: initialSelectedId,
255
256
  onSelect,
@@ -595,7 +596,7 @@ export function createGraphProjectsView({
595
596
  return children;
596
597
  }
597
598
 
598
- children.push(workspaceLedger(doc, {
599
+ if (showProjectLedger) children.push(workspaceLedger(doc, {
599
600
  className: 'bb-graph-project-ledger',
600
601
  ariaLabel: 'プロジェクトの一覧',
601
602
  columns: GRAPH_PROJECT_LEDGER_COLUMNS,
@@ -1,6 +1,13 @@
1
1
  /* Shared project knowledge workspace. Load after graph-view-shared.css. */
2
2
 
3
3
  .bb-pkw {
4
+ --bb-color-ink: var(--bb-color-project-ink);
5
+ --bb-color-muted: var(--bb-color-project-muted);
6
+ --bb-color-border: var(--bb-color-project-border);
7
+ --bb-color-border-strong: var(--bb-color-project-border-strong);
8
+ --bb-color-surface: var(--bb-color-panel);
9
+ --bb-color-accent: var(--bb-color-project-accent);
10
+ --bb-color-accent-strong: var(--bb-color-project-accent-strong);
4
11
  display: grid;
5
12
  gap: var(--bb-space-4);
6
13
  min-width: 0;
@@ -527,3 +534,384 @@
527
534
  font-size: 11px;
528
535
  line-height: 1.5;
529
536
  }
537
+
538
+ /* Dense project routes keep the first screen useful when the host supplies
539
+ many records. The overview carries only quantities and the next useful
540
+ entry point; rows and details carry the long form. */
541
+ .bb-pkw-dense-overview {
542
+ display: grid;
543
+ gap: var(--bb-space-4);
544
+ }
545
+
546
+ .bb-pkw-dense-intro {
547
+ display: none;
548
+ }
549
+
550
+ .bb-pkw-dense-eyebrow {
551
+ color: var(--bb-color-eyebrow);
552
+ font-size: 11px;
553
+ font-weight: 760;
554
+ letter-spacing: .08em;
555
+ }
556
+
557
+ .bb-pkw-dense-intro h3,
558
+ .bb-pkw-dense-card h4 {
559
+ margin: 0;
560
+ color: var(--bb-color-ink);
561
+ }
562
+
563
+ .bb-pkw-dense-intro h3 {
564
+ font-size: clamp(18px, 2vw, 24px);
565
+ letter-spacing: -.03em;
566
+ overflow-wrap: anywhere;
567
+ }
568
+
569
+ .bb-pkw-dense-metrics {
570
+ display: grid;
571
+ grid-template-columns: repeat(4, minmax(0, 1fr));
572
+ gap: var(--bb-space-2);
573
+ }
574
+
575
+ .bb-pkw-dense-metric {
576
+ display: grid;
577
+ gap: 3px;
578
+ min-width: 0;
579
+ border: 1px solid var(--bb-color-border);
580
+ border-radius: var(--bb-radius-sm);
581
+ padding: 8px 12px;
582
+ background: var(--bb-color-panel);
583
+ }
584
+
585
+ .bb-pkw-dense-metric-label {
586
+ color: var(--bb-color-muted);
587
+ font-size: 11px;
588
+ font-weight: 720;
589
+ }
590
+
591
+ .bb-pkw-dense-metric-value {
592
+ min-width: 0;
593
+ overflow-wrap: anywhere;
594
+ color: var(--bb-color-ink);
595
+ font-size: 20px;
596
+ letter-spacing: -.03em;
597
+ }
598
+
599
+ .bb-pkw-dense-grid {
600
+ display: grid;
601
+ grid-template-columns: minmax(0, 1fr);
602
+ gap: var(--bb-space-3);
603
+ }
604
+
605
+ .bb-pkw-dense-supporting-grid {
606
+ grid-template-columns: repeat(2, minmax(0, 1fr));
607
+ }
608
+
609
+ .bb-pkw-dense-card {
610
+ display: grid;
611
+ gap: 6px;
612
+ min-width: 0;
613
+ border: 1px solid var(--bb-color-border);
614
+ border-radius: var(--bb-radius-sm);
615
+ padding: 10px 14px;
616
+ background: var(--bb-color-panel);
617
+ }
618
+
619
+ .bb-pkw-dense-card-wide {
620
+ grid-column: 1 / -1;
621
+ }
622
+
623
+ .bb-pkw-dense-card-head,
624
+ .bb-pkw-dense-item-head,
625
+ .bb-pkw-route-heading {
626
+ display: flex;
627
+ flex-wrap: wrap;
628
+ gap: var(--bb-space-2);
629
+ align-items: center;
630
+ justify-content: space-between;
631
+ min-width: 0;
632
+ }
633
+
634
+ .bb-pkw-dense-action,
635
+ .bb-pkw-back {
636
+ border: 0;
637
+ padding: 0;
638
+ color: var(--bb-color-accent-strong);
639
+ background: transparent;
640
+ font: inherit;
641
+ font-size: 12px;
642
+ font-weight: 720;
643
+ cursor: pointer;
644
+ }
645
+
646
+ .bb-pkw-dense-action:hover,
647
+ .bb-pkw-back:hover {
648
+ color: var(--bb-color-accent);
649
+ text-decoration: underline;
650
+ text-underline-offset: 3px;
651
+ }
652
+
653
+ .bb-pkw-dense-action:focus-visible,
654
+ .bb-pkw-back:focus-visible,
655
+ .bb-pkw-dense-link:focus-visible {
656
+ outline: 3px solid color-mix(in srgb, var(--bb-color-focus) 45%, var(--bb-color-panel));
657
+ outline-offset: 2px;
658
+ }
659
+
660
+ .bb-pkw-dense-list,
661
+ .bb-pkw-dense-record-list {
662
+ display: grid;
663
+ gap: var(--bb-space-2);
664
+ margin: 0;
665
+ padding: 0;
666
+ list-style: none;
667
+ }
668
+
669
+ .bb-pkw-dense-item {
670
+ display: grid;
671
+ gap: 4px;
672
+ min-width: 0;
673
+ border-bottom: 1px solid var(--bb-color-border);
674
+ padding-bottom: var(--bb-space-2);
675
+ }
676
+
677
+ .bb-pkw-dense-item:last-child {
678
+ border-bottom: 0;
679
+ padding-bottom: 0;
680
+ }
681
+
682
+ .bb-pkw-dense-item-head {
683
+ justify-content: flex-start;
684
+ }
685
+
686
+ .bb-pkw-dense-link {
687
+ max-width: 100%;
688
+ overflow-wrap: anywhere;
689
+ border: 0;
690
+ padding: 0;
691
+ color: var(--bb-color-accent-strong);
692
+ background: transparent;
693
+ font: inherit;
694
+ font-weight: 720;
695
+ text-align: left;
696
+ text-decoration: underline;
697
+ text-decoration-color: color-mix(in srgb, var(--bb-color-accent) 35%, transparent);
698
+ text-underline-offset: 3px;
699
+ cursor: pointer;
700
+ }
701
+
702
+ .bb-pkw-dense-link:hover {
703
+ color: var(--bb-color-accent);
704
+ text-decoration-color: currentColor;
705
+ }
706
+
707
+ .bb-pkw-dense-item-facts {
708
+ display: flex;
709
+ flex-wrap: wrap;
710
+ gap: 4px var(--bb-space-3);
711
+ min-width: 0;
712
+ color: var(--bb-color-muted);
713
+ font-size: 11px;
714
+ line-height: 1.5;
715
+ }
716
+
717
+ .bb-pkw-dense-table {
718
+ gap: 0;
719
+ overflow-x: auto;
720
+ }
721
+
722
+ .bb-pkw-dense-table-header {
723
+ display: grid;
724
+ grid-template-columns: minmax(220px, 2fr) repeat(4, minmax(94px, 1fr));
725
+ gap: var(--bb-space-2);
726
+ min-width: 0;
727
+ border-bottom: 1px solid var(--bb-color-border-strong);
728
+ padding: 0 0 var(--bb-space-2);
729
+ color: var(--bb-color-muted);
730
+ font-size: 11px;
731
+ font-weight: 720;
732
+ }
733
+
734
+ .bb-pkw-dense-table .bb-pkw-dense-item {
735
+ display: grid;
736
+ grid-template-columns: minmax(220px, 2fr) repeat(4, minmax(94px, 1fr));
737
+ gap: var(--bb-space-2);
738
+ align-items: center;
739
+ min-width: 0;
740
+ border-bottom: 1px solid var(--bb-color-border);
741
+ padding: 5px 0;
742
+ }
743
+
744
+ .bb-pkw-dense-table .bb-pkw-dense-item-head {
745
+ min-width: 0;
746
+ }
747
+
748
+ .bb-pkw-dense-table .bb-pkw-dense-item-facts {
749
+ display: contents;
750
+ }
751
+
752
+ .bb-pkw-dense-table .bb-pkw-dense-fact,
753
+ .bb-pkw-dense-table .bb-pkw-dense-item-facts > .bb-pkw-muted {
754
+ min-width: 0;
755
+ overflow-wrap: anywhere;
756
+ color: var(--bb-color-muted);
757
+ font-size: 11px;
758
+ }
759
+
760
+ .bb-pkw-dense-table .bb-pkw-dense-item-facts > .bb-pkw-muted {
761
+ grid-column: 1 / -1;
762
+ }
763
+
764
+ .bb-pkw-dense-table .bb-pkw-dense-item:last-child {
765
+ border-bottom: 0;
766
+ }
767
+
768
+ .bb-pkw-dense-change-table .bb-pkw-dense-item {
769
+ grid-template-columns: minmax(220px, 2fr) repeat(4, minmax(80px, 1fr));
770
+ }
771
+
772
+ .bb-pkw-dense-supporting-grid {
773
+ align-items: start;
774
+ }
775
+
776
+ .bb-pkw-route {
777
+ display: grid;
778
+ gap: var(--bb-space-4);
779
+ }
780
+
781
+ .bb-pkw-route-heading {
782
+ align-items: flex-start;
783
+ justify-content: flex-start;
784
+ }
785
+
786
+ .bb-pkw-route-title {
787
+ display: grid;
788
+ gap: var(--bb-space-1);
789
+ min-width: 0;
790
+ }
791
+
792
+ .bb-pkw-route-title h3 {
793
+ margin: 0;
794
+ overflow-wrap: anywhere;
795
+ color: var(--bb-color-ink);
796
+ font-size: clamp(18px, 2vw, 26px);
797
+ letter-spacing: -.03em;
798
+ }
799
+
800
+ .bb-pkw-work-controls {
801
+ display: flex;
802
+ flex-wrap: wrap;
803
+ gap: var(--bb-space-2);
804
+ }
805
+
806
+ .bb-pkw-work-search,
807
+ .bb-pkw-work-status,
808
+ .bb-pkw-work-owner,
809
+ .bb-pkw-work-due {
810
+ min-height: 36px;
811
+ border: 1px solid var(--bb-color-border-strong);
812
+ border-radius: var(--bb-radius-sm);
813
+ padding: 7px 10px;
814
+ color: var(--bb-color-ink);
815
+ background: var(--bb-color-panel);
816
+ font: inherit;
817
+ font-size: 12px;
818
+ }
819
+
820
+ .bb-pkw-work-search {
821
+ flex: 1 1 260px;
822
+ min-width: 180px;
823
+ }
824
+
825
+ .bb-pkw-work-status {
826
+ flex: 0 1 200px;
827
+ }
828
+
829
+ .bb-pkw-work-owner,
830
+ .bb-pkw-work-due {
831
+ flex: 0 1 180px;
832
+ }
833
+
834
+ .bb-pkw-work-search:focus,
835
+ .bb-pkw-work-status:focus,
836
+ .bb-pkw-work-owner:focus,
837
+ .bb-pkw-work-due:focus {
838
+ outline: 3px solid color-mix(in srgb, var(--bb-color-focus) 35%, var(--bb-color-panel));
839
+ outline-offset: 1px;
840
+ border-color: var(--bb-color-accent);
841
+ }
842
+
843
+ .bb-pkw-work-count {
844
+ margin: 0;
845
+ color: var(--bb-color-muted);
846
+ font-size: 11px;
847
+ }
848
+
849
+ .bb-pkw-work-rows {
850
+ display: grid;
851
+ gap: 0;
852
+ min-width: 0;
853
+ overflow-x: auto;
854
+ border-top: 1px solid var(--bb-color-border);
855
+ }
856
+
857
+ .bb-pkw-work-row {
858
+ display: grid;
859
+ grid-template-columns: minmax(220px, 2fr) repeat(4, minmax(94px, 1fr));
860
+ gap: var(--bb-space-2);
861
+ align-items: center;
862
+ min-width: 680px;
863
+ border-bottom: 1px solid var(--bb-color-border);
864
+ padding: var(--bb-space-3) 0;
865
+ color: var(--bb-color-muted);
866
+ font-size: 12px;
867
+ }
868
+
869
+ .bb-pkw-work-row-header {
870
+ color: var(--bb-color-muted);
871
+ font-size: 11px;
872
+ font-weight: 720;
873
+ }
874
+
875
+ .bb-pkw-work-row > span,
876
+ .bb-pkw-work-row > button {
877
+ min-width: 0;
878
+ overflow-wrap: anywhere;
879
+ }
880
+
881
+ .bb-pkw-detail-columns {
882
+ display: grid;
883
+ grid-template-columns: minmax(0, 1.45fr) minmax(230px, .75fr);
884
+ gap: var(--bb-space-4);
885
+ align-items: start;
886
+ }
887
+
888
+ .bb-pkw-detail-main,
889
+ .bb-pkw-detail-side {
890
+ display: grid;
891
+ gap: var(--bb-space-3);
892
+ min-width: 0;
893
+ }
894
+
895
+ .bb-pkw-detail-body {
896
+ margin: 0;
897
+ white-space: pre-wrap;
898
+ overflow-wrap: anywhere;
899
+ color: var(--bb-color-ink);
900
+ font-size: 13px;
901
+ line-height: 1.8;
902
+ }
903
+
904
+ @media (max-width: 760px) {
905
+ .bb-pkw-dense-metrics {
906
+ grid-template-columns: repeat(2, minmax(0, 1fr));
907
+ }
908
+
909
+ .bb-pkw-dense-grid,
910
+ .bb-pkw-detail-columns {
911
+ grid-template-columns: minmax(0, 1fr);
912
+ }
913
+
914
+ .bb-pkw-dense-card-wide {
915
+ grid-column: auto;
916
+ }
917
+ }
@@ -28,7 +28,7 @@ import { workspaceDefinition, workspaceNotice } from './workspace-kit.js';
28
28
 
29
29
  export const PROJECT_WORKSPACE_CONTRACT_VERSION = 'brainbase.project-workspace.v1';
30
30
 
31
- const CONTEXT_STATES = new Set(['ok', 'loading', 'unavailable', 'failed']);
31
+ const CONTEXT_STATES = new Set(['ok', 'loading', 'unknown', 'unavailable', 'failed']);
32
32
 
33
33
  function cleanId(value) {
34
34
  return textOrNull(value);
@@ -62,10 +62,16 @@ function normalizeContextItem(item) {
62
62
  const title = cleanName(item.title, item.name);
63
63
  if (!id || !title) return null;
64
64
  const normalized = { id, title };
65
- for (const key of ['kind', 'status', 'summary', 'owner', 'dueAt', 'updatedAt']) {
65
+ for (const key of ['kind', 'status', 'priority', 'summary', 'owner', 'assignee', 'dueAt', 'updatedAt', 'eventAt', 'body', 'description', 'sourceRecordId', 'relatedRecordId', 'relatedTaskId', 'sourcePath', 'provenance']) {
66
66
  const value = textOrNull(item[key]);
67
67
  if (value) normalized[key] = value;
68
68
  }
69
+ for (const key of ['recordIds', 'unknowns']) {
70
+ if (Array.isArray(item[key])) {
71
+ const values = item[key].map((value) => textOrNull(value)).filter(Boolean);
72
+ if (values.length > 0) normalized[key] = values;
73
+ }
74
+ }
69
75
  const source = sourceLabel(item.source);
70
76
  if (source) normalized.source = source;
71
77
  return normalized;
@@ -82,6 +88,10 @@ function normalizeContextSection(section) {
82
88
  if (message) normalized.message = message;
83
89
  const asOf = textOrNull(section.asOf);
84
90
  if (asOf) normalized.asOf = asOf;
91
+ for (const key of ['total', 'totalCount', 'count']) {
92
+ const value = section[key];
93
+ if (Number.isFinite(value) || (typeof value === 'string' && /^\d+$/.test(value))) normalized[key] = Number(value);
94
+ }
85
95
  normalized.items = Array.isArray(section.items) ? section.items.map(normalizeContextItem).filter(Boolean) : [];
86
96
  return normalized;
87
97
  }
@@ -110,6 +120,9 @@ export function normalizeProjectContext(projectId, result) {
110
120
  if (result?.state === 'unavailable') {
111
121
  return { projectId: id, state: 'unavailable', message: firstText(result.message, result.reason) ?? '補足情報は接続されていません。', sections: [] };
112
122
  }
123
+ if (result?.state === 'unknown') {
124
+ return { projectId: id, state: 'unknown', message: firstText(result.message, result.reason) ?? '補足情報の取得状態を確認できません。', sections: [] };
125
+ }
113
126
  const payload = result?.state === 'ok' && isRecord(result.payload) ? result.payload : result;
114
127
  if (!isRecord(payload) || !Array.isArray(payload.sections)) {
115
128
  return { projectId: id, state: 'unavailable', message: '補足情報の形式を確認できません。', sections: [] };
@@ -331,7 +344,7 @@ function renderProjectContextGroup(doc, item) {
331
344
  attrs: { 'aria-label': item.title, ...(item.state === 'loading' ? { 'aria-busy': 'true' } : {}) },
332
345
  });
333
346
  const heading = makeElement(doc, 'div', { className: 'bb-pkw-context-heading' });
334
- 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'));
347
+ heading.append(makeElement(doc, 'h4', { text: item.title }), badge(doc, item.state === 'ok' ? '利用可能' : item.state === 'loading' ? '読み込み中' : item.state === 'unknown' ? '未確認' : item.state === 'failed' ? '読み取り失敗' : '未接続', item.state === 'ok' ? 'success' : item.state === 'failed' ? 'danger' : item.state === 'unknown' ? 'warning' : 'muted'));
335
348
  group.append(heading);
336
349
  if (item.message && !['failed', 'unavailable'].includes(item.state)) group.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: item.message }));
337
350
  if (item.asOf) group.append(makeElement(doc, 'small', { className: 'bb-pkw-muted', text: `読み取った時点: ${formatDateTime(item.asOf)}` }));
@@ -341,6 +354,8 @@ function renderProjectContextGroup(doc, item) {
341
354
  group.append(workspaceNotice(doc, { label: '読み取り失敗', text: item.message ?? 'このセクションを読み取れませんでした。', tone: 'danger', role: 'alert' }));
342
355
  } else if (item.state === 'unavailable') {
343
356
  group.append(workspaceNotice(doc, { label: '未接続', text: item.message ?? 'このセクションは接続されていません。', tone: 'warning' }));
357
+ } else if (item.state === 'unknown') {
358
+ group.append(workspaceNotice(doc, { label: '未確認', text: item.message ?? 'このセクションの取得状態は確認できません。', tone: 'warning' }));
344
359
  } else if (item.items.length > 0) {
345
360
  const list = makeElement(doc, 'ul', { className: 'bb-pkw-context-list' });
346
361
  for (const entry of item.items) {
@@ -385,7 +400,7 @@ export function renderProjectContext(doc, context, {
385
400
  section.append(workspaceNotice(doc, { label: '読み込み中', text: context.message ?? '補足情報を読み込んでいます。', tone: 'info', role: 'status' }));
386
401
  if (sections.length === 0) section.append(contextSectionSkeleton(doc));
387
402
  } else {
388
- section.append(workspaceNotice(doc, { label: context.state === 'failed' ? '読み取り失敗' : '未接続', text: context.message ?? '補足情報を確認できません。', tone: context.state === 'failed' ? 'danger' : 'warning' }));
403
+ section.append(workspaceNotice(doc, { label: context.state === 'failed' ? '読み取り失敗' : context.state === 'unknown' ? '未確認' : '未接続', text: context.message ?? '補足情報を確認できません。', tone: context.state === 'failed' ? 'danger' : 'warning' }));
389
404
  }
390
405
  }
391
406
  if (includeStatus && context.state === 'ok' && sections.length === 0) {
@@ -427,10 +442,383 @@ export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
427
442
  return panel;
428
443
  }
429
444
 
430
- function renderOverview(doc, detail, context, edges, entities, onSelect, onContext) {
445
+ function contextSectionById(context, ids = []) {
446
+ const sections = Array.isArray(context?.sections) ? context.sections : [];
447
+ const wanted = new Set(ids.map((value) => String(value).toLocaleLowerCase()));
448
+ return sections.find((section) => wanted.has(String(section.id).toLocaleLowerCase())) ?? null;
449
+ }
450
+
451
+ function contextTaskSection(context) {
452
+ const direct = contextSectionById(context, ['tasks', 'work', 'works']);
453
+ if (direct) return direct;
454
+ return (context?.sections ?? []).find((section) => /タスク|仕事|work|task/i.test(`${section.id} ${section.title}`)) ?? null;
455
+ }
456
+
457
+ function contextTaskItems(context) {
458
+ const section = contextTaskSection(context);
459
+ return Array.isArray(section?.items) ? section.items.map((item) => ({ ...item, kind: item.kind ?? 'task' })) : [];
460
+ }
461
+
462
+ function contextRecordItems(context) {
463
+ const sections = Array.isArray(context?.sections) ? context.sections : [];
464
+ return sections
465
+ .filter((section) => section !== contextTaskSection(context))
466
+ .flatMap((section) => Array.isArray(section.items) ? section.items.map((item) => ({ ...item, kind: item.kind ?? section.id })) : []);
467
+ }
468
+
469
+ function explicitSectionTotal(section) {
470
+ for (const key of ['total', 'totalCount', 'count']) {
471
+ const value = section?.[key];
472
+ if (Number.isFinite(value)) return value;
473
+ if (typeof value === 'string' && /^\d+$/.test(value)) return Number(value);
474
+ }
475
+ return null;
476
+ }
477
+
478
+ function denseDate(value) {
479
+ const text = textOrNull(value);
480
+ if (!text) return '未確認';
481
+ const formatted = formatDateTime(text);
482
+ return formatted === '—' ? text : formatted;
483
+ }
484
+
485
+ function denseButton(doc, label, className, onClick, attrs = {}) {
486
+ const button = makeElement(doc, 'button', { className, text: label, attrs: { type: 'button', ...attrs } });
487
+ if (typeof onClick === 'function') button.addEventListener('click', (event) => {
488
+ event?.preventDefault?.();
489
+ onClick();
490
+ });
491
+ return button;
492
+ }
493
+
494
+ function denseMetric(doc, label, value, note) {
495
+ const item = makeElement(doc, 'div', { className: 'bb-pkw-dense-metric' });
496
+ item.append(makeElement(doc, 'span', { className: 'bb-pkw-dense-metric-label', text: label }), makeElement(doc, 'strong', { className: 'bb-pkw-dense-metric-value', text: value ?? '未確認' }));
497
+ if (note) item.append(makeElement(doc, 'small', { className: 'bb-pkw-muted', text: note }));
498
+ return item;
499
+ }
500
+
501
+ function denseItemFacts(doc, item, { includeSummary = true, table = false } = {}) {
502
+ const facts = [];
503
+ if (table) {
504
+ facts.push(item.status ?? '未確認');
505
+ facts.push(item.owner || item.assignee ? `担当 ${item.owner ?? item.assignee}` : '担当 未確認');
506
+ facts.push(item.dueAt ? `期限 ${denseDate(item.dueAt)}` : '期限 未確認');
507
+ facts.push(item.updatedAt || item.eventAt ? `更新 ${denseDate(item.updatedAt ?? item.eventAt)}` : '更新 未確認');
508
+ } else {
509
+ if (item.status) facts.push(item.status);
510
+ if (item.owner || item.assignee) facts.push(`担当 ${item.owner ?? item.assignee}`);
511
+ if (item.dueAt) facts.push(`期限 ${denseDate(item.dueAt)}`);
512
+ if (item.updatedAt || item.eventAt) facts.push(`更新 ${denseDate(item.updatedAt ?? item.eventAt)}`);
513
+ }
514
+ const row = makeElement(doc, 'div', { className: 'bb-pkw-dense-item-facts' });
515
+ for (const fact of facts) row.append(makeElement(doc, 'span', { className: 'bb-pkw-dense-fact', text: fact }));
516
+ if (includeSummary && item.summary) row.append(makeElement(doc, 'span', { className: 'bb-pkw-muted', text: item.summary }));
517
+ return row;
518
+ }
519
+
520
+ function denseContextStateNotice(doc, section, fallback = 'この情報はまだ確認できません。') {
521
+ if (!section || section.state === 'ok') return null;
522
+ const label = section.state === 'loading' ? '読み込み中' : section.state === 'failed' ? '読み取り失敗' : section.state === 'unknown' ? '未確認' : '未接続';
523
+ const tone = section.state === 'failed' ? 'danger' : section.state === 'unknown' ? 'warning' : 'info';
524
+ return workspaceNotice(doc, { label, text: section.message ?? fallback, tone, role: section.state === 'failed' ? 'alert' : 'status' });
525
+ }
526
+
527
+ function renderDenseItemRow(doc, item, { kind = 'task', onOpen, table = false } = {}) {
528
+ const row = makeElement(doc, 'li', { className: `bb-pkw-dense-item bb-pkw-dense-item-${kind}` });
529
+ const head = makeElement(doc, 'div', { className: 'bb-pkw-dense-item-head' });
530
+ const title = cleanName(item.title, item.name) ?? '名前不明';
531
+ if (typeof onOpen === 'function' && item.id) head.append(denseButton(doc, title, 'bb-pkw-dense-link', () => onOpen(item.id), { 'data-item-id': item.id }));
532
+ else head.append(makeElement(doc, 'strong', { text: title }));
533
+ if (item.status) head.append(badge(doc, item.status, 'muted'));
534
+ row.append(head, denseItemFacts(doc, item, { table, includeSummary: !table }));
535
+ return row;
536
+ }
537
+
538
+ function renderDenseTableHeader(doc) {
539
+ const row = makeElement(doc, 'li', { className: 'bb-pkw-dense-table-header', attrs: { role: 'presentation' } });
540
+ row.append(makeElement(doc, 'span', { text: '仕事' }), makeElement(doc, 'span', { text: '状態' }), makeElement(doc, 'span', { text: '担当' }), makeElement(doc, 'span', { text: '期限' }), makeElement(doc, 'span', { text: '更新' }));
541
+ return row;
542
+ }
543
+
544
+ function sortDenseItems(items) {
545
+ return [...items].sort((left, right) => {
546
+ const leftDate = Date.parse(left.dueAt ?? left.updatedAt ?? left.eventAt ?? '') || Number.MAX_SAFE_INTEGER;
547
+ const rightDate = Date.parse(right.dueAt ?? right.updatedAt ?? right.eventAt ?? '') || Number.MAX_SAFE_INTEGER;
548
+ return leftDate - rightDate;
549
+ });
550
+ }
551
+
552
+ function todayActionItems(items, now = new Date()) {
553
+ const endOfToday = new Date(now);
554
+ endOfToday.setHours(23, 59, 59, 999);
555
+ return sortDenseItems(items.filter((item) => {
556
+ if (/完了|取消|キャンセル|中止|取下げ|completed|cancell?ed|done|closed/i.test(item.status ?? '')) return false;
557
+ if (/確認待ち|要確認|承認待ち|blocked|needs.review|waiting/i.test(item.status ?? '')) return true;
558
+ const due = Date.parse(item.dueAt ?? '');
559
+ return Number.isFinite(due) && due <= endOfToday.getTime();
560
+ }));
561
+ }
562
+
563
+ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
564
+ const payload = projectDetailPayload(detail);
565
+ const taskSection = contextTaskSection(context);
566
+ const tasks = contextTaskItems(context);
567
+ const records = contextRecordItems(context);
568
+ const graphRecords = [...(entities?.values?.() ?? [])].filter((entity) => !['project', 'person'].includes(entity.type));
569
+ const participants = [...new Map((payload?.participants ?? [])
570
+ .map((edge) => edge?.counterpart)
571
+ .filter((person) => person?.id && (person.name || person.title))
572
+ .map((person) => [person.id, person])).values()].slice(0, 6);
573
+ const recentRecords = records.length > 0 ? sortDenseItems(records).slice(0, 4) : graphRecords.slice(0, 4);
574
+ const updated = [...tasks, ...records]
575
+ .filter((item) => item.updatedAt || item.eventAt)
576
+ .sort((left, right) => (Date.parse(right.updatedAt ?? right.eventAt) || 0) - (Date.parse(left.updatedAt ?? left.eventAt) || 0))
577
+ .slice(0, 4);
578
+ const due = todayActionItems(tasks).slice(0, 6);
579
+ const total = explicitSectionTotal(taskSection);
580
+ const taskMetric = total === null ? (taskSection?.state === 'ok' ? `${tasks.length}件表示` : '未確認') : `${total}件`;
581
+ const taskNote = taskSection?.state === 'ok' ? (total === null ? '総数は未確認' : '取得済みの総数') : '取得状態を確認中';
582
+ const panel = makeElement(doc, 'section', { className: 'bb-pkw-dense-overview', attrs: { 'aria-label': 'プロジェクト概要' } });
583
+ const intro = makeElement(doc, 'div', { className: 'bb-pkw-dense-intro' });
584
+ intro.append(makeElement(doc, 'div', { className: 'bb-pkw-dense-eyebrow', text: '今日見る' }), makeElement(doc, 'h3', { text: cleanName(payload?.project?.name, 'プロジェクト') }), makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '量を把握して、必要な仕事と根拠へ進みます。' }));
585
+ panel.append(intro);
586
+ const metrics = makeElement(doc, 'div', { className: 'bb-pkw-dense-metrics', attrs: { 'aria-label': '量の指標' } });
587
+ metrics.append(
588
+ denseMetric(doc, '仕事', taskMetric, taskNote),
589
+ denseMetric(doc, '今日扱う仕事', taskSection?.state === 'ok' ? `${due.length}件` : '未確認', taskSection?.state === 'ok' ? '期限・更新が確認できる順' : 'タスクの状態を確認中'),
590
+ denseMetric(doc, '前回からの変化', updated.length > 0 ? `${updated.length}件` : '未確認', updated.length > 0 ? '更新時点がある記録' : '更新時点が未確認'),
591
+ denseMetric(doc, '関係者', Array.isArray(payload?.participants) && payload.participants.length > 0 ? `${payload.participants.length}人` : '未確認', 'Graphに記録された参加関係'),
592
+ );
593
+ panel.append(metrics);
594
+
595
+ const primaryGrid = makeElement(doc, 'div', { className: 'bb-pkw-dense-grid' });
596
+ const today = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '今日判断・対応すること' } });
597
+ const todayHead = makeElement(doc, 'div', { className: 'bb-pkw-dense-card-head' });
598
+ todayHead.append(makeElement(doc, 'h4', { text: '今日判断・対応すること' }));
599
+ if (typeof actions.openWorkList === 'function') todayHead.append(denseButton(doc, 'すべて見る', 'bb-pkw-dense-action', actions.openWorkList));
600
+ today.append(todayHead);
601
+ const todayNotice = denseContextStateNotice(doc, taskSection ?? (context?.state !== 'ok' ? context : null), 'タスクを読み取れませんでした。');
602
+ if (todayNotice) today.append(todayNotice);
603
+ if (!todayNotice && due.length === 0) today.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '表示できる仕事はありません。' }));
604
+ if (!todayNotice && due.length > 0) {
605
+ const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list bb-pkw-dense-table' });
606
+ list.append(renderDenseTableHeader(doc));
607
+ for (const item of due) list.append(renderDenseItemRow(doc, item, { onOpen: actions.openWorkDetail, table: true }));
608
+ today.append(list);
609
+ }
610
+ primaryGrid.append(today);
611
+
612
+ const changes = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '前回からの変化' } });
613
+ const changesHead = makeElement(doc, 'div', { className: 'bb-pkw-dense-card-head' });
614
+ changesHead.append(makeElement(doc, 'h4', { text: '前回からの変化' }));
615
+ changes.append(changesHead);
616
+ if (updated.length === 0) changes.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '更新時点のある記録は未確認です。' }));
617
+ else {
618
+ const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list bb-pkw-dense-table bb-pkw-dense-change-table' });
619
+ list.append(renderDenseTableHeader(doc));
620
+ for (const item of updated) list.append(renderDenseItemRow(doc, item, { kind: 'change', onOpen: records.includes(item) ? actions.openRecordDetail : actions.openWorkDetail, table: true }));
621
+ changes.append(list);
622
+ }
623
+ primaryGrid.append(changes);
624
+ panel.append(primaryGrid);
625
+
626
+ const supportingGrid = makeElement(doc, 'div', { className: 'bb-pkw-dense-grid bb-pkw-dense-supporting-grid' });
627
+ const recent = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '最近の記録' } });
628
+ const recentHead = makeElement(doc, 'div', { className: 'bb-pkw-dense-card-head' });
629
+ recentHead.append(makeElement(doc, 'h4', { text: '最近の記録' }), makeElement(doc, 'span', { className: 'bb-pkw-muted', text: recentRecords.length > 0 ? `${recentRecords.length}件を表示` : '未確認' }));
630
+ recent.append(recentHead);
631
+ if (recentRecords.length === 0) recent.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '最近の記録は未確認です。' }));
632
+ else {
633
+ const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-record-list' });
634
+ for (const item of recentRecords) list.append(renderDenseItemRow(doc, item, { kind: 'record', onOpen: actions.openRecordDetail }));
635
+ recent.append(list);
636
+ }
637
+ supportingGrid.append(recent);
638
+
639
+ const people = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '関係者' } });
640
+ const peopleHead = makeElement(doc, 'div', { className: 'bb-pkw-dense-card-head' });
641
+ peopleHead.append(makeElement(doc, 'h4', { text: '関係者' }), makeElement(doc, 'span', { className: 'bb-pkw-muted', text: participants.length > 0 ? `${participants.length}人を表示` : '未確認' }));
642
+ people.append(peopleHead);
643
+ if (participants.length === 0) people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: 'Graphで確認できる関係者は未確認です。' }));
644
+ else {
645
+ const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list' });
646
+ for (const person of participants) {
647
+ const row = makeElement(doc, 'li', { className: 'bb-pkw-dense-item' });
648
+ row.append(makeElement(doc, 'strong', { text: cleanName(person.name, person.title) }));
649
+ if (person.role) row.append(makeElement(doc, 'span', { className: 'bb-pkw-muted', text: person.role }));
650
+ list.append(row);
651
+ }
652
+ people.append(list);
653
+ }
654
+ supportingGrid.append(people);
655
+ panel.append(supportingGrid);
656
+ return panel;
657
+ }
658
+
659
+ function renderWorkList(doc, context, actions = {}) {
660
+ const section = contextTaskSection(context);
661
+ const allItems = contextTaskItems(context);
662
+ const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-work-list', attrs: { role: 'tabpanel', 'aria-label': '仕事一覧' } });
663
+ const heading = makeElement(doc, 'div', { className: 'bb-pkw-route-heading' });
664
+ heading.append(denseButton(doc, '← 概要', 'bb-pkw-back', actions.openOverview), makeElement(doc, 'div', { className: 'bb-pkw-route-title' }));
665
+ heading.children[1].append(makeElement(doc, 'h3', { text: '仕事一覧' }), makeElement(doc, 'p', { className: 'bb-pkw-muted', text: section?.state === 'ok' ? '状態・担当・期限で必要な行だけを確認します。' : '取得状態を確認しながら表示します。' }));
666
+ panel.append(heading);
667
+ const notice = denseContextStateNotice(doc, section ?? (context?.state !== 'ok' ? context : null), 'タスクを読み取れませんでした。');
668
+ if (notice) panel.append(notice);
669
+ const controls = makeElement(doc, 'div', { className: 'bb-pkw-work-controls', attrs: { 'aria-label': '仕事の絞り込み' } });
670
+ const search = makeElement(doc, 'input', { className: 'bb-pkw-work-search', attrs: { type: 'search', name: 'work-search', placeholder: '仕事を検索', 'aria-label': '仕事を検索' } });
671
+ const status = makeElement(doc, 'select', { className: 'bb-pkw-work-status', attrs: { name: 'work-status', 'aria-label': '状態で絞り込む' } });
672
+ status.append(makeElement(doc, 'option', { text: 'すべての状態', attrs: { value: '' } }));
673
+ for (const value of new Set(allItems.map((item) => item.status).filter(Boolean))) status.append(makeElement(doc, 'option', { text: value, attrs: { value } }));
674
+ const owner = makeElement(doc, 'select', { className: 'bb-pkw-work-owner', attrs: { name: 'work-owner', 'aria-label': '担当で絞り込む' } });
675
+ owner.append(makeElement(doc, 'option', { text: 'すべての担当', attrs: { value: '' } }));
676
+ for (const value of new Set(allItems.map((item) => item.owner ?? item.assignee).filter(Boolean))) owner.append(makeElement(doc, 'option', { text: value, attrs: { value } }));
677
+ const due = makeElement(doc, 'select', { className: 'bb-pkw-work-due', attrs: { name: 'work-due', 'aria-label': '期限で絞り込む' } });
678
+ due.append(
679
+ makeElement(doc, 'option', { text: 'すべての期限', attrs: { value: '' } }),
680
+ makeElement(doc, 'option', { text: '期限あり', attrs: { value: 'set' } }),
681
+ makeElement(doc, 'option', { text: '期限未確認', attrs: { value: 'missing' } }),
682
+ );
683
+ controls.append(search, status, owner, due);
684
+ panel.append(controls);
685
+ const count = makeElement(doc, 'p', { className: 'bb-pkw-work-count' });
686
+ const rows = makeElement(doc, 'div', { className: 'bb-pkw-work-rows', attrs: { role: 'list', 'aria-label': '仕事の行' } });
687
+ panel.append(count, rows);
688
+ const renderRows = () => {
689
+ const query = String(search.value ?? '').trim().toLocaleLowerCase();
690
+ const selectedStatus = String(status.value ?? '');
691
+ const selectedOwner = String(owner.value ?? '');
692
+ const selectedDue = String(due.value ?? '');
693
+ const visible = allItems.filter((item) => {
694
+ if (selectedStatus && item.status !== selectedStatus) return false;
695
+ if (selectedOwner && (item.owner ?? item.assignee) !== selectedOwner) return false;
696
+ if (selectedDue === 'set' && !item.dueAt) return false;
697
+ if (selectedDue === 'missing' && item.dueAt) return false;
698
+ if (!query) return true;
699
+ return [item.title, item.summary, item.owner, item.assignee, item.status].filter(Boolean).some((value) => String(value).toLocaleLowerCase().includes(query));
700
+ });
701
+ const total = explicitSectionTotal(section);
702
+ const display = visible.slice(0, 16);
703
+ count.textContent = `表示 ${display.length}件${display.length < visible.length ? `(条件一致 ${visible.length}件の先頭)` : ''} / 総数 ${total === null ? '未確認' : `${total}件`}`;
704
+ rows.replaceChildren();
705
+ if (display.length === 0) {
706
+ rows.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: section?.state === 'ok' ? '条件に一致する仕事はありません。' : '仕事の行は未確認です。' }));
707
+ return;
708
+ }
709
+ const header = makeElement(doc, 'div', { className: 'bb-pkw-work-row bb-pkw-work-row-header', attrs: { role: 'presentation' } });
710
+ header.append(makeElement(doc, 'span', { text: '仕事' }), makeElement(doc, 'span', { text: '状態' }), makeElement(doc, 'span', { text: '担当' }), makeElement(doc, 'span', { text: '期限' }), makeElement(doc, 'span', { text: '更新' }));
711
+ rows.append(header);
712
+ for (const item of display) {
713
+ const row = makeElement(doc, 'div', { className: 'bb-pkw-work-row', attrs: { role: 'listitem' } });
714
+ row.append(denseButton(doc, item.title, 'bb-pkw-dense-link', () => actions.openWorkDetail?.(item.id), { 'data-item-id': item.id }), makeElement(doc, 'span', { text: item.status ?? '未確認' }), makeElement(doc, 'span', { text: item.owner ?? item.assignee ?? '未確認' }), makeElement(doc, 'span', { text: denseDate(item.dueAt) }), makeElement(doc, 'span', { text: denseDate(item.updatedAt) }));
715
+ rows.append(row);
716
+ }
717
+ };
718
+ search.addEventListener('input', renderRows);
719
+ status.addEventListener('change', renderRows);
720
+ owner.addEventListener('change', renderRows);
721
+ due.addEventListener('change', renderRows);
722
+ renderRows();
723
+ return panel;
724
+ }
725
+
726
+ function detailDefinition(doc, item) {
727
+ return workspaceDefinition(doc, [
728
+ ['状態', item.status],
729
+ ['担当', item.owner ?? item.assignee],
730
+ ['期限', item.dueAt ? denseDate(item.dueAt) : null],
731
+ ['更新', item.updatedAt ? denseDate(item.updatedAt) : null],
732
+ ['発生', item.eventAt ? denseDate(item.eventAt) : null],
733
+ ['出典', item.source ?? item.sourcePath ?? item.provenance],
734
+ ]);
735
+ }
736
+
737
+ function renderWorkDetail(doc, item, context, actions = {}) {
738
+ const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-detail', attrs: { role: 'tabpanel', 'aria-label': '仕事詳細' } });
739
+ const heading = makeElement(doc, 'div', { className: 'bb-pkw-route-heading' });
740
+ heading.append(denseButton(doc, '← 仕事一覧', 'bb-pkw-back', actions.openWorkList), makeElement(doc, 'div', { className: 'bb-pkw-route-title' }));
741
+ heading.children[1].append(makeElement(doc, 'h3', { text: item?.title ?? '仕事詳細' }), item?.status ? badge(doc, item.status, 'accent') : null);
742
+ panel.append(heading);
743
+ if (!item) {
744
+ panel.append(workspaceNotice(doc, { label: '未確認', text: 'この仕事のIDに一致する記録を確認できません。', tone: 'warning' }));
745
+ return panel;
746
+ }
747
+ const columns = makeElement(doc, 'div', { className: 'bb-pkw-detail-columns' });
748
+ const main = makeElement(doc, 'div', { className: 'bb-pkw-detail-main' });
749
+ const bodyCard = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '仕事の内容' } });
750
+ bodyCard.append(makeElement(doc, 'h4', { text: '仕事の内容' }));
751
+ const body = firstText(item.body, item.description, item.summary);
752
+ bodyCard.append(makeElement(doc, 'p', { className: body ? 'bb-pkw-detail-body' : 'bb-pkw-muted', text: body ?? '本文は未接続です。要約以外の内容は未確認です。' }));
753
+ main.append(bodyCard);
754
+ const records = [item.sourceRecordId, item.relatedRecordId, ...(item.recordIds ?? [])].filter(Boolean);
755
+ const related = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '関連記録' } });
756
+ related.append(makeElement(doc, 'h4', { text: '関連記録' }));
757
+ if (records.length === 0) related.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '関連記録のIDは未接続です。関係を推測して追加していません。' }));
758
+ else {
759
+ const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list' });
760
+ for (const id of records) {
761
+ const row = makeElement(doc, 'li');
762
+ row.append(denseButton(doc, id, 'bb-pkw-dense-link', () => actions.openRecordDetail?.(id), { 'data-record-id': id }));
763
+ list.append(row);
764
+ }
765
+ related.append(list);
766
+ }
767
+ main.append(related);
768
+ columns.append(main);
769
+ const side = makeElement(doc, 'aside', { className: 'bb-pkw-detail-side', attrs: { 'aria-label': '仕事の属性' } });
770
+ const sideCard = makeElement(doc, 'section', { className: 'bb-pkw-dense-card' });
771
+ sideCard.append(makeElement(doc, 'h4', { text: '属性' }), detailDefinition(doc, item));
772
+ if (Array.isArray(item.unknowns) && item.unknowns.length > 0) sideCard.append(makeElement(doc, 'h4', { text: '未確認' }), makeElement(doc, 'p', { className: 'bb-pkw-muted', text: item.unknowns.join(' · ') }));
773
+ side.append(sideCard);
774
+ columns.append(side);
775
+ panel.append(columns);
776
+ return panel;
777
+ }
778
+
779
+ function renderRecordDetail(doc, record, context, actions = {}) {
780
+ const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-detail', attrs: { role: 'tabpanel', 'aria-label': '記録詳細' } });
781
+ const heading = makeElement(doc, 'div', { className: 'bb-pkw-route-heading' });
782
+ heading.append(denseButton(doc, '← 概要', 'bb-pkw-back', actions.openOverview), makeElement(doc, 'div', { className: 'bb-pkw-route-title' }));
783
+ 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);
784
+ panel.append(heading);
785
+ if (!record) {
786
+ panel.append(workspaceNotice(doc, { label: '未確認', text: 'この記録のIDに一致する内容を確認できません。', tone: 'warning' }));
787
+ return panel;
788
+ }
789
+ const columns = makeElement(doc, 'div', { className: 'bb-pkw-detail-columns' });
790
+ const main = makeElement(doc, 'div', { className: 'bb-pkw-detail-main' });
791
+ const summary = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '記録の要点' } });
792
+ summary.append(makeElement(doc, 'h4', { text: '記録の要点' }), makeElement(doc, 'p', { className: record.body || record.summary ? 'bb-pkw-detail-body' : 'bb-pkw-muted', text: firstText(record.body, record.summary) ?? '本文・要約は未確認です。' }));
793
+ main.append(summary);
794
+ const related = makeElement(doc, 'section', { className: 'bb-pkw-dense-card', attrs: { 'aria-label': '関連する仕事' } });
795
+ related.append(makeElement(doc, 'h4', { text: '関連する仕事' }));
796
+ const tasks = contextTaskItems(context);
797
+ const relatedTask = tasks.find((task) => task.relatedTaskId === record.id || task.sourceRecordId === record.id || (task.recordIds ?? []).includes(record.id));
798
+ if (record.relatedTaskId) {
799
+ const task = tasks.find((candidate) => candidate.id === record.relatedTaskId);
800
+ if (task) related.append(denseButton(doc, task.title, 'bb-pkw-dense-link', () => actions.openWorkDetail?.(task.id), { 'data-task-id': task.id }));
801
+ else related.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: `仕事ID ${record.relatedTaskId} は未確認です。` }));
802
+ } else if (relatedTask) related.append(denseButton(doc, relatedTask.title, 'bb-pkw-dense-link', () => actions.openWorkDetail?.(relatedTask.id), { 'data-task-id': relatedTask.id }));
803
+ else related.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '関連する仕事のIDは未接続です。' }));
804
+ main.append(related);
805
+ columns.append(main);
806
+ const side = makeElement(doc, 'aside', { className: 'bb-pkw-detail-side', attrs: { 'aria-label': '記録の属性' } });
807
+ const sideCard = makeElement(doc, 'section', { className: 'bb-pkw-dense-card' });
808
+ sideCard.append(makeElement(doc, 'h4', { text: '出典と状態' }), detailDefinition(doc, record));
809
+ const unknowns = Array.isArray(record.unknowns) ? record.unknowns : [];
810
+ if (unknowns.length > 0) sideCard.append(makeElement(doc, 'h4', { text: '未確認' }), makeElement(doc, 'p', { className: 'bb-pkw-muted', text: unknowns.join(' · ') }));
811
+ side.append(sideCard);
812
+ columns.append(side);
813
+ panel.append(columns);
814
+ return panel;
815
+ }
816
+
817
+ function renderOverview(doc, detail, context, edges, entities, onSelect, onContext, actions = {}) {
431
818
  const payload = projectDetailPayload(detail);
432
819
  const project = payload?.project;
433
820
  const panel = makeElement(doc, 'div', { className: 'bb-pkw-panel bb-pkw-overview', attrs: { role: 'tabpanel', 'aria-label': '概要' } });
821
+ panel.append(renderDenseOverview(doc, detail, context, entities, actions));
434
822
  const columns = makeElement(doc, 'div', { className: 'bb-pkw-columns' });
435
823
  const summary = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '目的と現在地' } });
436
824
  summary.append(makeElement(doc, 'h3', { text: '目的と現在地' }), workspaceDefinition(doc, [
@@ -565,6 +953,8 @@ export function createProjectKnowledgeWorkspace({
565
953
  let graphSearch = '';
566
954
  let graphType = '';
567
955
  let currentContext = context;
956
+ let activeView = 'overview';
957
+ let selectedContextId = null;
568
958
  // Keep direct references as well as the DOM lookup. The local test harness
569
959
  // intentionally has no querySelector implementation, and a host can replace
570
960
  // the panel while a detail read is still in flight.
@@ -597,10 +987,53 @@ export function createProjectKnowledgeWorkspace({
597
987
 
598
988
  function setTab(tab) {
599
989
  activeTab = tab === 'graph' ? 'graph' : 'overview';
990
+ if (activeTab === 'overview') {
991
+ activeView = 'overview';
992
+ selectedContextId = null;
993
+ }
600
994
  if (typeof onTabChange === 'function') onTabChange(activeTab);
601
995
  render();
602
996
  }
603
997
 
998
+ function findContextItem(id, kind = null) {
999
+ const target = cleanId(id);
1000
+ if (!target) return null;
1001
+ const candidates = kind === 'task' ? contextTaskItems(currentContext) : kind === 'record' ? contextRecordItems(currentContext) : [...contextTaskItems(currentContext), ...contextRecordItems(currentContext)];
1002
+ return candidates.find((item) => item.id === target) ?? null;
1003
+ }
1004
+
1005
+ function openOverview() {
1006
+ if (destroyed) return;
1007
+ activeTab = 'overview';
1008
+ activeView = 'overview';
1009
+ selectedContextId = null;
1010
+ render();
1011
+ }
1012
+
1013
+ function openWorkList() {
1014
+ if (destroyed) return;
1015
+ activeTab = 'overview';
1016
+ activeView = 'work-list';
1017
+ selectedContextId = null;
1018
+ render();
1019
+ }
1020
+
1021
+ function openWorkDetail(id) {
1022
+ if (destroyed) return;
1023
+ activeTab = 'overview';
1024
+ activeView = 'work-detail';
1025
+ selectedContextId = cleanId(id);
1026
+ render();
1027
+ }
1028
+
1029
+ function openRecordDetail(id) {
1030
+ if (destroyed) return;
1031
+ activeTab = 'overview';
1032
+ activeView = 'record-detail';
1033
+ selectedContextId = cleanId(id);
1034
+ render();
1035
+ }
1036
+
604
1037
  function mergeEntityRead(read) {
605
1038
  if (!isRecord(read?.entity)) return;
606
1039
  addEntity(entities, read.entity);
@@ -813,6 +1246,10 @@ export function createProjectKnowledgeWorkspace({
813
1246
  function replaceContextBlock(nextContext) {
814
1247
  currentContext = nextContext;
815
1248
  if (destroyed) return;
1249
+ if (activeTab === 'overview' && activeView !== 'overview') {
1250
+ render();
1251
+ return;
1252
+ }
816
1253
  const nextBlock = renderProjectContext(doc, currentContext);
817
1254
  if (!nextBlock) {
818
1255
  if (contextBlock?.parentNode && typeof contextBlock.parentNode.replaceChild === 'function') {
@@ -852,10 +1289,15 @@ export function createProjectKnowledgeWorkspace({
852
1289
  panels.replaceChildren();
853
1290
  contextBlock = null;
854
1291
  contextAnchor = null;
855
- const overview = renderOverview(doc, payload, currentContext, overviewEdges, overviewEntities, selectAndExplore, (node, anchor) => {
1292
+ const actions = { openOverview, openWorkList, openWorkDetail, openRecordDetail };
1293
+ let overview;
1294
+ if (activeView === 'work-list') overview = renderWorkList(doc, currentContext, actions);
1295
+ else if (activeView === 'work-detail') overview = renderWorkDetail(doc, findContextItem(selectedContextId, 'task'), currentContext, actions);
1296
+ else if (activeView === 'record-detail') overview = renderRecordDetail(doc, findContextItem(selectedContextId, 'record') ?? entities.get(selectedContextId), currentContext, actions);
1297
+ else overview = renderOverview(doc, payload, currentContext, overviewEdges, overviewEntities, selectAndExplore, (node, anchor) => {
856
1298
  contextBlock = node;
857
1299
  contextAnchor = anchor;
858
- });
1300
+ }, actions);
859
1301
  overview.id = 'bb-pkw-panel-overview';
860
1302
  overview.hidden = activeTab !== 'overview';
861
1303
  overviewPanel = overview;