@unson/brainbase-mcp 0.10.8 → 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.8",
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": "0c5238559b78d31ea5c3dc7f2b27361b8d8b03c4"
339
+ "gitHead": "12af97cb3f02f9964c7bb7ca8ba7a5debdf29e11"
340
340
  }
@@ -218,6 +218,120 @@
218
218
  flex-basis: 100%;
219
219
  }
220
220
 
221
+ .bb-pkw-context-task-list {
222
+ gap: 0;
223
+ border-top: 1px solid var(--bb-color-border);
224
+ }
225
+
226
+ .bb-pkw-context-list .bb-pkw-context-task-row {
227
+ display: grid;
228
+ grid-template-columns: 28px minmax(0, 2.4fr) minmax(112px, .75fr) 92px minmax(100px, .9fr);
229
+ align-items: center;
230
+ gap: 0;
231
+ min-height: 54px;
232
+ border-bottom: 1px solid var(--bb-color-border);
233
+ padding: 0;
234
+ font-size: 12px;
235
+ }
236
+
237
+ .bb-pkw-context-list .bb-pkw-context-task-row:last-child {
238
+ border-bottom: 1px solid var(--bb-color-border);
239
+ }
240
+
241
+ .bb-pkw-context-task-row > * {
242
+ min-width: 0;
243
+ padding: 8px 10px;
244
+ }
245
+
246
+ .bb-pkw-context-task-row .bb-pkw-context-task-state {
247
+ width: 9px;
248
+ height: 9px;
249
+ margin: auto;
250
+ padding: 0;
251
+ border: 1.5px solid var(--bb-color-muted);
252
+ border-radius: 50%;
253
+ }
254
+
255
+ .bb-pkw-context-task-row .bb-pkw-context-task-state.bb-pkw-context-task-wait {
256
+ border-color: var(--bb-color-warning);
257
+ background: var(--bb-color-warning);
258
+ }
259
+
260
+ .bb-pkw-context-task-row .bb-pkw-context-task-state.bb-pkw-context-task-fail {
261
+ border-color: var(--bb-color-danger);
262
+ background: var(--bb-color-danger);
263
+ }
264
+
265
+ .bb-pkw-context-task-row .bb-pkw-context-task-state.bb-pkw-context-task-ok {
266
+ border-color: var(--bb-color-accent);
267
+ background: var(--bb-color-accent);
268
+ }
269
+
270
+ .bb-pkw-context-task-row .bb-pkw-context-task-title {
271
+ width: 100%;
272
+ overflow: hidden;
273
+ border: 0;
274
+ background: transparent;
275
+ color: var(--bb-color-ink);
276
+ font: inherit;
277
+ font-size: 14px;
278
+ font-weight: 690;
279
+ text-align: left;
280
+ text-overflow: ellipsis;
281
+ white-space: nowrap;
282
+ }
283
+
284
+ button.bb-pkw-context-task-title { cursor: pointer; }
285
+ button.bb-pkw-context-task-title:hover { color: var(--bb-color-accent-strong); text-decoration: underline; }
286
+ button.bb-pkw-context-task-title:focus-visible { outline: 3px solid var(--bb-color-focus); outline-offset: -2px; }
287
+
288
+ .bb-pkw-context-task-owner {
289
+ color: var(--bb-color-muted);
290
+ overflow-wrap: anywhere;
291
+ }
292
+
293
+ .bb-pkw-context-task-due {
294
+ white-space: nowrap;
295
+ }
296
+
297
+ .bb-pkw-context-task-due.bb-pkw-context-task-overdue {
298
+ color: var(--bb-color-danger);
299
+ font-weight: 740;
300
+ }
301
+
302
+ .bb-pkw-context-task-stage {
303
+ display: flex;
304
+ align-items: center;
305
+ gap: 7px;
306
+ color: var(--bb-color-muted);
307
+ }
308
+
309
+ .bb-pkw-context-task-stage::before {
310
+ width: 6px;
311
+ height: 6px;
312
+ flex: 0 0 auto;
313
+ border-radius: 50%;
314
+ background: var(--bb-color-muted);
315
+ content: '';
316
+ }
317
+
318
+ .bb-pkw-context-task-stage.bb-pkw-context-task-wait::before { background: var(--bb-color-warning); }
319
+ .bb-pkw-context-task-stage.bb-pkw-context-task-fail::before { background: var(--bb-color-danger); }
320
+ .bb-pkw-context-task-stage.bb-pkw-context-task-ok::before { background: var(--bb-color-accent); }
321
+
322
+ @media (max-width: 760px) {
323
+ .bb-pkw-context-list .bb-pkw-context-task-row {
324
+ grid-template-columns: 28px minmax(0, 1fr) auto;
325
+ grid-template-areas: 'state title stage' '. owner due';
326
+ }
327
+
328
+ .bb-pkw-context-task-state { grid-area: state; }
329
+ .bb-pkw-context-task-title { grid-area: title; }
330
+ .bb-pkw-context-task-owner { grid-area: owner; }
331
+ .bb-pkw-context-task-due { grid-area: due; }
332
+ .bb-pkw-context-task-stage { grid-area: stage; }
333
+ }
334
+
221
335
  .bb-pkw-info-grid {
222
336
  display: grid;
223
337
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
@@ -819,6 +933,56 @@
819
933
  gap: var(--bb-space-2);
820
934
  }
