@unson/brainbase-mcp 0.41.3 → 0.41.6

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.
@@ -11,10 +11,14 @@
11
11
  import {
12
12
  makeWorkspaceElement,
13
13
  workspaceButton,
14
+ workspaceDialog,
15
+ workspaceFilterPopover,
16
+ workspaceErrorCard,
17
+ workspaceLoadingCard,
18
+ workspaceDismissNotification,
14
19
  workspaceDefinition,
15
20
  workspaceDetailEmpty,
16
21
  workspaceLedger,
17
- workspaceNotice,
18
22
  workspacePageHeader,
19
23
  workspaceRailBlock,
20
24
  workspaceRailHead,
@@ -198,7 +202,7 @@ function normalizeNormalReference(value) {
198
202
  why: text(value.why) || null,
199
203
  usage: text(value.usage) || null,
200
204
  availability: text(value.availability).toLowerCase() || 'unknown',
201
- label: text(value.label ?? value.name) || ref,
205
+ label: text(value.label ?? value.name) || null,
202
206
  };
203
207
  }
204
208
 
@@ -806,23 +810,50 @@ function formatDate(value) {
806
810
  }
807
811
  }
808
812
 
813
+ function referenceLabel(ref) {
814
+ const label = text(ref?.label);
815
+ const id = text(ref?.id ?? ref?.ref);
816
+ if (!label || (id && label === id)) return '';
817
+ return label;
818
+ }
819
+
809
820
  function makeReferenceSummary(doc, references) {
810
821
  if (references === null || references === undefined) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照は未記録' });
811
822
  if (!references.length) return makeWorkspaceElement(doc, 'span', { className: 'bb-jh-unrecorded', text: '参照はありません' });
823
+ const labeled = [];
824
+ let unlabeled = 0;
825
+ for (const ref of references) {
826
+ const label = referenceLabel(ref);
827
+ if (label) labeled.push(label);
828
+ else unlabeled += 1;
829
+ }
812
830
  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}件` }));
831
+ if (labeled.length) {
832
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: labeled.slice(0, 2).join('、') }));
833
+ if (labeled.length > 2) wrap.append(makeWorkspaceElement(doc, 'small', { text: `ほか${labeled.length - 2}件` }));
834
+ }
835
+ if (unlabeled > 0) wrap.append(makeWorkspaceElement(doc, 'strong', { text: `名前のない参照 ${unlabeled}件` }));
816
836
  return wrap;
817
837
  }
818
838
 
839
+ function judgmentStatusLabel(status) {
840
+ const labels = {
841
+ resolved: '解決済み',
842
+ needs_clarification: '確認が必要',
843
+ needs_classification: '分類待ち',
844
+ needs_policy_resolution: '方針の確認待ち',
845
+ unknown: '状態は未記録',
846
+ };
847
+ const key = text(status);
848
+ if (!key) return '状態は未記録';
849
+ return labels[key] ?? '状態は未記録';
850
+ }
851
+
819
852
  function makeDecisionSummary(doc, row) {
820
853
  if (row.mode === 'normal') {
821
854
  const summary = text(row.judgment?.summary);
822
- const status = text(row.judgment?.status);
823
855
  const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-decision is-normal' });
824
856
  wrap.append(makeWorkspaceElement(doc, 'strong', { text: summary || '判断内容は未記録' }));
825
- wrap.append(makeWorkspaceElement(doc, 'small', { text: `${NORMAL_ENTRYPOINT_LABELS[row.entrypoint] ?? '入口不明'}${status ? `・${status}` : ''}` }));
826
857
  return wrap;
827
858
  }
828
859
  const summary = text(row.proof?.decision?.summary);
@@ -833,19 +864,28 @@ function makeDecisionSummary(doc, row) {
833
864
  return wrap;
834
865
  }
835
866
 
867
+ const REFERENCE_AVAILABILITY_LABELS = Object.freeze({
868
+ recorded: '今の権限で開けます',
869
+ permission_denied: '開く権限がありません',
870
+ unavailable: '本文はもうありません',
871
+ unknown: '未確認',
872
+ });
873
+
836
874
  function appendReferenceList(doc, references) {
837
875
  if (references === null || references === undefined) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照は記録されていません。' });
838
876
  if (!references.length) return makeWorkspaceElement(doc, 'p', { className: 'bb-jh-unrecorded', text: '参照はありません。' });
839
877
  const list = makeWorkspaceElement(doc, 'ul', { className: 'bb-jh-reference-list' });
840
878
  for (const ref of references) {
841
879
  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 }));
880
+ const label = referenceLabel(ref);
881
+ item.append(makeWorkspaceElement(doc, 'strong', { text: label || '名前のない参照' }));
882
+ const recordId = text(ref.ref ?? ref.id);
883
+ if (recordId) {
884
+ item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-record-id', text: `記録ID:${recordId}(コピー)` }));
848
885
  }
886
+ if (ref.application && ref.application !== ref.label && ref.application !== label) item.append(makeWorkspaceElement(doc, 'span', { text: ref.application }));
887
+ const availability = REFERENCE_AVAILABILITY_LABELS[text(ref.availability)] ?? REFERENCE_AVAILABILITY_LABELS.unknown;
888
+ item.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-reference-availability', text: availability }));
849
889
  if (ref.why) item.append(makeWorkspaceElement(doc, 'span', { text: `理由: ${ref.why}` }));
850
890
  if (ref.usage) item.append(makeWorkspaceElement(doc, 'span', { text: `用途: ${ref.usage}` }));
851
891
  const alternateLabels = (ref.labels ?? []).filter((label) => label && label !== ref.label);
@@ -1147,7 +1187,9 @@ function renderRail(doc, rail, row, detail, onClose, options = {}) {
1147
1187
  rail.append(workspaceDetailEmpty(doc, {
1148
1188
  mark: '↗',
1149
1189
  title: '履歴を選択',
1150
- text: '行を選ぶと、使った参照と行った判断を確認できます。',
1190
+ text: options.loading
1191
+ ? '行を選ぶと、使った参照と行った判断を確認できます。読み込み中の行も、届いたものから選べます。'
1192
+ : '行を選ぶと、使った参照と行った判断を確認できます。',
1151
1193
  }));
1152
1194
  return;
1153
1195
  }
@@ -1189,43 +1231,108 @@ function safeDocument(explicit) {
1189
1231
  return doc;
1190
1232
  }
1191
1233
 
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',
1234
+ const RESPONSE_ERROR_CODES = new Set([
1235
+ 'response_not_object', 'records_missing', 'sections_missing', 'unsupported_status',
1236
+ 'coverage_incomplete', 'record_rejected', 'record_invalid',
1237
+ ]);
1238
+
1239
+ function publicErrorText(error) {
1240
+ const code = text(error);
1241
+ if (!code) return '';
1242
+ if (RESPONSE_ERROR_CODES.has(code)) return '応答を読み取れませんでした';
1243
+ return '';
1244
+ }
1245
+
1246
+ function prefersReducedMotion() {
1247
+ try {
1248
+ return globalThis.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
1249
+ } catch {
1250
+ return false;
1251
+ }
1252
+ }
1253
+
1254
+ function sourceDetail(doc, home) {
1255
+ const details = makeWorkspaceElement(doc, 'details', { className: 'bb-jh-source-detail' });
1256
+ details.append(makeWorkspaceElement(doc, 'summary', { text: '取得元の詳細' }));
1257
+ const body = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-source-detail-body' });
1258
+ const storage = home?.coverage?.storage;
1259
+ if (storage) body.append(makeWorkspaceElement(doc, 'p', { text: `保存先: ${normalStorageLabel(storage)}` }));
1260
+ const sources = Array.isArray(home?.coverage?.sources) ? home.coverage.sources : [];
1261
+ for (const source of sources) {
1262
+ if (!source?.reason) continue;
1263
+ body.append(makeWorkspaceElement(doc, 'p', { text: `理由コード: ${source.reason}` }));
1264
+ }
1265
+ if (home?.coverage?.reason && !sources.some((source) => source?.reason === home.coverage.reason)) {
1266
+ body.append(makeWorkspaceElement(doc, 'p', { text: `理由コード: ${home.coverage.reason}` }));
1267
+ }
1268
+ const rejected = home?.coverage?.rejected;
1269
+ if (Number.isInteger(rejected) && rejected > 0) {
1270
+ body.append(makeWorkspaceElement(doc, 'p', { text: `読めなかった件数: ${rejected}` }));
1271
+ }
1272
+ if (text(home?.coverage?.reason) === 'journal_scan_capped') {
1273
+ body.append(makeWorkspaceElement(doc, 'p', { text: '記録が多いため、新しい順に上限まで読みました。古い判断が抜けている可能性があります。' }));
1274
+ }
1275
+ details.append(body);
1276
+ return details;
1277
+ }
1278
+
1279
+ function sourceChips(doc, sources) {
1280
+ const wrap = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-banner-sources' });
1281
+ for (const source of sources) wrap.append(makeWorkspaceElement(doc, 'span', { text: sourceChipText(source) }));
1282
+ return wrap;
1283
+ }
1284
+
1285
+ function historyBanner(doc, { label, tone, role, sources, home, note, reload }) {
1286
+ const banner = workspaceErrorCard(doc, {
1287
+ title: label,
1288
+ tone,
1289
+ message: note || '取得できた記録だけを表示しています。全体の取得状態は通知の詳細で確認できます。',
1228
1290
  });
1291
+ banner.className += ` bb-jh-banner is-${tone}`;
1292
+ banner.setAttribute('role', role ?? 'status');
1293
+ const details = findFirst(banner, node => node.tagName === 'DETAILS');
1294
+ if (sources?.length) details.append(sourceChips(doc, sources));
1295
+ if (home) details.append(sourceDetail(doc, home));
1296
+ if (reload) banner.append(reload);
1297
+ return banner;
1298
+ }
1299
+
1300
+ function statusNotice(doc, phase, error, home, onRetry, { loading = false, slow = false, arrived = null } = {}) {
1301
+ const reload = workspaceButton(doc, { text: '再読み込み', variant: 'quiet', onClick: onRetry, attrs: { 'data-action': 'reload' } });
1302
+ if (phase === 'error' || home?.status === 'error' || home?.status === 'invalid') {
1303
+ const detail = publicErrorText(error);
1304
+ return historyBanner(doc, {
1305
+ label: '取得できません',
1306
+ tone: 'danger',
1307
+ role: 'alert',
1308
+ note: `判断履歴を取得できませんでした。データが0件とは確認できていません。${detail ? ` ${detail}` : ''}`,
1309
+ home: error ? { coverage: { reason: text(error) } } : home,
1310
+ reload,
1311
+ });
1312
+ }
1313
+ if (home?.status === 'unavailable') {
1314
+ return historyBanner(doc, {
1315
+ label: '取得できません',
1316
+ tone: 'danger',
1317
+ role: 'alert',
1318
+ sources: home.coverage?.sources,
1319
+ home,
1320
+ note: '判断の記録に接続できません。データが0件とは確認できていません。',
1321
+ reload,
1322
+ });
1323
+ }
1324
+ if (loading) {
1325
+ return workspaceLoadingCard(doc, { title: 'ロード中' });
1326
+ }
1327
+ if (home?.status === 'partial') {
1328
+ return historyBanner(doc, {
1329
+ label: '一部の記録だけを表示中',
1330
+ tone: 'warning',
1331
+ role: 'status',
1332
+ sources: home.coverage?.sources,
1333
+ home,
1334
+ });
1335
+ }
1229
1336
  return null;
1230
1337
  }
1231
1338
 
@@ -1255,38 +1362,51 @@ function createSearch(doc, value, onInput) {
1255
1362
  }
1256
1363
 
1257
1364
  function normalStorageLabel(storage) {
1258
- if (storage === 'local') return '端末';
1259
- if (storage === 'server') return 'サーバー';
1260
- return storage || '接続先不明';
1365
+ if (storage === 'local') return 'この端末';
1366
+ if (storage === 'server') return '組織のサーバー';
1367
+ return '接続先不明';
1261
1368
  }
1262
1369
 
1263
1370
  function normalReasonLabel(reason) {
1264
1371
  const labels = {
1372
+ journal_scan_capped: '読み込み上限まで',
1373
+ source_not_connected: '未接続',
1265
1374
  unconnected: '未接続',
1266
- mana_not_connected: '未接続(Mana)',
1267
- company_os_native_source_not_connected: 'Company OSの判断実行履歴が未接続',
1268
- journal_unavailable: '判断記録がありません',
1269
- journal_unreadable: '判断記録を読み取れません',
1375
+ mana_not_connected: '未接続',
1376
+ company_os_native_source_not_connected: '実行履歴は未接続',
1377
+ journal_unavailable: '判断の記録が見つかりません',
1378
+ journal_unreadable: '判断の記録を読み取れません',
1379
+ some_journal_records_unreadable: '一部の記録を読み取れません',
1380
+ project_scope_unverified: 'プロジェクトの範囲を確かめられていません',
1381
+ project_scope_forbidden: 'このプロジェクトを見る権限がありません',
1382
+ journal_not_directory: '記録の保存場所が正しくありません',
1383
+ journal_symlink_rejected: '記録の保存場所が正しくありません',
1384
+ permission_denied: '見る権限がありません',
1385
+ judgment_history_partial: '一部だけ取得',
1386
+ judgment_history_unavailable: '取得できません',
1387
+ judgment_journal_unavailable: '判断の記録に接続できません',
1270
1388
  };
1271
- return labels[reason] ?? reason;
1389
+ if (!reason) return '';
1390
+ return labels[reason] ?? '理由を確かめられません';
1272
1391
  }
1273
1392
 
1274
1393
  function normalSourceStatusLabel(status) {
1275
1394
  const labels = {
1276
- available: '接続済み',
1277
- partial: '一部取得',
1395
+ available: '取得済み',
1396
+ partial: '一部',
1278
1397
  unavailable: '未接続',
1279
1398
  };
1280
1399
  return labels[status] ?? '未確認';
1281
1400
  }
1282
1401
 
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('、');
1402
+ function sourceChipText(source) {
1403
+ const name = NORMAL_ENTRYPOINT_LABELS[normalEntrypoint(source.entrypoint)] ?? '入口不明';
1404
+ if (source?.status === 'pending' || source?.status === 'loading') return `${name}:確認中…`;
1405
+ if (Number.isInteger(source?.retrieved)) return `${name}:${source.retrieved}件まで届きました`;
1406
+ const statusLabel = normalSourceStatusLabel(source.status);
1407
+ const reasonLabel = source.reason ? normalReasonLabel(source.reason) : '';
1408
+ const reason = reasonLabel && reasonLabel !== statusLabel ? `(${reasonLabel})` : '';
1409
+ return `${name}:${statusLabel}${reason}`;
1290
1410
  }
1291
1411
 
1292
1412
  function createFilterSelect(doc, { label, value, options, onChange, className = '' }) {
@@ -1309,12 +1429,12 @@ function createFilterSelect(doc, { label, value, options, onChange, className =
1309
1429
  function normalFilterControls(doc, home, state, onChange) {
1310
1430
  const filters = home?.filters ?? {};
1311
1431
  const projects = [...(filters.projects ?? [])];
1312
- const knownProjects = (home.records ?? []).map((record) => record.projectCode).filter(Boolean);
1432
+ const knownProjects = (home?.records ?? []).map((record) => record.projectCode).filter(Boolean);
1313
1433
  for (const project of knownProjects) {
1314
1434
  if (!projects.some((option) => option.value === project)) projects.push({ value: project, label: project });
1315
1435
  }
1316
1436
  const entrypoints = [...(filters.entrypoints ?? [])];
1317
- const knownEntrypoints = (home.records ?? []).map((record) => record.entrypoint).filter(Boolean);
1437
+ const knownEntrypoints = (home?.records ?? []).map((record) => record.entrypoint).filter(Boolean);
1318
1438
  for (const entrypoint of [...NORMAL_ENTRYPOINTS, ...knownEntrypoints]) {
1319
1439
  if (!entrypoints.some((option) => option.value === entrypoint)) {
1320
1440
  entrypoints.push({ value: entrypoint, label: NORMAL_ENTRYPOINT_LABELS[entrypoint] ?? entrypoint });
@@ -1350,6 +1470,65 @@ function paginationControls(doc, aggregate, onNext) {
1350
1470
  return wrap;
1351
1471
  }
1352
1472
 
1473
+ function makeEntrypointStatus(doc, row) {
1474
+ const wrap = makeWorkspaceElement(doc, 'span', { className: 'bb-jh-entrypoint' });
1475
+ if (row.mode !== 'normal') {
1476
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: row.delegated ? 'Brainbase' : '本人' }));
1477
+ return wrap;
1478
+ }
1479
+ wrap.append(makeWorkspaceElement(doc, 'strong', { text: NORMAL_ENTRYPOINT_LABELS[row.entrypoint] ?? '入口不明' }));
1480
+ wrap.append(makeWorkspaceElement(doc, 'small', { text: judgmentStatusLabel(row.judgment?.status) }));
1481
+ return wrap;
1482
+ }
1483
+
1484
+ function skeletonRows(doc, count = 5) {
1485
+ return Array.from({ length: count }, (_, index) => ({
1486
+ key: `skeleton-${index}`,
1487
+ className: 'bb-jh-skeleton-row',
1488
+ cells: [
1489
+ makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-date' }),
1490
+ makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-decision' }),
1491
+ makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-reference' }),
1492
+ makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-status' }),
1493
+ ],
1494
+ }));
1495
+ }
1496
+
1497
+ function summaryLine(doc, aggregate, { loading = false } = {}) {
1498
+ const line = makeWorkspaceElement(doc, 'p', { className: 'bb-jh-summary' });
1499
+ const retrieved = Number.isInteger(aggregate?.retrievedCount) ? aggregate.retrievedCount : (aggregate?.rows?.length ?? 0);
1500
+ const complete = aggregate?.countComplete === true && Number.isInteger(aggregate?.totalCount);
1501
+ const showCount = complete || retrieved > 0;
1502
+ if (showCount) {
1503
+ line.append(makeWorkspaceElement(doc, 'strong', { text: `${complete ? aggregate.totalCount : retrieved}件` }));
1504
+ line.append(makeWorkspaceElement(doc, 'span', { text: ' 取得済み' }));
1505
+ } else {
1506
+ line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
1507
+ line.append(makeWorkspaceElement(doc, 'span', { text: ' 取得済み' }));
1508
+ }
1509
+ const references = loading ? null : (aggregate?.stats?.referenceCount ?? aggregate?.stats?.references);
1510
+ const confirmed = loading ? null : (aggregate?.stats?.confirmedOutcomes ?? aggregate?.stats?.outcomes);
1511
+ line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 使った参照 ' }));
1512
+ if (Number.isInteger(references)) line.append(makeWorkspaceElement(doc, 'strong', { text: `${references}件` }));
1513
+ else line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
1514
+ line.append(makeWorkspaceElement(doc, 'span', { text: ' ・ 結果を確認済み ' }));
1515
+ if (Number.isInteger(confirmed)) line.append(makeWorkspaceElement(doc, 'strong', { text: `${confirmed}件` }));
1516
+ else line.append(makeWorkspaceElement(doc, 'span', { className: 'bb-jh-skeleton is-inline', attrs: { 'aria-hidden': 'true' } }));
1517
+ return line;
1518
+ }
1519
+
1520
+ function formatCheckedAt(value) {
1521
+ const date = new Date(value);
1522
+ if (!Number.isFinite(date.getTime())) return '';
1523
+ try {
1524
+ return new Intl.DateTimeFormat('ja-JP', {
1525
+ timeZone: 'Asia/Tokyo', hour: '2-digit', minute: '2-digit', hour12: false,
1526
+ }).format(date);
1527
+ } catch {
1528
+ return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
1529
+ }
1530
+ }
1531
+
1353
1532
  function makeRows(doc, aggregate, selectedKey, onSelect) {
1354
1533
  return aggregate.rows.map((row) => ({
1355
1534
  key: row.key,
@@ -1359,8 +1538,9 @@ function makeRows(doc, aggregate, selectedKey, onSelect) {
1359
1538
  onSelect,
1360
1539
  cells: [
1361
1540
  makeWorkspaceElement(doc, 'span', { className: 'bb-jh-date', text: formatDate(row.recordedAt) }),
1362
- makeReferenceSummary(doc, row.references),
1363
1541
  makeDecisionSummary(doc, row),
1542
+ makeReferenceSummary(doc, row.references),
1543
+ makeEntrypointStatus(doc, row),
1364
1544
  ],
1365
1545
  }));
1366
1546
  }
@@ -1376,7 +1556,10 @@ export function createJudgmentHistoryUI({
1376
1556
  page,
1377
1557
  autoLoad = true,
1378
1558
  pageHeader = true,
1559
+ notificationRoot = null,
1379
1560
  now = new Date(),
1561
+ skeletonDelayMs = 300,
1562
+ slowDelayMs = 10000,
1380
1563
  } = {}) {
1381
1564
  if (!root) throw new TypeError('root is required');
1382
1565
  const doc = safeDocument(explicitDocument);
@@ -1409,20 +1592,110 @@ export function createJudgmentHistoryUI({
1409
1592
  focusSearch: false,
1410
1593
  lastFocusedKey: null,
1411
1594
  pendingDecisionId: null,
1595
+ showSkeleton: false,
1596
+ slow: false,
1597
+ arrived: null,
1598
+ checkedAt: null,
1599
+ skeletonTimer: null,
1600
+ slowTimer: null,
1412
1601
  };
1413
1602
 
1603
+ let filterPopover = null;
1604
+ let filterExpanded = false;
1605
+ let drawer = null;
1606
+ let drawerContent = null;
1607
+ let drawerKey = null;
1608
+ let suppressDrawerClose = false;
1609
+ function disposeDrawer() {
1610
+ suppressDrawerClose = true;
1611
+ drawer?.dispose();
1612
+ drawer = null; drawerContent = null; drawerKey = null;
1613
+ suppressDrawerClose = false;
1614
+ }
1615
+ const nativeDrawer = Boolean(doc.body && typeof doc.createElement('dialog').showModal === 'function');
1616
+ const previousRailHidden = rail?.hidden;
1617
+ if (nativeDrawer && rail) rail.hidden = true;
1618
+ const notifications = makeWorkspaceElement(doc, 'div', {
1619
+ className: 'bb-ws-error-stack bb-jh-notifications', attrs: { 'aria-label': '判断履歴の通知' },
1620
+ });
1621
+ if (notificationRoot) notificationRoot.append(notifications);
1622
+ let loadingCard = null;
1623
+ let dismissLoading = null;
1624
+ let statusCard = null;
1625
+ let statusKey = null;
1626
+
1627
+ function updateNotifications(loading) {
1628
+ if (loading) {
1629
+ dismissLoading?.(); dismissLoading = null;
1630
+ if (!loadingCard) loadingCard = workspaceLoadingCard(doc, { title: 'ロード中' });
1631
+ if (loadingCard.parentNode !== notifications) notifications.append(loadingCard);
1632
+ } else if (loadingCard && !dismissLoading) {
1633
+ dismissLoading = workspaceDismissNotification(loadingCard);
1634
+ }
1635
+ const home = state.home;
1636
+ const key = loading ? null : JSON.stringify([state.phase, state.error, home?.status, home?.coverage]);
1637
+ if (statusKey !== key) {
1638
+ statusCard?.remove?.();
1639
+ // Small host DOM adapters may not implement Element.remove().
1640
+ if (statusCard?.parentNode === notifications && typeof statusCard.remove !== 'function') {
1641
+ notifications.replaceChildren(...notifications.children.filter(node => node !== statusCard));
1642
+ }
1643
+ statusKey = key;
1644
+ statusCard = loading ? null : statusNotice(doc, state.phase, state.error, home, () => void load());
1645
+ if (statusCard) notifications.append(statusCard);
1646
+ }
1647
+ }
1648
+
1649
+ function clearLoadTimers() {
1650
+ if (state.skeletonTimer !== null) {
1651
+ clearTimeout(state.skeletonTimer);
1652
+ state.skeletonTimer = null;
1653
+ }
1654
+ if (state.slowTimer !== null) {
1655
+ clearTimeout(state.slowTimer);
1656
+ state.slowTimer = null;
1657
+ }
1658
+ }
1659
+
1660
+ function scheduleLoadTimers(generation) {
1661
+ clearLoadTimers();
1662
+ const skeletonDelay = Number.isFinite(skeletonDelayMs) ? Math.max(0, skeletonDelayMs) : 300;
1663
+ const slowDelay = Number.isFinite(slowDelayMs) ? Math.max(0, slowDelayMs) : 10000;
1664
+ const stillLoading = () => generation === state.generation && !state.disposed && state.phase === 'loading';
1665
+ if (skeletonDelay === 0) state.showSkeleton = true;
1666
+ else {
1667
+ state.skeletonTimer = setTimeout(() => {
1668
+ if (!stillLoading()) return;
1669
+ state.showSkeleton = true;
1670
+ render();
1671
+ }, skeletonDelay);
1672
+ }
1673
+ if (slowDelay === 0) {
1674
+ state.slow = true;
1675
+ state.showSkeleton = true;
1676
+ } else {
1677
+ state.slowTimer = setTimeout(() => {
1678
+ if (!stillLoading()) return;
1679
+ state.slow = true;
1680
+ state.showSkeleton = true;
1681
+ render();
1682
+ }, slowDelay);
1683
+ }
1684
+ }
1685
+
1414
1686
  function closeSelected() {
1415
1687
  if (!state.selectedKey || state.disposed) return false;
1416
1688
  state.lastFocusedKey = state.selectedKey;
1417
1689
  state.selectedKey = null;
1418
1690
  state.detailPhase = 'idle';
1419
1691
  state.detailError = null;
1692
+ if (drawer) drawer.close();
1420
1693
  render();
1421
1694
  return true;
1422
1695
  }
1423
1696
 
1424
1697
  function handleKeyDown(event) {
1425
- if (event?.key === 'Escape') closeSelected();
1698
+ if (event?.key === 'Escape' && !drawer) closeSelected();
1426
1699
  }
1427
1700
 
1428
1701
  root.addEventListener?.('keydown', handleKeyDown);
@@ -1661,84 +1934,130 @@ export function createJudgmentHistoryUI({
1661
1934
 
1662
1935
  function render() {
1663
1936
  if (state.disposed) return;
1937
+ updateNotifications(state.phase === 'loading');
1938
+ if (state.phase === 'loading' && !state.showSkeleton && !state.arrived) {
1939
+ if (!notificationRoot && notifications.parentNode !== root) root.append(notifications);
1940
+ return;
1941
+ }
1942
+ const focusedFilter = filterExpanded && filterPopover
1943
+ ? findFirst(filterPopover.panel, node => node === doc.activeElement) : null;
1944
+ const filterFocus = focusedFilter && {
1945
+ label: attributeValue(focusedFilter, 'aria-label'), period: attributeValue(focusedFilter, 'data-period'),
1946
+ };
1947
+ filterPopover?.destroy();
1948
+ filterPopover = null;
1664
1949
  const previousSearchInput = findFirst(root, (node) => node.tagName === 'INPUT');
1665
1950
  const previousSearchSelection = previousSearchInput && Number.isInteger(previousSearchInput.selectionStart)
1666
1951
  ? { start: previousSearchInput.selectionStart, end: previousSearchInput.selectionEnd ?? previousSearchInput.selectionStart }
1667
1952
  : null;
1668
1953
  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 } });
1954
+ const loading = state.phase === 'loading';
1955
+ const sourceHome = loading ? state.arrived : state.home;
1956
+ const aggregate = loading
1957
+ ? (sourceHome
1958
+ ? aggregateJudgmentHistory(sourceHome, { now: readNow(), period: state.period, query: state.query })
1959
+ : { status: 'partial', rows: [], visibleCount: 0, stats: {}, coverage: null, countComplete: false, retrievedCount: 0, mode: 'normal' })
1960
+ : (state.aggregate ?? {
1961
+ status: state.home?.status ?? 'unavailable', rows: [], visibleCount: 0, stats: {}, coverage: state.home?.coverage ?? null,
1962
+ });
1963
+ const wrapper = makeWorkspaceElement(doc, 'div', {
1964
+ className: `bb-jh${prefersReducedMotion() ? ' is-reduced-motion' : ''}`,
1965
+ attrs: { 'data-contract-version': JUDGMENT_HISTORY_UI_CONTRACT_VERSION, 'aria-busy': loading ? 'true' : 'false' },
1966
+ });
1673
1967
  if (pageHeader) {
1674
1968
  const context = currentPage();
1675
1969
  wrapper.append(workspacePageHeader(doc, {
1676
1970
  crumbs: context.crumbs,
1677
1971
  title: '判断の履歴',
1678
- lead: 'Brainbaseにどんな参照が入り、どんな判断を代わりに行ったかを見返せます。',
1972
+ lead: '参照した内容と、Brainbaseが代わりに行った判断を見返す',
1679
1973
  source: context.source,
1680
1974
  }));
1681
1975
  }
1682
- const notice = statusNotice(doc, state.phase, state.error, state.home, () => void load());
1683
- 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: '再試行すると最新の状態を確認します。' }));
1976
+ if (!notificationRoot) wrapper.append(notifications);
1977
+ const failed = state.phase === 'error' || state.home?.status === 'unavailable' || state.home?.status === 'error' || state.home?.status === 'invalid';
1978
+ if (failed) {
1979
+ wrapper.append(workspaceDetailEmpty(doc, { mark: '…', title: '判断履歴を確認できません', text: '再試行すると最新の状態を確認します。' }));
1686
1980
  root.replaceChildren(wrapper);
1687
- renderRail(doc, rail, null, null);
1981
+ if (drawer) disposeDrawer();
1982
+ if (!nativeDrawer) renderRail(doc, rail, null, null);
1688
1983
  return;
1689
1984
  }
1690
1985
  const controls = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-controls' });
1691
- controls.append(periodButtons(doc, state.period, (period) => {
1986
+ const filterFields = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-filter-fields' });
1987
+ filterFields.append(periodButtons(doc, state.period, (period) => {
1692
1988
  state.period = period;
1693
1989
  state.selectedKey = null;
1694
1990
  state.cursor = null;
1695
- if (normalMode()) void load({ append: false });
1991
+ if (loading || normalMode()) void load({ append: false });
1696
1992
  else {
1697
1993
  state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
1698
1994
  render();
1699
1995
  }
1700
1996
  }));
1701
- if (normalMode()) controls.append(normalFilterControls(doc, state.home, state, handleNormalFilterChange));
1997
+ const showNormalFilters = loading ? sourceHome?.mode !== 'legacy' : normalMode();
1998
+ if (showNormalFilters) filterFields.append(normalFilterControls(doc, sourceHome ?? state.home, state, handleNormalFilterChange));
1702
1999
  controls.append(createSearch(doc, state.query, (query) => {
1703
2000
  state.query = String(query);
1704
2001
  state.focusSearch = true;
1705
2002
  state.selectedKey = null;
1706
- state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
2003
+ const searchable = loading ? sourceHome : state.home;
2004
+ if (searchable) state.aggregate = aggregateJudgmentHistory(searchable, { now: readNow(), period: state.period, query: state.query });
1707
2005
  render();
1708
2006
  }));
1709
- controls.append(workspaceButton(doc, {
1710
- text: '再読み込み',
1711
- variant: 'quiet',
1712
- onClick: () => void load(),
1713
- attrs: { 'aria-label': '判断履歴を再読み込み', 'data-action': 'reload' },
1714
- }));
2007
+ filterPopover = workspaceFilterPopover(doc, {
2008
+ content: filterFields, open: filterExpanded,
2009
+ onToggle: value => { filterExpanded = value; },
2010
+ });
2011
+ controls.append(filterPopover.root);
2012
+ const toolbarStatus = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-toolbar-status' });
2013
+ if (state.checkedAt) {
2014
+ const stamp = formatCheckedAt(state.checkedAt);
2015
+ if (stamp) toolbarStatus.append(makeWorkspaceElement(doc, 'span', {
2016
+ className: 'bb-jh-checked', text: stamp, attrs: { 'aria-label': `${stamp}に確認` },
2017
+ }));
2018
+ }
2019
+ const reloadButton = workspaceButton(doc, {
2020
+ variant: 'quiet', disabled: loading, onClick: () => void load(),
2021
+ attrs: { 'aria-label': '判断履歴を再読み込み', title: '再読み込み', 'data-action': 'reload' },
2022
+ });
2023
+ reloadButton.className += ' bb-jh-refresh';
2024
+ // Text-independent vector keeps the refresh control available in standalone hosts.
2025
+ const refreshMark = doc.createElementNS?.('http://www.w3.org/2000/svg', 'svg')
2026
+ ?? makeWorkspaceElement(doc, 'span', { text: '↻' });
2027
+ refreshMark.setAttribute('class', 'bb-jh-refresh-mark'); refreshMark.setAttribute('aria-hidden', 'true');
2028
+ if (doc.createElementNS) {
2029
+ refreshMark.setAttribute('viewBox', '0 0 24 24'); refreshMark.setAttribute('fill', 'none');
2030
+ refreshMark.setAttribute('stroke', 'currentColor'); refreshMark.setAttribute('stroke-width', '1.7');
2031
+ refreshMark.setAttribute('stroke-linecap', 'round'); refreshMark.setAttribute('stroke-linejoin', 'round');
2032
+ const path = doc.createElementNS('http://www.w3.org/2000/svg', 'path');
2033
+ path.setAttribute('d', 'M20 7v5h-5M4 17v-5h5M6.1 6.1a8 8 0 0 1 13.2 3.2M4.7 14.7a8 8 0 0 0 13.2 3.2');
2034
+ refreshMark.append(path);
2035
+ }
2036
+ reloadButton.append(refreshMark);
2037
+ toolbarStatus.append(reloadButton);
2038
+ controls.append(toolbarStatus);
1715
2039
  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
- }));
1723
- const emptyText = state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
2040
+ wrapper.append(summaryLine(doc, aggregate, { loading }));
2041
+ const emptyText = loading ? '' : state.home?.status === 'partial' || (normalMode() && !aggregate.countComplete)
1724
2042
  ? 'この期間に取得できた判断履歴はありません。全件数が未確認のため0件とは確認できません。'
1725
2043
  : 'この期間の判断履歴はありません。';
2044
+ const dataRows = makeRows(doc, aggregate, state.selectedKey, (key) => {
2045
+ state.lastFocusedKey = key;
2046
+ state.selectedKey = key;
2047
+ state.detailPhase = 'idle';
2048
+ state.detailError = null;
2049
+ render();
2050
+ const selectedRow = aggregate.rows.find((row) => row.key === key);
2051
+ if (selectedRow?.mode === 'normal') void loadDetail(selectedRow);
2052
+ });
1726
2053
  wrapper.append(workspaceLedger(doc, {
1727
2054
  className: 'bb-jh-ledger',
1728
2055
  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
- }),
2056
+ columns: ['日時', '行った判断', '使った参照', '入口・状態'],
2057
+ rows: loading ? [...dataRows, ...skeletonRows(doc)] : dataRows,
1739
2058
  empty: emptyText,
1740
2059
  }));
1741
- const pagination = paginationControls(doc, aggregate, () => {
2060
+ const pagination = loading ? null : paginationControls(doc, aggregate, () => {
1742
2061
  if (aggregate.pagination?.nextCursor) {
1743
2062
  state.cursor = aggregate.pagination.nextCursor;
1744
2063
  void load({ cursor: state.cursor, append: true });
@@ -1751,6 +2070,12 @@ export function createJudgmentHistoryUI({
1751
2070
  replaceNode(nextSearchInput, previousSearchInput);
1752
2071
  }
1753
2072
  root.replaceChildren(wrapper);
2073
+ if (filterExpanded && filterFocus) {
2074
+ const field = findFirst(filterPopover.panel, node => filterFocus.label
2075
+ ? attributeValue(node, 'aria-label') === filterFocus.label
2076
+ : filterFocus.period && attributeValue(node, 'data-period') === filterFocus.period);
2077
+ field?.focus?.();
2078
+ }
1754
2079
  if (state.focusSearch || previousSearchFocused) {
1755
2080
  const input = findFirst(root, (node) => node.tagName === 'INPUT');
1756
2081
  input?.focus?.();
@@ -1776,8 +2101,38 @@ export function createJudgmentHistoryUI({
1776
2101
  onCopyReference: (payload) => void copyReference(payload),
1777
2102
  }
1778
2103
  : {};
1779
- renderRail(doc, rail, selected, selectedDetail, selected ? closeSelected : null, railOptions);
1780
- if (state.lastFocusedKey) {
2104
+ if (nativeDrawer) {
2105
+ if (selected) {
2106
+ if (!drawer || drawerKey !== selected.key) {
2107
+ if (drawer) disposeDrawer();
2108
+ drawerKey = selected.key;
2109
+ drawerContent = makeWorkspaceElement(doc, 'div', { className: 'bb-jh-detail' });
2110
+ const selectedKey = selected.key;
2111
+ drawer = workspaceDialog(doc, {
2112
+ title: '判断の詳細', content: drawerContent, presentation: 'drawer',
2113
+ opener: () => findFirst(root, node => attributeValue(node, 'data-key') === selectedKey),
2114
+ onClose: () => {
2115
+ if (suppressDrawerClose) return;
2116
+ drawer = null; drawerContent = null; drawerKey = null;
2117
+ if (state.disposed) return;
2118
+ state.selectedKey = null; state.detailPhase = 'idle'; state.detailError = null;
2119
+ state.lastFocusedKey = selectedKey; render();
2120
+ },
2121
+ });
2122
+ }
2123
+ const detailFocus = findFirst(drawerContent, node => node === doc.activeElement);
2124
+ const detailLabel = attributeValue(detailFocus, 'aria-label');
2125
+ const detailSelection = Number.isInteger(detailFocus?.selectionStart)
2126
+ ? [detailFocus.selectionStart, detailFocus.selectionEnd] : null;
2127
+ renderRail(doc, drawerContent, selected, selectedDetail, null, { ...railOptions, loading });
2128
+ if (detailLabel) {
2129
+ const field = findFirst(drawerContent, node => attributeValue(node, 'aria-label') === detailLabel);
2130
+ field?.focus?.();
2131
+ if (detailSelection) field?.setSelectionRange?.(...detailSelection);
2132
+ }
2133
+ } else if (drawer) drawer.close();
2134
+ } else renderRail(doc, rail, selected, selectedDetail, selected ? closeSelected : null, { ...railOptions, loading });
2135
+ if (state.lastFocusedKey && !drawer) {
1781
2136
  const row = findFirst(root, (node) => attributeValue(node, 'data-key') === state.lastFocusedKey);
1782
2137
  row?.focus?.();
1783
2138
  state.lastFocusedKey = null;
@@ -1787,26 +2142,39 @@ export function createJudgmentHistoryUI({
1787
2142
  async function load({ cursor = null, append = false } = {}) {
1788
2143
  if (state.disposed) return null;
1789
2144
  const generation = ++state.generation;
1790
- state.phase = 'loading';
1791
2145
  state.error = null;
1792
2146
  if (!append) {
2147
+ state.phase = 'loading';
1793
2148
  state.home = null;
1794
2149
  state.aggregate = null;
2150
+ state.arrived = null;
2151
+ state.showSkeleton = false;
2152
+ state.slow = false;
2153
+ scheduleLoadTimers(generation);
2154
+ render();
1795
2155
  }
1796
- render();
1797
2156
  if (!request) {
1798
2157
  if (generation !== state.generation || state.disposed) return null;
2158
+ clearLoadTimers();
2159
+ state.showSkeleton = false;
1799
2160
  state.phase = 'error';
1800
2161
  state.error = 'fetch_unavailable';
1801
2162
  render();
1802
2163
  return null;
1803
2164
  }
1804
2165
  try {
2166
+ const report = (payload) => {
2167
+ if (generation !== state.generation || state.disposed || state.phase !== 'loading') return;
2168
+ const normalized = normalizeJudgmentHistoryHome(payload);
2169
+ if (!normalized || normalized.status === 'invalid') return;
2170
+ state.arrived = normalized;
2171
+ render();
2172
+ };
1805
2173
  let activePath = normalizedBasePath;
1806
2174
  let response = await request(historyPath(normalizedBasePath, cursor), {
1807
2175
  method: 'GET',
1808
2176
  headers: token ? { 'x-brainbase-review-token': token } : undefined,
1809
- });
2177
+ }, { report });
1810
2178
  if (generation !== state.generation || state.disposed) return null;
1811
2179
  if (response?.status === 404 && legacyBasePath && !append && !state.project && !state.entrypoint && !state.cursor) {
1812
2180
  activePath = legacyBasePath;
@@ -1837,6 +2205,11 @@ export function createJudgmentHistoryUI({
1837
2205
  state.entrypoint = state.entrypoint ?? state.home.filters?.entrypoint ?? null;
1838
2206
  state.cursor = state.home.pagination?.nextCursor ?? null;
1839
2207
  }
2208
+ clearLoadTimers();
2209
+ state.showSkeleton = false;
2210
+ state.slow = false;
2211
+ state.arrived = null;
2212
+ state.checkedAt = readNow();
1840
2213
  state.phase = 'ready';
1841
2214
  state.aggregate = aggregateJudgmentHistory(state.home, { now: readNow(), period: state.period, query: state.query });
1842
2215
  if (state.pendingDecisionId) {
@@ -1848,6 +2221,9 @@ export function createJudgmentHistoryUI({
1848
2221
  return state.aggregate;
1849
2222
  } catch (error) {
1850
2223
  if (generation !== state.generation || state.disposed) return null;
2224
+ clearLoadTimers();
2225
+ state.showSkeleton = false;
2226
+ state.slow = false;
1851
2227
  state.phase = 'error';
1852
2228
  state.error = error instanceof Error ? error.message : String(error);
1853
2229
  render();
@@ -1875,10 +2251,16 @@ export function createJudgmentHistoryUI({
1875
2251
  if (state.disposed) return;
1876
2252
  state.disposed = true;
1877
2253
  state.generation += 1;
2254
+ clearLoadTimers();
2255
+ filterPopover?.destroy();
2256
+ dismissLoading?.();
2257
+ notifications.remove?.();
2258
+ disposeDrawer();
1878
2259
  root.removeEventListener?.('keydown', handleKeyDown);
1879
2260
  rail?.removeEventListener?.('keydown', handleKeyDown);
1880
2261
  root.replaceChildren();
1881
2262
  rail?.replaceChildren();
2263
+ if (nativeDrawer && rail) rail.hidden = previousRailHidden;
1882
2264
  }
1883
2265
 
1884
2266
  const controller = { load, render, dispose, openDecision, get state() { return { ...state }; } };