@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 +2 -2
- package/ui/project-workspace.css +50 -0
- package/ui/project-workspace.js +185 -165
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unson/brainbase-mcp",
|
|
3
|
-
"version": "0.10.
|
|
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": "
|
|
339
|
+
"gitHead": "12af97cb3f02f9964c7bb7ca8ba7a5debdf29e11"
|
|
340
340
|
}
|
package/ui/project-workspace.css
CHANGED
|
@@ -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,
|
package/ui/project-workspace.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
615
|
-
.
|
|
616
|
-
.
|
|
617
|
-
|
|
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, '関係者',
|
|
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
|
|
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(
|
|
694
|
-
|
|
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
|
-
|
|
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
|
-
|
|
748
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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,
|
|
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';
|