821
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
+
822
986
  .bb-pkw-work-search,
823
987
  .bb-pkw-work-status,
824
988
  .bb-pkw-work-owner,
@@ -62,7 +62,7 @@ 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', 'priority', 'summary', 'owner', 'assignee', 'dueAt', 'updatedAt', 'eventAt', 'body', 'description', 'sourceRecordId', 'relatedRecordId', 'relatedTaskId', 'sourcePath', 'provenance']) {
65
+ for (const key of ['kind', 'status', 'statusKind', '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
  }
@@ -338,7 +338,46 @@ function contextSectionSkeleton(doc, { title = null, message = null } = {}) {
338
338
  return group;
339
339
  }
340
340
 
341
- function renderProjectContextGroup(doc, item) {
341
+ function contextTaskDue(value) {
342
+ if (!value) return '未確認';
343
+ const date = new Date(value);
344
+ return Number.isNaN(date.getTime())
345
+ ? value
346
+ : new Intl.DateTimeFormat('ja-JP', { month: 'numeric', day: 'numeric' }).format(date);
347
+ }
348
+
349
+ function contextTaskOverdue(value, statusKind) {
350
+ if (!value || ['ok', 'fail'].includes(statusKind)) return false;
351
+ const date = new Date(value);
352
+ if (Number.isNaN(date.getTime())) return false;
353
+ const today = new Date();
354
+ date.setHours(0, 0, 0, 0);
355
+ today.setHours(0, 0, 0, 0);
356
+ return date < today;
357
+ }
358
+
359
+ function renderProjectContextTaskRow(doc, entry, onTaskOpen) {
360
+ const status = entry.status ?? '未確認';
361
+ const kind = ['ok', 'wait', 'fail'].includes(entry.statusKind) ? entry.statusKind : '';
362
+ const row = makeElement(doc, 'li', { className: 'bb-pkw-context-task-row' });
363
+ row.append(makeElement(doc, 'span', {
364
+ className: `bb-pkw-context-task-state${kind ? ` bb-pkw-context-task-${kind}` : ''}`,
365
+ attrs: { 'aria-hidden': 'true' },
366
+ }));
367
+ row.append(typeof onTaskOpen === 'function'
368
+ ? denseButton(doc, entry.title, 'bb-pkw-context-task-title', () => onTaskOpen(entry.id), { title: entry.title, 'data-item-id': entry.id })
369
+ : makeElement(doc, 'strong', { className: 'bb-pkw-context-task-title', text: entry.title, attrs: { title: entry.title } }));
370
+ row.append(makeElement(doc, 'span', { className: 'bb-pkw-context-task-owner', text: entry.owner ?? entry.assignee ?? '担当者を確認できていない' }));
371
+ row.append(makeElement(doc, 'time', {
372
+ className: `bb-pkw-context-task-due${contextTaskOverdue(entry.dueAt, kind) ? ' bb-pkw-context-task-overdue' : ''}`,
373
+ text: contextTaskDue(entry.dueAt),
374
+ attrs: entry.dueAt ? { datetime: entry.dueAt } : {},
375
+ }));
376
+ row.append(makeElement(doc, 'span', { className: `bb-pkw-context-task-stage${kind ? ` bb-pkw-context-task-${kind}` : ''}`, text: status }));
377
+ return row;
378
+ }
379
+
380
+ function renderProjectContextGroup(doc, item, onTaskOpen) {
342
381
  const group = makeElement(doc, 'section', {
343
382
  className: `bb-pkw-context-group${item.state === 'loading' ? ' bb-pkw-context-group-loading' : ''}`,
344
383
  attrs: { 'aria-label': item.title, ...(item.state === 'loading' ? { 'aria-busy': 'true' } : {}) },
@@ -357,8 +396,13 @@ function renderProjectContextGroup(doc, item) {
357
396
  } else if (item.state === 'unknown') {
358
397
  group.append(workspaceNotice(doc, { label: '未確認', text: item.message ?? 'このセクションの取得状態は確認できません。', tone: 'warning' }));
359
398
  } else if (item.items.length > 0) {
360
- const list = makeElement(doc, 'ul', { className: 'bb-pkw-context-list' });
399
+ const taskList = item.items.every((entry) => entry.kind === 'task');
400
+ const list = makeElement(doc, 'ul', { className: `bb-pkw-context-list${taskList ? ' bb-pkw-context-task-list' : ''}` });
361
401
  for (const entry of item.items) {
402
+ if (entry.kind === 'task') {
403
+ list.append(renderProjectContextTaskRow(doc, entry, onTaskOpen));
404
+ continue;
405
+ }
362
406
  const row = makeElement(doc, 'li');
363
407
  row.append(makeElement(doc, 'strong', { text: entry.title }));
364
408
  if (entry.status) row.append(badge(doc, entry.status, 'muted'));
@@ -373,18 +417,12 @@ function renderProjectContextGroup(doc, item) {
373
417
  return group;
374
418
  }
375
419
 
376
- function projectContextInsertionAnchor(doc) {
377
- return makeElement(doc, 'span', {
378
- className: 'bb-pkw-context-anchor',
379
- attrs: { 'aria-hidden': 'true', hidden: 'true' },
380
- });
381
- }
382
-
383
420
  /** Renders supplementary context while preserving each section's own state. */
384
421
  export function renderProjectContext(doc, context, {
385
422
  sections: selectedSections = null,
386
423
  includeStatus = true,
387
424
  includeHeading = true,
425
+ onTaskOpen = null,
388
426
  } = {}) {
389
427
  if (!context) return null;
390
428
  const sections = Array.isArray(selectedSections)
@@ -407,7 +445,7 @@ export function renderProjectContext(doc, context, {
407
445
  section.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '補足情報のセクションはありません。' }));
408
446
  return section;
409
447
  }
410
- for (const item of sections) section.append(renderProjectContextGroup(doc, item));
448
+ for (const item of sections) section.append(renderProjectContextGroup(doc, item, onTaskOpen));
411
449
  return section;
412
450
  }
413
451
 
@@ -417,12 +455,7 @@ export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
417
455
  className: 'bb-pkw-panel bb-pkw-overview bb-pkw-overview-loading',
418
456
  attrs: { 'aria-label': '概要' },
419
457
  });
420
- const contextSections = Array.isArray(context?.sections) ? context.sections : [];
421
- const readySections = contextSections.filter((item) => item?.state === 'ok' && Array.isArray(item.items) && item.items.length > 0);
422
- const deferredSections = contextSections.filter((item) => !readySections.includes(item));
423
- if (readySections.length > 0) {
424
- panel.append(renderProjectContext(doc, context, { sections: readySections, includeStatus: false }));
425
- }
458
+ panel.append(renderContextDirectory(doc, context));
426
459
  const columns = makeElement(doc, 'div', { className: 'bb-pkw-columns' });
427
460
  for (const title of ['目的と現在地', '関係者と役割']) {
428
461
  const item = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-skeleton-section', attrs: { 'aria-label': title, 'aria-busy': 'true' } });
@@ -430,15 +463,6 @@ export function renderProjectKnowledgeSkeleton(doc, { context = null } = {}) {
430
463
  columns.append(item);
431
464
  }
432
465
  panel.append(columns);
433
- for (const title of ['記録された判断', '関連する情報']) {
434
- const item = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-skeleton-section', attrs: { 'aria-label': title, 'aria-busy': 'true' } });
435
- item.append(makeElement(doc, 'h3', { text: title }), skeletonBars(doc, 2));
436
- panel.append(item);
437
- }
438
- const contextBlock = readySections.length > 0
439
- ? (deferredSections.length > 0 ? renderProjectContext(doc, context, { sections: deferredSections, includeHeading: false }) : null)
440
- : renderProjectContext(doc, context ?? { state: 'loading', sections: [] });
441
- if (contextBlock) panel.append(contextBlock);
442
466
  return panel;
443
467
  }
444
468
 
@@ -475,6 +499,51 @@ function explicitSectionTotal(section) {
475
499
  return null;
476
500
  }
477
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
+
478
547
  function denseDate(value) {
479
548
  const text = textOrNull(value);
480
549
  if (!text) return '未確認';
@@ -524,14 +593,14 @@ function denseContextStateNotice(doc, section, fallback = 'この情報はまだ
524
593
  return workspaceNotice(doc, { label, text: section.message ?? fallback, tone, role: section.state === 'failed' ? 'alert' : 'status' });
525
594
  }
526
595
 
527
- function renderDenseItemRow(doc, item, { kind = 'task', onOpen, table = false } = {}) {
596
+ function renderDenseItemRow(doc, item, { kind = 'task', onOpen, table = false, includeSummary = true } = {}) {
528
597
  const row = makeElement(doc, 'li', { className: `bb-pkw-dense-item bb-pkw-dense-item-${kind}` });
529
598
  const head = makeElement(doc, 'div', { className: 'bb-pkw-dense-item-head' });
530
599
  const title = cleanName(item.title, item.name) ?? '名前不明';
531
600
  if (typeof onOpen === 'function' && item.id) head.append(denseButton(doc, title, 'bb-pkw-dense-link', () => onOpen(item.id), { 'data-item-id': item.id }));
532
601
  else head.append(makeElement(doc, 'strong', { text: title }));
533
602
  if (item.status) head.append(badge(doc, item.status, 'muted'));
534
- row.append(head, denseItemFacts(doc, item, { table, includeSummary: !table }));
603
+ row.append(head, denseItemFacts(doc, item, { table, includeSummary: !table && includeSummary }));
535
604
  return row;
536
605
  }
537
606
 
@@ -566,10 +635,10 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
566
635
  const tasks = contextTaskItems(context);
567
636
  const records = contextRecordItems(context);
568
637
  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);
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);
573
642
  const recentRecords = records.length > 0 ? sortDenseItems(records).slice(0, 4) : graphRecords.slice(0, 4);
574
643
  const updated = [...tasks, ...records]
575
644
  .filter((item) => item.updatedAt || item.eventAt)
@@ -588,7 +657,7 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
588
657
  denseMetric(doc, '仕事', taskMetric, taskNote),
589
658
  denseMetric(doc, '今日扱う仕事', taskSection?.state === 'ok' ? `${due.length}件` : '未確認', taskSection?.state === 'ok' ? '期限・更新が確認できる順' : 'タスクの状態を確認中'),
590
659
  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に記録された参加関係'),
660
+ denseMetric(doc, '関係者', participantEdges.length > 0 ? `${participantEdges.length}人` : '未確認', 'Graphに記録された参加関係'),
592
661
  );
593
662
  panel.append(metrics);
594
663
 
@@ -643,10 +712,13 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
643
712
  if (participants.length === 0) people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: 'Graphで確認できる関係者は未確認です。' }));
