@unson/brainbase-mcp 0.41.3 → 0.41.4

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.
@@ -14,7 +14,6 @@ import {
14
14
  workspaceDefinition,
15
15
  workspaceDetailEmpty,
16
16
  workspaceLedger,
17
- workspaceNotice,
18
17
  workspacePageHeader,
19
18
  workspaceRailBlock,
20
19
  workspaceRailHead,
@@ -198,7 +197,7 @@ function normalizeNormalReference(value) {
198
197
  why: text(value.why) || null,
199
198
  usage: text(value.usage) || null,
200
199
  availability: text(value.availability).toLowerCase() || 'unknown',
201
- label: text(value.label ?? value.name) || ref,
200
+ label: text(value.label ?? value.name) || null,
202
201
  };
203
202
  }
204
203
 
@@ -806,23 +805,50 @@ function formatDate(value) {
806
805
  }
807
806
  }
808
807
 
808
+ function referenceLabel(ref) {
809
+ const label = text(ref?.label);
810
+ const id = text(ref?.id ?? ref?.ref);
811
+ if (!label || (id && label === id)) return '';
812
+ return label;
813
+ }
814
+
809
815
  function makeReferenceSummary(doc, references) {
810
816
  if (references === null || references === undefined) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照は未記録' });
811
817
  if (!references.length) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照はありません' });
818
+ const labeled = [];
819
+ let unlabeled = 0;
820
+ for (const ref of references) {
821
+ const label = referenceLabel(ref);
822
+ if (label) labeled.push(label);
823
+ else unlabeled += 1;
824
+ }
812
825
  const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-summary' });
813
- const first = references.slice(0, 2).map((ref) => ref.label || ref.id).join('、');
814
- wrap.append(makeWorkspaceElement(doc, 'strong', { text: first }));
815
- if (references.length > 2) wrap.append(makeWorkspaceElement(doc, 'small', { text: `ほか${references.length - 2}件` }));
826
+ if (labeled.length) {
827
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: labeled.slice(0, 2).join('、') }));
828
+ if (labeled.length > 2) wrap.append(makeWorkspaceElement(doc, 'small', { text: `ほか${labeled.length - 2}件` }));
829
+ }
830
+ if (unlabeled > 0) wrap.append(makeWorkspaceElement(doc, 'strong', { text: `名前のない参照 ${unlabeled}件` }));
816
831
  return wrap;
817
832
  }
818
833
 