644
713
  else {
645
714
  const list = makeElement(doc, 'ul', { className: 'bb-pkw-dense-list' });
646
- for (const person of participants) {
715
+ for (const edge of participants) {
716
+ const person = edge.counterpart;
647
717
  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 }));
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(' · ') }));
650
722
  list.append(row);
651
723
  }
652
724
  people.append(list);
@@ -656,7 +728,7 @@ function renderDenseOverview(doc, detail, context, entities, actions = {}) {
656
728
  return panel;
657
729
  }
658
730
 
659
- function renderWorkList(doc, context, actions = {}) {
731
+ function renderWorkList(doc, context, actions = {}, state = {}) {
660
732
  const section = contextTaskSection(context);
661
733
  const allItems = contextTaskItems(context);
662
734
  const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-work-list', attrs: { role: 'tabpanel', 'aria-label': '仕事一覧' } });
@@ -681,10 +753,19 @@ function renderWorkList(doc, context, actions = {}) {
681
753
  makeElement(doc, 'option', { text: '期限未確認', attrs: { value: 'missing' } }),
682
754
  );
683
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 ?? '';
684
760
  panel.append(controls);
685
761
  const count = makeElement(doc, 'p', { className: 'bb-pkw-work-count' });
686
762
  const rows = makeElement(doc, 'div', { className: 'bb-pkw-work-rows', attrs: { role: 'list', 'aria-label': '仕事の行' } });
687
- 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);
688
769
  const renderRows = () => {
689
770
  const query = String(search.value ?? '').trim().toLocaleLowerCase();
690
771
  const selectedStatus = String(status.value ?? '');
@@ -699,8 +780,14 @@ function renderWorkList(doc, context, actions = {}) {
699
780
  return [item.title, item.summary, item.owner, item.assignee, item.status].filter(Boolean).some((value) => String(value).toLocaleLowerCase().includes(query));
700
781
  });
701
782
  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}件`}`;
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;
704
791
  rows.replaceChildren();
705
792
  if (display.length === 0) {
706
793
  rows.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: section?.state === 'ok' ? '条件に一致する仕事はありません。' : '仕事の行は未確認です。' }));
@@ -715,10 +802,71 @@ function renderWorkList(doc, context, actions = {}) {
715
802
  rows.append(row);
716
803
  }
717
804
  };
718
- search.addEventListener('input', renderRows);
719
- status.addEventListener('change', renderRows);
720
- owner.addEventListener('change', renderRows);
721
- 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);
722
870
  renderRows();
723
871
  return panel;
724
872
  }
@@ -779,7 +927,7 @@ function renderWorkDetail(doc, item, context, actions = {}) {
779
927
  function renderRecordDetail(doc, record, context, actions = {}) {
780
928
  const panel = makeElement(doc, 'section', { className: 'bb-pkw-panel bb-pkw-route bb-pkw-detail', attrs: { role: 'tabpanel', 'aria-label': '記録詳細' } });
781
929
  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' }));
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' }));
783
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);
784
932
  panel.append(heading);
785
933
  if (!record) {
@@ -814,12 +962,12 @@ function renderRecordDetail(doc, record, context, actions = {}) {
814
962
  return panel;
815
963
  }
816
964
 
817
- function renderOverview(doc, detail, context, edges, entities, onSelect, onContext, actions = {}) {
965
+ function renderOverview(doc, detail, context, entities, actions = {}) {
818
966
  const payload = projectDetailPayload(detail);
819
967
  const project = payload?.project;
820
968
  const panel = makeElement(doc, 'div', { className: 'bb-pkw-panel bb-pkw-overview', attrs: { role: 'tabpanel', 'aria-label': '概要' } });
821
969
  panel.append(renderDenseOverview(doc, detail, context, entities, actions));
822
- const columns = makeElement(doc, 'div', { className: 'bb-pkw-columns' });
970
+ panel.append(renderContextDirectory(doc, context, actions));
823
971
  const summary = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '目的と現在地' } });
824
972
  summary.append(makeElement(doc, 'h3', { text: '目的と現在地' }), workspaceDefinition(doc, [
825
973
  ['目的', textOrNull(project?.goal)],
@@ -828,83 +976,7 @@ function renderOverview(doc, detail, context, edges, entities, onSelect, onConte
828
976
  ['出典', sourceLabel(payload?.source)],
829
977
  ['読み取った時点', payload?.asOf ? formatDateTime(payload.asOf) : null],
830
978
  ]));
831
- columns.append(summary);
832
-
833
- const people = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '関係者' } });
834
- people.append(makeElement(doc, 'h3', { text: '関係者と役割' }));
835
- const participantEdges = Array.isArray(payload?.participants) ? payload.participants : [];
836
- if (participantEdges.length === 0) {
837
- people.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '関係者の記録はありません。' }));
838
- } else {
839
- const list = makeElement(doc, 'ul', { className: 'bb-pkw-people-list' });
840
- for (const edge of participantEdges) {
841
- const item = makeElement(doc, 'li');
842
- const counterpart = edge.counterpart ?? {};
843
- item.append(recordLink(doc, cleanName(counterpart.name, counterpart.id) ?? '名前不明', cleanId(counterpart.id), onSelect), badge(doc, relationLabel(edge.relation), edge.relation === 'accountable_for' ? 'accent' : 'muted'));
844
- const facts = [textOrNull(edge.role) ? `役割: ${edge.role}` : null, textOrNull(edge.context) ? `文脈: ${edge.context}` : null].filter(Boolean);
845
- if (facts.length > 0) item.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: facts.join(' · ') }));
846
- list.append(item);
847
- }
848
- people.append(list);
849
- }
850
- columns.append(people);
851
- panel.append(columns);
852
-
853
- const contextBlock = renderProjectContext(doc, context);
854
- if (contextBlock) {
855
- panel.append(contextBlock);
856
- if (typeof onContext === 'function') onContext(contextBlock, null);
857
- } else {
858
- const contextAnchor = projectContextInsertionAnchor(doc);
859
- panel.append(contextAnchor);
860
- if (typeof onContext === 'function') onContext(null, contextAnchor);
861
- }
862
-
863
- const decisions = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '記録された判断' } });
864
- decisions.append(makeElement(doc, 'h3', { text: '記録された判断' }));
865
- const decisionEdges = edges.filter((edge) => entities.get(edge.source)?.type === 'decision' || entities.get(edge.target)?.type === 'decision');
866
- if (decisionEdges.length === 0) decisions.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: 'このプロジェクトに結びつく判断の記録はありません。' }));
867
- else {
868
- const list = makeElement(doc, 'ul', { className: 'bb-pkw-decision-list' });
869
- for (const edge of decisionEdges) {
870
- const decisionId = entities.get(edge.source)?.type === 'decision' ? edge.source : edge.target;
871
- const decision = entities.get(decisionId);
872
- const item = makeElement(doc, 'li');
873
- item.append(recordLink(doc, decision?.name ?? decisionId, decisionId, onSelect), badge(doc, relationLabel(edge.relation), 'muted'));
874
- if (decision?.summary) item.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: decision.summary }));
875
- list.append(item);
876
- }
877
- decisions.append(list);
878
- }
879
- panel.append(decisions);
880
-
881
- const objects = makeElement(doc, 'section', { className: 'bb-pkw-section', attrs: { 'aria-label': '関連する情報' } });
882
- objects.append(makeElement(doc, 'h3', { text: '関連する情報' }));
883
- const groups = new Map();
884
- for (const entity of entities.values()) {
885
- if (entity.id === project?.id || entity.type === 'person' || entity.type === 'decision') continue;
886
- if (!groups.has(entity.type)) groups.set(entity.type, []);
887
- groups.get(entity.type).push(entity);
888
- }
889
- if (groups.size === 0) objects.append(makeElement(doc, 'p', { className: 'bb-pkw-muted', text: '関連する情報の記録はありません。' }));
890
- else {
891
- const grid = makeElement(doc, 'div', { className: 'bb-pkw-info-grid' });
892
- for (const [type, records] of groups) {
893
- const group = makeElement(doc, 'section', { className: 'bb-pkw-object', attrs: { 'aria-label': typeLabel(type) } });
894
- group.append(makeElement(doc, 'h4', { text: typeLabel(type) }));
895
- const list = makeElement(doc, 'ul');
896
- for (const record of records) {
897
- const item = makeElement(doc, 'li');
898
- item.append(recordLink(doc, record.name, record.id, onSelect));
899
- list.append(item);
900
- }
901
- group.append(list);
902
- grid.append(group);
903
- }
904
- objects.append(grid);
905
- }
906
- panel.append(objects);
907
-
979
+ panel.append(summary);
908
980
  const unknowns = projectKnowledgeUnknowns(detail);