834
+ function judgmentStatusLabel(status) {
835
+ const labels = {
836
+ resolved: '解決済み',
837
+ needs_clarification: '確認が必要',
838
+ needs_classification: '分類待ち',
839
+ needs_policy_resolution: '方針の確認待ち',
840
+ unknown: '状態は未記録',
841
+ };
842
+ const key = text(status);
843
+ if (!key) return '状態は未記録';
844
+ return labels[key] ?? '状態は未記録';
845
+ }
846
+
819
847
  function makeDecisionSummary(doc, row) {
820
848
  if (row.mode === 'normal') {
821
849
  const summary = text(row.judgment?.summary);
822
- const status = text(row.judgment?.status);
823
850
  const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-decision is-normal' });
824
851
  wrap.append(makeWorkspaceElement(doc, 'strong', { text: summary || '判断内容は未記録' }));
825
- wrap.append(makeWorkspaceElement(doc, 'small', { text: `${NORMAL_ENTRYPOINT_LABELS[row.entrypoint] ?? '入口不明'}${status ? `・${status}` : ''}` }));
826
852
  return wrap;
827
853
  }
828
854
  const summary = text(row.proof?.decision?.summary);
@@ -833,19 +859,28 @@ function makeDecisionSummary(doc, row) {
833
859
  return wrap;
834
860
  }
835
861
 
862
+ const REFERENCE_AVAILABILITY_LABELS = Object.freeze({
863
+ recorded: '今の権限で開けます',
864
+ permission_denied: '開く権限がありません',
865
+ unavailable: '本文はもうありません',
866
+ unknown: '未確認',
867
+ });
868
+
836
869
  function appendReferenceList(doc, references) {
837
870
  if (references === null || references === undefined) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照は記録されていません。' });
838
871
  if (!references.length) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照はありません。' });
839
872
  const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-reference-list' });
840
873
  for (const ref of references) {
841
874
  const item = makeWorkspaceElement(doc, 'li');
842
- item.append(makeWorkspaceElement(doc, 'strong', { text: ref.label || ref.id }));
843
- item.append(makeWorkspaceElement(doc, 'code', { text: `${ref.ref ?? ref.id}${ref.version ? `@${ref.version}` : ''}${ref.digest ? `#${ref.digest}` : ''}` }));
844
- if (ref.application && ref.application !== ref.label) item.append(makeWorkspaceElement(doc, 'span', { text: ref.application }));
845
- if (ref.availability && ref.availability !== 'recorded') {
846
- const availability = ref.availability === 'permission_denied' ? '権限がありません' : ref.availability === 'unavailable' ? '現在は参照できません' : '参照状態は未確認';
847
- item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-availability', text: availability }));
875
+ const label = referenceLabel(ref);
876
+ item.append(makeWorkspaceElement(doc, 'strong', { text: label || '名前のない参照' }));
877
+ const recordId = text(ref.ref ?? ref.id);
878
+ if (recordId) {
879
+ item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-record-id', text: `記録ID:${recordId}(コピー)` }));
848
880
  }
881
+ if (ref.application && ref.application !== ref.label && ref.application !== label) item.append(makeWorkspaceElement(doc, 'span', { text: ref.application }));
882
+ const availability = REFERENCE_AVAILABILITY_LABELS[text(ref.availability)] ?? REFERENCE_AVAILABILITY_LABELS.unknown;
883
+ item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-availability', text: availability }));
849
884
  if (ref.why) item.append(makeWorkspaceElement(doc, 'span', { text: `理由: ${ref.why}` }));
850
885
  if (ref.usage) item.append(makeWorkspaceElement(doc, 'span', { text: `用途: ${ref.usage}` }));
851
886
  const alternateLabels = (ref.labels ?? []).filter((label) => label && label !== ref.label);
@@ -1147,7 +1182,9 @@ function renderRail(doc, rail, row, detail, onClose, options = {}) {
1147
1182
  rail.append(workspaceDetailEmpty(doc, {
1148
1183
  mark: '↗',
1149
1184
  title: '履歴を選択',
1150
- text: '行を選ぶと、使った参照と行った判断を確認できます。',
1185
+ text: options.loading
1186
+ ? '行を選ぶと、使った参照と行った判断を確認できます。読み込み中の行も、届いたものから選べます。'
1187
+ : '行を選ぶと、使った参照と行った判断を確認できます。',
1151
1188
  }));
1152
1189
  return;
1153
1190
  }
@@ -1189,43 +1226,129 @@ function safeDocument(explicit) {
1189
1226
  return doc;
1190
1227
  }
1191
1228
 
1192
- function statusNotice(doc, phase, error, home, onRetry) {
1193
- if (phase === 'loading') return workspaceNotice(doc, { label: '読み込み中', text: '判断履歴を確認しています。', tone: 'info' });
1194
- if (phase === 'error') return workspaceNotice(doc, {
1195
- label: '取得できません',
1196
- text: [
1197
- { className: 'bb-jh-error-primary', text: '判断履歴を取得できませんでした。' },
1198
- error ? { className: 'bb-jh-error-detail', text: `詳細: ${error}` } : null,
1199
- workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry }),
1200
- ],
1201
- tone: 'danger',
1202
- role: 'alert',
1203
- });
1204
- if (home?.status === 'error') return workspaceNotice(doc, {
1205
- label: '取得に失敗',
1206
- text: ['判断の記録を確認できません。データが0件とは確認できていません。', workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry })],
1207
- tone: 'danger',
1208
- role: 'alert',
1209
- });
1210
- const sourceSummary = normalSourcesSummary(home);
1211
- if (home?.status === 'unavailable') return workspaceNotice(doc, {
1212
- label: '未接続',
1213
- text: [`保存された判断に接続できません${home.coverage?.storage ? `(${normalStorageLabel(home.coverage.storage)})` : ''}。データが0件とは確認できていません。`, sourceSummary ? `取得元: ${sourceSummary}` : null, workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry })],
1214
- tone: 'warning',
1215
- role: 'alert',
1216
- });
1217
- if (home?.status === 'invalid') return workspaceNotice(doc, {
1218
- label: '未確認',
1219
- text: ['判断の記録の応答を読み取れません。データが0件とは確認できていません。', workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry })],
1220
- tone: 'danger',
1221
- role: 'alert',
1222
- });
1223
- if (home?.status === 'partial') return workspaceNotice(doc, {
1224
- label: '取得できた範囲',
1225
- text: [`読み取れた判断だけを表示しています。${home.coverage?.storage ? `(${normalStorageLabel(home.coverage.storage)})` : ''}${home.coverage?.reason ? ` ${normalReasonLabel(home.coverage.reason)}` : ''}${home.coverage?.rejected ? ` 読み取れなかった記録は${home.coverage.rejected}件あります。` : ''}`, sourceSummary ? `取得元: ${sourceSummary}` : null, workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry })],
1226
- tone: 'warning',
1227
- role: 'status',
1229
+ const RESPONSE_ERROR_CODES = new Set([
1230
+ 'response_not_object', 'records_missing', 'sections_missing', 'unsupported_status',
1231
+ 'coverage_incomplete', 'record_rejected', 'record_invalid',
1232
+ ]);
1233
+
1234
+ function publicErrorText(error) {
1235
+ const code = text(error);
1236
+ if (!code) return '';
1237
+ if (RESPONSE_ERROR_CODES.has(code)) return '応答を読み取れませんでした';
1238
+ return '';
1239
+ }
1240
+
1241
+ function prefersReducedMotion() {
1242
+ try {
1243
+ return globalThis.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
1244
+ } catch {
1245
+ return false;
1246
+ }
1247
+ }
1248
+
1249
+ function sourceDetail(doc, home) {
1250
+ const details = makeWorkspaceElement(doc, 'details', { className: 'bb-jh-source-detail' });
1251
+ details.append(makeWorkspaceElement(doc, 'summary', { text: '取得元の詳細' }));
1252
+ const body = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-source-detail-body' });
1253
+ const storage = home?.coverage?.storage;
1254
+ if (storage) body.append(makeWorkspaceElement(doc, 'p', { text: `保存先: ${normalStorageLabel(storage)}` }));
1255
+ const sources = Array.isArray(home?.coverage?.sources) ? home.coverage.sources : [];
1256
+ for (const source of sources) {
1257
+ if (!source?.reason) continue;
1258
+ body.append(makeWorkspaceElement(doc, 'p', { text: `理由コード: ${source.reason}` }));
1259
+ }
1260
+ if (home?.coverage?.reason && !sources.some((source) => source?.reason === home.coverage.reason)) {
1261
+ body.append(makeWorkspaceElement(doc, 'p', { text: `理由コード: ${home.coverage.reason}` }));
1262
+ }
1263
+ const rejected = home?.coverage?.rejected;
1264
+ if (Number.isInteger(rejected) && rejected > 0) {
1265
+ body.append(makeWorkspaceElement(doc, 'p', { text: `読めなかった件数: ${rejected}` }));
1266
+ }
1267
+ if (text(home?.coverage?.reason) === 'journal_scan_capped') {
1268
+ body.append(makeWorkspaceElement(doc, 'p', { text: '記録が多いため、新しい順に上限まで読みました。古い判断が抜けている可能性があります。' }));
1269
+ }
1270
+ details.append(body);
1271
+ return details;
1272
+ }
1273
+
1274
+ function sourceChips(doc, sources) {
1275
+ const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-banner-sources' });
1276
+ for (const source of sources) wrap.append(makeWorkspaceElement(doc, 'span', { text: sourceChipText(source) }));
1277
+ return wrap;
1278
+ }
1279
+
1280
+ function historyBanner(doc, { label, tone, role, sources, home, note, reload }) {
1281
+ const banner = makeWorkspaceElement(doc, 'div', {
1282
+ className: `bb-jh-banner is-${tone}`,
1283
+ attrs: { role: role ?? 'status' },
1228
1284
  });
1285
+ banner.append(makeWorkspaceElement(doc, 'strong', { text: label }));
1286
+ if (sources?.length) banner.append(sourceChips(doc, sources));
1287
+ if (note) banner.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-banner-note', text: note }));
1288
+ if (home) banner.append(sourceDetail(doc, home));
1289
+ if (reload) banner.append(reload);
1290
+ return banner;
1291
+ }
1292
+
1293
+ function statusNotice(doc, phase, error, home, onRetry, { loading = false, slow = false, arrived = null } = {}) {
1294
+ const reload = workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry, attrs: { 'data-action': 'reload' } });
1295
+ if (phase === 'error' || home?.status === 'error' || home?.status === 'invalid') {
1296
+ const detail = publicErrorText(error);
1297
+ return historyBanner(doc, {
1298
+ label: '取得できません',
1299
+ tone: 'danger',
1300
+ role: 'alert',
1301
+ note: `判断履歴を取得できませんでした。データが0件とは確認できていません。${detail ? ` ${detail}` : ''}`,
1302
+ home: error ? { coverage: { reason: text(error) } } : home,
1303
+ reload,
1304
+ });
1305
+ }
1306
+ if (home?.status === 'unavailable') {
1307
+ return historyBanner(doc, {
1308
+ label: '取得できません',
1309
+ tone: 'danger',
1310
+ role: 'alert',
1311
+ sources: home.coverage?.sources,
1312
+ home,
1313
+ note: '判断の記録に接続できません。データが0件とは確認できていません。',
1314
+ reload,
1315
+ });
1316
+ }
1317
+ if (loading) {
1318
+ const progressHome = arrived ?? home;
1319
+ const counts = new Map();
1320
+ for (const record of progressHome?.records ?? []) {
1321
+ const key = normalEntrypoint(record.entrypoint);
1322
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1323
+ }
1324
+ const known = Array.isArray(progressHome?.coverage?.sources) && progressHome.coverage.sources.length
1325
+ ? progressHome.coverage.sources
1326
+ : NORMAL_ENTRYPOINTS.filter((entrypoint) => entrypoint !== 'unknown').map((entrypoint) => ({ entrypoint, status: 'pending', reason: null }));
1327
+ const sources = known.map((source) => {
1328
+ const count = counts.get(normalEntrypoint(source.entrypoint)) ?? 0;
1329
+ if (count > 0) return { entrypoint: source.entrypoint, status: 'partial', retrieved: count, reason: null };
1330
+ if (source.status === 'unavailable') return { entrypoint: source.entrypoint, status: 'unavailable', reason: source.reason ?? null };
1331
+ return { entrypoint: source.entrypoint, status: 'pending', reason: null };
1332
+ });
1333
+ return historyBanner(doc, {
1334
+ label: '読み込み中',
1335
+ tone: 'info',
1336
+ role: 'status',
1337
+ sources,
1338
+ note: slow
1339
+ ? '時間がかかっています。届いた分だけ先に見られます。'
1340
+ : '取得元ごとに届いた順に表示します',
1341
+ });
1342
+ }
1343
+ if (home?.status === 'partial') {
1344
+ return historyBanner(doc, {
1345
+ label: '一部の記録だけを表示中',
1346
+ tone: 'warning',
1347
+ role: 'status',
1348
+ sources: home.coverage?.sources,
1349
+ home,
1350
+ });
1351
+ }
1229
1352
  return null;