909
981
  if (unknowns.length > 0) {
910
982
  const missing = makeElement(doc, 'section', { className: 'bb-pkw-section bb-pkw-unknown', attrs: { 'aria-label': '未記録・未確認' } });
@@ -955,13 +1027,16 @@ export function createProjectKnowledgeWorkspace({
955
1027
  let currentContext = context;
956
1028
  let activeView = 'overview';
957
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
+ };
958
1036
  // Keep direct references as well as the DOM lookup. The local test harness
959
1037
  // intentionally has no querySelector implementation, and a host can replace
960
1038
  // the panel while a detail read is still in flight.
961
1039
  let selectedPanel = null;
962
- let overviewPanel = null;
963
- let contextBlock = null;
964
- let contextAnchor = null;
965
1040
 
966
1041
  const root = makeElement(doc, 'section', {
967
1042
  className: 'bb-pkw',
@@ -1007,6 +1082,7 @@ export function createProjectKnowledgeWorkspace({
1007
1082
  activeTab = 'overview';
1008
1083
  activeView = 'overview';
1009
1084
  selectedContextId = null;
1085
+ selectedSectionId = null;
1010
1086
  render();
1011
1087
  }
1012
1088
 
@@ -1015,6 +1091,17 @@ export function createProjectKnowledgeWorkspace({
1015
1091
  activeTab = 'overview';
1016
1092
  activeView = 'work-list';
1017
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;
1018
1105
  render();
1019
1106
  }
1020
1107
 
@@ -1031,6 +1118,8 @@ export function createProjectKnowledgeWorkspace({
1031
1118
  activeTab = 'overview';
1032
1119
  activeView = 'record-detail';
1033
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;
1034
1123
  render();
1035
1124
  }
1036
1125
 
@@ -1245,39 +1334,17 @@ export function createProjectKnowledgeWorkspace({
1245
1334
 
1246
1335
  function replaceContextBlock(nextContext) {
1247
1336
  currentContext = nextContext;
1248
- if (destroyed) return;
1249
- if (activeTab === 'overview' && activeView !== 'overview') {
1250
- render();
1251
- return;
1252
- }
1253
- const nextBlock = renderProjectContext(doc, currentContext);
1254
- if (!nextBlock) {
1255
- if (contextBlock?.parentNode && typeof contextBlock.parentNode.replaceChild === 'function') {
1256
- const nextAnchor = projectContextInsertionAnchor(doc);
1257
- contextBlock.parentNode.replaceChild(nextAnchor, contextBlock);
1258
- contextBlock = null;
1259
- contextAnchor = nextAnchor;
1260
- } else if (contextBlock?.parentNode?.removeChild) {
1261
- contextBlock.parentNode.removeChild(contextBlock);
1262
- contextBlock = null;
1263
- }
1264
- return;
1265
- }
1266
- if (contextBlock?.parentNode && typeof contextBlock.parentNode.replaceChild === 'function') {
1267
- contextBlock.parentNode.replaceChild(nextBlock, contextBlock);
1268
- contextBlock = nextBlock;
1269
- return;
1270
- }
1271
- if (contextAnchor?.parentNode && typeof contextAnchor.parentNode.replaceChild === 'function') {
1272
- contextAnchor.parentNode.replaceChild(nextBlock, contextAnchor);
1273
- contextBlock = nextBlock;
1274
- contextAnchor = null;
1275
- return;
1276
- }
1277
- if (contextBlock?.replaceWith) {
1278
- contextBlock.replaceWith(nextBlock);
1279
- contextBlock = nextBlock;
1280
- 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);
1281
1348
  }
1282
1349
  }
1283
1350
 
@@ -1287,20 +1354,18 @@ export function createProjectKnowledgeWorkspace({
1287
1354
  teardownGraph();
1288
1355
  tabs.replaceChildren(tabButton('概要', 'overview'), tabButton('情報を探す', 'graph'));
1289
1356
  panels.replaceChildren();
1290
- contextBlock = null;
1291
- contextAnchor = null;
1292
- 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
+ };
1293
1361
  let overview;
1294
- 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));
1295
1364
  else if (activeView === 'work-detail') overview = renderWorkDetail(doc, findContextItem(selectedContextId, 'task'), currentContext, actions);
1296
1365
  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) => {
1298
- contextBlock = node;
1299
- contextAnchor = anchor;
1300
- }, actions);
1366
+ else overview = renderOverview(doc, payload, currentContext, overviewEntities, actions);
1301
1367
  overview.id = 'bb-pkw-panel-overview';
1302
1368
  overview.hidden = activeTab !== 'overview';
1303
- overviewPanel = overview;
1304
1369
  panels.append(overview);
1305
1370
  const graph = renderGraphPanel();
1306
1371
  graph.hidden = activeTab !== 'graph';