1230
1353
  }
1231
1354
 
@@ -1255,38 +1378,51 @@ function createSearch(doc, value, onInput) {
1255
1378
  }
1256
1379
 
1257
1380
  function normalStorageLabel(storage) {
1258
- if (storage === 'local') return '端末';
1259
- if (storage === 'server') return 'サーバー';
1260
- return storage || '接続先不明';
1381
+ if (storage === 'local') return 'この端末';
1382
+ if (storage === 'server') return '組織のサーバー';
1383
+ return '接続先不明';
1261
1384
  }
1262
1385
 
1263
1386
  function normalReasonLabel(reason) {
1264
1387
  const labels = {
1388
+ journal_scan_capped: '読み込み上限まで',
1389
+ source_not_connected: '未接続',
1265
1390
  unconnected: '未接続',
1266
- mana_not_connected: '未接続(Mana)',
1267
- company_os_native_source_not_connected: 'Company OSの判断実行履歴が未接続',
1268
- journal_unavailable: '判断記録がありません',
1269
- journal_unreadable: '判断記録を読み取れません',
1391
+ mana_not_connected: '未接続',
1392
+ company_os_native_source_not_connected: '実行履歴は未接続',
1393
+ journal_unavailable: '判断の記録が見つかりません',
1394
+ journal_unreadable: '判断の記録を読み取れません',
1395
+ some_journal_records_unreadable: '一部の記録を読み取れません',
1396
+ project_scope_unverified: 'プロジェクトの範囲を確かめられていません',
1397
+ project_scope_forbidden: 'このプロジェクトを見る権限がありません',
1398
+ journal_not_directory: '記録の保存場所が正しくありません',
1399
+ journal_symlink_rejected: '記録の保存場所が正しくありません',
1400
+ permission_denied: '見る権限がありません',
1401
+ judgment_history_partial: '一部だけ取得',
1402
+ judgment_history_unavailable: '取得できません',
1403
+ judgment_journal_unavailable: '判断の記録に接続できません',
1270
1404
  };
1271
- return labels[reason] ?? reason;
1405
+ if (!reason) return '';
1406
+ return labels[reason] ?? '理由を確かめられません';
1272
1407
  }
1273
1408
 
1274
1409
  function normalSourceStatusLabel(status) {
1275
1410
  const labels = {
1276
- available: '接続済み',
1277
- partial: '一部取得',
1411
+ available: '取得済み',
1412
+ partial: '一部',
1278
1413
  unavailable: '未接続',
1279
1414
  };
1280
1415
  return labels[status] ?? '未確認';
1281
1416
  }
1282
1417
 
1283
- function normalSourcesSummary(home) {
1284
- if (home?.mode !== 'normal' || !Array.isArray(home.coverage?.sources) || home.coverage.sources.length === 0) return '';
1285
- return home.coverage.sources.map((source) => {
1286
- const entrypoint = NORMAL_ENTRYPOINT_LABELS[normalEntrypoint(source.entrypoint)] ?? '入口不明';
1287
- const reason = source.reason ? `(${normalReasonLabel(source.reason)})` : '';
1288
- return `${entrypoint}: ${normalSourceStatusLabel(source.status)}${reason}`;
1289
- }).join('、');
1418
+ function sourceChipText(source) {
1419
+ const name = NORMAL_ENTRYPOINT_LABELS[normalEntrypoint(source.entrypoint)] ?? '入口不明';
1420
+ if (source?.status === 'pending' || source?.status === 'loading') return `${name}:確認中…`;
1421
+ if (Number.isInteger(source?.retrieved)) return `${name}:${source.retrieved}件まで届きました`;
1422
+ const statusLabel = normalSourceStatusLabel(source.status);
1423
+ const reasonLabel = source.reason ? normalReasonLabel(source.reason) : '';
1424
+ const reason = reasonLabel && reasonLabel !== statusLabel ? `(${reasonLabel})` : '';
1425
+ return `${name}:${statusLabel}${reason}`;
1290
1426
  }
1291
1427
 
1292
1428
  function createFilterSelect(doc, { label, value, options, onChange, className = '' }) {
@@ -1309,12 +1445,12 @@ function createFilterSelect(doc, { label, value, options, onChange, className =
1309
1445
  function normalFilterControls(doc, home, state, onChange) {
1310
1446
  const filters = home?.filters ?? {};
1311
1447
  const projects = [...(filters.projects ?? [])];
1312
- const knownProjects = (home.records ?? []).map((record) => record.projectCode).filter(Boolean);
1448
+ const knownProjects = (home?.records ?? []).map((record) => record.projectCode).filter(Boolean);
1313
1449
  for (const project of knownProjects) {
1314
1450
  if (!projects.some((option) => option.value === project)) projects.push({ value: project, label: project });
1315
1451
  }
1316
1452
  const entrypoints = [...(filters.entrypoints ?? [])];
1317
- const knownEntrypoints = (home.records ?? []).map((record) => record.entrypoint).filter(Boolean);
1453
+ const knownEntrypoints = (home?.records ?? []).map((record) => record.entrypoint).filter(Boolean);
1318
1454
  for (const entrypoint of [...NORMAL_ENTRYPOINTS, ...knownEntrypoints]) {
1319
1455
  if (!entrypoints.some((option) => option.value === entrypoint)) {
1320
1456
  entrypoints.push({ value: entrypoint, label: NORMAL_ENTRYPOINT_LABELS[entrypoint] ?? entrypoint });
@@ -1350,6 +1486,71 @@ function paginationControls(doc, aggregate, onNext) {
1350
1486
  return wrap;
1351
1487
  }
1352
1488
 
1489
+ function makeEntrypointStatus(doc, row) {
1490
+ const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-entrypoint' });
1491
+ if (row.mode !== 'normal') {
1492
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: row.delegated ? 'Brainbase' : '本人' }));
1493
+ return wrap;
1494
+ }
1495
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: NORMAL_ENTRYPOINT_LABELS[row.entrypoint] ?? '入口不明' }));
1496
+ wrap.append(makeWorkspaceElement(doc, 'small', { text: judgmentStatusLabel(row.judgment?.status) }));
1497
+ return wrap;
1498
+ }
1499
+
1500
+ function skeletonRows(doc, count = 5) {
1501
+ return Array.from({ length: count }, (_, index) => ({
1502
+ key: `skeleton-${index}`,
1503
+ className: 'bb-jh-skeleton-row',
1504
+ cells: [
1505
+ makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-date' }),
1506
+ makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-decision' }),
1507
+ makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-reference' }),
1508
+ makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-status' }),
1509
+ ],
1510
+ }));
1511
+ }
1512
+
1513
+ function summaryLine(doc, aggregate, { loading = false } = {}) {
1514
+ const line = makeWorkspaceElement(doc, 'p', { className: 'bb-jh-summary' });
1515
+ const retrieved = Number.isInteger(aggregate?.retrievedCount) ? aggregate.retrievedCount : (aggregate?.rows?.length ?? 0);
1516
+ const complete = aggregate?.countComplete === true && Number.isInteger(aggregate?.totalCount);
1517
+ const showCount = complete || retrieved > 0;
1518
+ if (showCount) {
1519
+ line.append(makeWorkspaceElement(doc, 'strong', { text: `${complete ? aggregate.totalCount : retrieved}件` }));
1520
+ line.append(makeWorkspaceElement(doc, 'span', { text: ' 取得済み' }));
1521
+ } else {
1522
+ line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
1523
+ line.append(makeWorkspaceElement(doc, 'span', { text: ' 取得済み' }));
1524
+ }
1525
+ if (!complete) {
1526
+ line.append(makeWorkspaceElement(doc, 'span', {
1527
+ className: 'bb-jh-summary-note',
1528
+ text: loading ? '(読み込み中・全件数は未確認)' : '(全件数は未確認)',
1529
+ }));
1530
+ }
1531
+ const references = loading ? null : (aggregate?.stats?.referenceCount ?? aggregate?.stats?.references);
1532
+ const confirmed = loading ? null : (aggregate?.stats?.confirmedOutcomes ?? aggregate?.stats?.outcomes);
1533
+ line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 使った参照 ' }));
1534
+ if (Number.isInteger(references)) line.append(makeWorkspaceElement(doc, 'strong', { text: `${references}件` }));
1535
+ else line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
1536
+ line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 結果を確認済み ' }));
1537
+ if (Number.isInteger(confirmed)) line.append(makeWorkspaceElement(doc, 'strong', { text: `${confirmed}件` }));
1538
+ else line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
1539
+ return line;
1540
+ }
1541
+
1542
+ function formatCheckedAt(value) {
1543
+ const date = new Date(value);
1544
+ if (!Number.isFinite(date.getTime())) return '';
1545
+ try {
1546
+ return new Intl.DateTimeFormat('ja-JP', {
1547
+ timeZone: 'Asia/Tokyo', hour: '2-digit', minute: '2-digit', hour12: false,
1548
+ }).format(date);
1549
+ } catch {
1550
+ return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
1551
+ }
1552
+ }
1553
+
1353
1554
  function makeRows(doc, aggregate, selectedKey, onSelect) {
1354
1555
  return aggregate.rows.map((row) => ({
1355
1556
  key: row.key,
@@ -1359,8 +1560,9 @@ function makeRows(doc, aggregate, selectedKey, onSelect) {
1359
1560
  onSelect,
1360
1561
  cells: [
1361
1562
  makeWorkspaceElement(doc, 'span', { className: 'bb-jh-date', text: formatDate(row.recordedAt) }),
1362
- makeReferenceSummary(doc, row.references),
1363
1563
  makeDecisionSummary(doc, row),
1564
+ makeReferenceSummary(doc, row.references),
1565
+ makeEntrypointStatus(doc, row),
1364
1566
  ],
1365
1567
  }));
1366
1568
  }
@@ -1377,6 +1579,8 @@ export function createJudgmentHistoryUI({
1377
1579
  autoLoad = true,
1378
1580
  pageHeader = true,
1379
1581
  now = new Date(),
1582
+ skeletonDelayMs = 300,
1583
+ slowDelayMs = 10000,
1380
1584
  } = {}) {
1381
1585
  if (!root) throw new TypeError('root is required');
1382
1586
  const doc = safeDocument(explicitDocument);
@@ -1409,8 +1613,51 @@ export function createJudgmentHistoryUI({
1409
1613
  focusSearch: false,
1410
1614
  lastFocusedKey: null,
1411
1615
  pendingDecisionId: null,
1616
+ showSkeleton: false,
1617
+ slow: false,
1618
+ arrived: null,
1619
+ checkedAt: null,
1620
+ skeletonTimer: null,
1621
+ slowTimer: null,
1412
1622
  };
1413
1623
 
1624
+ function clearLoadTimers() {
1625
+ if (state.skeletonTimer !== null) {
1626
+ clearTimeout(state.skeletonTimer);
1627
+ state.skeletonTimer = null;
1628
+ }
1629
+ if (state.slowTimer !== null) {
1630
+ clearTimeout(state.slowTimer);
1631
+ state.slowTimer = null;
1632
+ }
1633
+ }
1634
+
1635
+ function scheduleLoadTimers(generation) {
1636
+ clearLoadTimers();
1637
+ const skeletonDelay = Number.isFinite(skeletonDelayMs) ? Math.max(0, skeletonDelayMs) : 300;
1638
+ const slowDelay = Number.isFinite(slowDelayMs) ? Math.max(0, slowDelayMs) : 10000;
1639
+ const stillLoading = () => generation === state.generation && !state.disposed && state.phase === 'loading';
1640
+ if (skeletonDelay === 0) state.showSkeleton = true;
1641
+ else {
1642
+ state.skeletonTimer = setTimeout(() => {
1643
+ if (!stillLoading()) return;
1644
+ state.showSkeleton = true;
1645
+ render();
1646
+ }, skeletonDelay);
1647
+ }
1648
+ if (slowDelay === 0) {
1649
+ state.slow = true;
1650
+ state.showSkeleton = true;
1651
+ } else {
1652
+ state.slowTimer = setTimeout(() => {
1653
+ if (!stillLoading()) return;
1654
+ state.slow = true;
1655
+ state.showSkeleton = true;
1656
+ render();
1657
+ }, slowDelay);
1658
+ }
1659
+ }
1660
+
1414
1661
  function closeSelected() {
1415
1662
  if (!state.selectedKey || state.disposed) return false;
1416
1663
  state.lastFocusedKey = state.selectedKey;
@@ -1661,28 +1908,43 @@ export function createJudgmentHistoryUI({
1661
1908
 
1662
1909
  function render() {
1663
1910
  if (state.disposed) return;
1911
+ if (state.phase === 'loading' && !state.showSkeleton && !state.arrived) return;
1664
1912
  const previousSearchInput = findFirst(root, (node) => node.tagName === 'INPUT');
1665
1913
  const previousSearchSelection = previousSearchInput && Number.isInteger(previousSearchInput.selectionStart)
1666
1914
  ? { start: previousSearchInput.selectionStart, end: previousSearchInput.selectionEnd ?? previousSearchInput.selectionStart }
1667
1915
  : null;
1668
1916
  const previousSearchFocused = previousSearchInput && doc.activeElement === previousSearchInput;
1669
- const aggregate = state.aggregate ?? {
1670
- status: state.home?.status ?? 'unavailable', rows: [], visibleCount: 0, stats: {}, coverage: state.home?.coverage ?? null,
1671
- };
1672
- const wrapper = makeWorkspaceElement(doc, 'div', { className: 'bb-jh', attrs: { 'data-contract-version': JUDGMENT_HISTORY_UI_CONTRACT_VERSION } });
1917
+ const loading = state.phase === 'loading';
1918
+ const sourceHome = loading ? state.arrived : state.home;
1919
+ const aggregate = loading
1920
+ ? (sourceHome
1921
+ ? aggregateJudgmentHistory(sourceHome, { now: readNow(), period: state.period, query: state.query })
1922
+ : { status: 'partial', rows: [], visibleCount: 0, stats: {}, coverage: null, countComplete: false, retrievedCount: 0, mode: 'normal' })
1923
+ : (state.aggregate ?? {
1924
+ status: state.home?.status ?? 'unavailable', rows: [], visibleCount: 0, stats: {}, coverage: state.home?.coverage ?? null,
1925
+ });
1926
+ const wrapper = makeWorkspaceElement(doc, 'div', {
1927
+ className: `bb-jh${prefersReducedMotion() ? ' is-reduced-motion' : ''}`,
1928
+ attrs: { 'data-contract-version': JUDGMENT_HISTORY_UI_CONTRACT_VERSION, 'aria-busy': loading ? 'true' : 'false' },
1929
+ });
1673
1930
  if (pageHeader) {
1674
1931
  const context = currentPage();
1675
1932
  wrapper.append(workspacePageHeader(doc, {
1676
1933
  crumbs: context.crumbs,
1677
1934
  title: '判断の履歴',
1678
- lead: 'Brainbaseにどんな参照が入り、どんな判断を代わりに行ったかを見返せます。',
1935
+ lead: '参照した内容と、Brainbaseが代わりに行った判断を見返す',
1679
1936
  source: context.source,
1680
1937
  }));
1681
1938
  }
1682
- const notice = statusNotice(doc, state.phase, state.error, state.home, () => void load());
1939
+ const notice = statusNotice(doc, state.phase, state.error, loading ? (state.arrived ?? state.home) : state.home, () => void load(), {
1940
+ loading,
1941
+ slow: state.slow,
1942
+ arrived: state.arrived,
1943
+ });
1683
1944
  if (notice) wrapper.append(notice);
1684
- if (state.phase === 'loading' || state.phase === 'error' || state.home?.status === 'unavailable' || state.home?.status === 'error' || state.home?.status === 'invalid') {
1685
- wrapper.append(workspaceDetailEmpty(doc, { mark: '…', title: state.phase === 'loading' ? '判断履歴を読み込んでいます' : '判断履歴を確認できません', text: '再試行すると最新の状態を確認します。' }));
1945
+ const failed = state.phase === 'error' || state.home?.status === 'unavailable' || state.home?.status === 'error' || state.home?.status === 'invalid';
1946
+ if (failed) {
1947
+ wrapper.append(workspaceDetailEmpty(doc, { mark: '…', title: '判断履歴を確認できません', text: '再試行すると最新の状態を確認します。' }));
1686
1948
  root.replaceChildren(wrapper);
1687
1949
  renderRail(doc, rail, null, null);
1688
1950
  return;
@@ -1692,53 +1954,63 @@ export function createJudgmentHistoryUI({
1692
1954
  state.period = period;
1693
1955
  state.selectedKey = null;
1694
1956
  state.cursor = null;
1695
- if (normalMode()) void load({ append: false });
1957
+ if (loading || normalMode()) void load({ append: false });
1696
1958
  else {
1697
1959
  state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
1698
1960
  render();
1699
1961
  }
1700
1962
  }));
1701
- if (normalMode()) controls.append(normalFilterControls(doc, state.home, state, handleNormalFilterChange));
1963
+ const showNormalFilters = loading ? sourceHome?.mode !== 'legacy' : normalMode();
1964
+ if (showNormalFilters) controls.append(normalFilterControls(doc, sourceHome ?? state.home, state, handleNormalFilterChange));
1702
1965
  controls.append(createSearch(doc, state.query, (query) => {
1703
1966
  state.query = String(query);
1704
1967
  state.focusSearch = true;
1705
1968
  state.selectedKey = null;
1706
- state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
1969
+ const searchable = loading ? sourceHome : state.home;
1970
+ if (searchable) state.aggregate = aggregateJudgmentHistory(searchable, { now: readNow(), period: state.period, query: state.query });
1707
1971
  render();
1708
1972
  }));
1709
- controls.append(workspaceButton(doc, {
1973
+ const toolbarStatus = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-toolbar-status' });
1974
+ if (loading) {
1975
+ toolbarStatus.append(makeWorkspaceElement(doc, 'span', {
1976
+ className: 'bb-jh-checked',
1977
+ text: '確認しています…',
1978
+ attrs: { role: 'status' },
1979
+ }));
1980
+ } else if (state.checkedAt) {
1981
+ const stamp = formatCheckedAt(state.checkedAt);
1982
+ if (stamp) toolbarStatus.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-checked', text: `${stamp}に確認` }));
1983
+ }
1984
+ toolbarStatus.append(workspaceButton(doc, {
1710
1985
  text: '再読み込み',
1711
1986
  variant: 'quiet',
1987
+ disabled: loading,
1712
1988
  onClick: () => void load(),
1713
1989
  attrs: { 'aria-label': '判断履歴を再読み込み', 'data-action': 'reload' },
1714
1990
  }));
1991
+ controls.append(toolbarStatus);
1715
1992
  wrapper.append(controls);
1716
- const normalSourceSummary = normalSourcesSummary(state.home);
1717
- wrapper.append(makeWorkspaceElement(doc, 'p', {
1718
- className: 'bb-jh-reference-note',
1719
- text: normalMode()
1720
- ? `保存された判断の理由、当時の参照、案、実行と結果を表示しています。参照の本文は現在の権限で確認できる場合だけ開きます。${normalSourceSummary ? ` 取得元: ${normalSourceSummary}` : ''}`
1721
- : '判断に残った参照だけを表示しています。実行結果の証拠は参照件数に含めません。',
1722
- }));
1993
+ wrapper.append(summaryLine(doc, aggregate, { loading }));
1723
1994
  const emptyText = state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
1724
1995
  ? 'この期間に取得できた判断履歴はありません。全件数が未確認のため0件とは確認できません。'
1725
1996
  : 'この期間の判断履歴はありません。';
1997
+ const dataRows = makeRows(doc, aggregate, state.selectedKey, (key) => {
1998
+ state.lastFocusedKey = key;
1999
+ state.selectedKey = key;
2000
+ state.detailPhase = 'idle';
2001
+ state.detailError = null;
2002
+ render();
2003
+ const selectedRow = aggregate.rows.find((row) => row.key === key);
2004
+ if (selectedRow?.mode === 'normal') void loadDetail(selectedRow);
2005
+ });
1726
2006
  wrapper.append(workspaceLedger(doc, {
1727
2007
  className: 'bb-jh-ledger',
1728
2008
  ariaLabel: '判断履歴',
1729
- columns: ['日時', '使った参照', '行った判断'],
1730
- rows: makeRows(doc, aggregate, state.selectedKey, (key) => {
1731
- state.lastFocusedKey = key;
1732
- state.selectedKey = key;
1733
- state.detailPhase = 'idle';
1734
- state.detailError = null;
1735
- render();
1736
- const selectedRow = aggregate.rows.find((row) => row.key === key);
1737
- if (selectedRow?.mode === 'normal') void loadDetail(selectedRow);
1738
- }),
2009
+ columns: ['日時', '行った判断', '使った参照', '入口・状態'],
2010
+ rows: loading ? [...dataRows, ...skeletonRows(doc)] : dataRows,
1739
2011
  empty: emptyText,
1740
2012
  }));
1741
- const pagination = paginationControls(doc, aggregate, () => {
2013
+ const pagination = loading ? null : paginationControls(doc, aggregate, () => {
1742
2014
  if (aggregate.pagination?.nextCursor) {
1743
2015
  state.cursor = aggregate.pagination.nextCursor;
1744
2016
  void load({ cursor: state.cursor, append: true });
@@ -1776,7 +2048,7 @@ export function createJudgmentHistoryUI({
1776
2048
  onCopyReference: (payload) => void copyReference(payload),
1777
2049
  }
1778
2050
  : {};
1779
- renderRail(doc, rail, selected, selectedDetail, selected ? closeSelected : null, railOptions);
2051
+ renderRail(doc, rail, selected, selectedDetail, selected ? closeSelected : null, { ...railOptions, loading });
1780
2052
  if (state.lastFocusedKey) {
1781
2053
  const row = findFirst(root, (node) => attributeValue(node, 'data-key') === state.lastFocusedKey);
1782
2054
  row?.focus?.();
@@ -1787,26 +2059,39 @@ export function createJudgmentHistoryUI({
1787
2059
  async function load({ cursor = null, append = false } = {}) {
1788
2060
  if (state.disposed) return null;
1789
2061
  const generation = ++state.generation;
1790
- state.phase = 'loading';
1791
2062
  state.error = null;
1792
2063
  if (!append) {
2064
+ state.phase = 'loading';
1793
2065
  state.home = null;
1794
2066
  state.aggregate = null;
2067
+ state.arrived = null;
2068
+ state.showSkeleton = false;
2069
+ state.slow = false;
2070
+ scheduleLoadTimers(generation);
2071
+ render();
1795
2072
  }
1796
- render();
1797
2073
  if (!request) {
1798
2074
  if (generation !== state.generation || state.disposed) return null;
2075
+ clearLoadTimers();
2076
+ state.showSkeleton = false;
1799
2077
  state.phase = 'error';
1800
2078
  state.error = 'fetch_unavailable';
1801
2079
  render();
1802
2080
  return null;
1803
2081
  }
1804
2082
  try {
2083
+ const report = (payload) => {
2084
+ if (generation !== state.generation || state.disposed || state.phase !== 'loading') return;
2085
+ const normalized = normalizeJudgmentHistoryHome(payload);
2086
+ if (!normalized || normalized.status === 'invalid') return;
2087
+ state.arrived = normalized;
2088
+ render();
2089
+ };
1805
2090
  let activePath = normalizedBasePath;
1806
2091
  let response = await request(historyPath(normalizedBasePath, cursor), {
1807
2092
  method: 'GET',
1808
2093
  headers: token ? { 'x-brainbase-review-token': token } : undefined,
1809
- });
2094
+ }, { report });
1810
2095
  if (generation !== state.generation || state.disposed) return null;
1811
2096
  if (response?.status === 404 && legacyBasePath && !append && !state.project && !state.entrypoint && !state.cursor) {
1812
2097
  activePath = legacyBasePath;
@@ -1837,6 +2122,11 @@ export function createJudgmentHistoryUI({
1837
2122
  state.entrypoint = state.entrypoint ?? state.home.filters?.entrypoint ?? null;
1838
2123
  state.cursor = state.home.pagination?.nextCursor ?? null;
1839
2124
  }
2125
+ clearLoadTimers();
2126
+ state.showSkeleton = false;
2127
+ state.slow = false;
2128
+ state.arrived = null;
2129
+ state.checkedAt = readNow();
1840
2130
  state.phase = 'ready';
1841
2131
  state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
1842
2132
  if (state.pendingDecisionId) {
@@ -1848,6 +2138,9 @@ export function createJudgmentHistoryUI({
1848
2138
  return state.aggregate;
1849
2139
  } catch (error) {
1850
2140
  if (generation !== state.generation || state.disposed) return null;
2141
+ clearLoadTimers();
2142
+ state.showSkeleton = false;
2143
+ state.slow = false;
1851
2144
  state.phase = 'error';
1852
2145
  state.error = error instanceof Error ? error.message : String(error);
1853
2146
  render();
@@ -1875,6 +2168,7 @@ export function createJudgmentHistoryUI({
1875
2168
  if (state.disposed) return;
1876
2169
  state.disposed = true;
1877
2170
  state.generation += 1;
2171
+ clearLoadTimers();
1878
2172
  root.removeEventListener?.('keydown', handleKeyDown);
1879
2173
  rail?.removeEventListener?.('keydown', handleKeyDown);
1880
2174
  root.replaceChildren();