@principal-ai/subsystems-react 0.24.1 → 0.24.3

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.
Files changed (29) hide show
  1. package/dist/components/session-events/SessionEventFeed.d.ts +2 -1
  2. package/dist/components/session-events/SessionEventFeed.d.ts.map +1 -1
  3. package/dist/components/session-events/SessionEventFeed.js +43 -35
  4. package/dist/components/session-events/SessionEventFeed.js.map +1 -1
  5. package/dist/pierre/PierreFileView.d.ts.map +1 -1
  6. package/dist/pierre/PierreFileView.js +23 -1
  7. package/dist/pierre/PierreFileView.js.map +1 -1
  8. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
  9. package/dist/subsystem/ConstructsCatalog.js +18 -38
  10. package/dist/subsystem/ConstructsCatalog.js.map +1 -1
  11. package/dist/subsystem/IssueList.d.ts +1 -0
  12. package/dist/subsystem/IssueList.d.ts.map +1 -1
  13. package/dist/subsystem/IssueList.js +38 -10
  14. package/dist/subsystem/IssueList.js.map +1 -1
  15. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  16. package/dist/subsystem/SubsystemComponentGraph.js +2 -2
  17. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  18. package/dist/subsystem/SubsystemFileTree.js +2 -2
  19. package/dist/subsystem/SubsystemFileTree.js.map +1 -1
  20. package/dist/subsystem/nodes.js +5 -5
  21. package/dist/subsystem/nodes.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/components/session-events/SessionEventFeed.tsx +54 -45
  24. package/src/pierre/PierreFileView.tsx +27 -1
  25. package/src/subsystem/ConstructsCatalog.tsx +32 -72
  26. package/src/subsystem/IssueList.tsx +41 -10
  27. package/src/subsystem/SubsystemComponentGraph.tsx +1 -2
  28. package/src/subsystem/SubsystemFileTree.tsx +2 -2
  29. package/src/subsystem/nodes.tsx +5 -5
@@ -10,7 +10,8 @@
10
10
  * repo-normalized form, and `accumulated` the AgentSessionEvent the File City
11
11
  * UI actually renders (null when the accumulator drops the event).
12
12
  *
13
- * Dark diagnostic palette — intentionally self-contained rather than themed.
13
+ * The categorical event-hue palette (kind/operation/todo colors) is intentionally
14
+ * self-contained rather than themed; typography comes from the theme scale.
14
15
  */
15
16
 
16
17
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
@@ -422,7 +423,7 @@ function RawEventCard({
422
423
  border: '1px dashed #1f2937',
423
424
  backgroundColor: '#0d1117',
424
425
  fontFamily: theme.fonts.body,
425
- fontSize: 12,
426
+ fontSize: theme.fontSizes[0],
426
427
  }}
427
428
  >
428
429
  <span style={{ color: '#4b5563', fontStyle: 'italic' }}>no raw payload</span>
@@ -483,7 +484,7 @@ function RawEventCard({
483
484
  const todos = (state?.input as AnyRecord | undefined)?.['todos'];
484
485
  return (
485
486
  <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
486
- <div style={{ fontSize: 12, color: '#9ca3af' }}>
487
+ <div style={{ fontSize: theme.fontSizes[0], color: '#9ca3af' }}>
487
488
  callID {String(part?.callID ?? '')} · {String(state?.status ?? '')}
488
489
  </div>
489
490
  {Array.isArray(todos) ? <TodosView todos={todos} /> : null}
@@ -547,7 +548,7 @@ function RawEventCard({
547
548
  overflow: 'hidden',
548
549
  backgroundColor: expanded ? '#111827' : '#161b26',
549
550
  fontFamily: theme.fonts.body,
550
- fontSize: 13,
551
+ fontSize: theme.fontSizes[0],
551
552
  }}
552
553
  >
553
554
  <button
@@ -566,10 +567,10 @@ function RawEventCard({
566
567
  textAlign: 'left',
567
568
  }}
568
569
  >
569
- <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>#{event.seq}</span>
570
+ <span style={{ color: '#6b7280', fontSize: theme.fontSizes[0], whiteSpace: 'nowrap' }}>#{event.seq}</span>
570
571
  <span
571
572
  style={{
572
- fontSize: 11,
573
+ fontSize: theme.fontSizes[0],
573
574
  fontWeight: 600,
574
575
  textTransform: 'uppercase',
575
576
  letterSpacing: 0.5,
@@ -583,7 +584,7 @@ function RawEventCard({
583
584
  <span
584
585
  title={`${runCount} separate runs of this command`}
585
586
  style={{
586
- fontSize: 11,
587
+ fontSize: theme.fontSizes[0],
587
588
  fontWeight: 700,
588
589
  color: '#f59e0b',
589
590
  whiteSpace: 'nowrap',
@@ -596,7 +597,7 @@ function RawEventCard({
596
597
  <span
597
598
  title={`${snapshotCount} raw event snapshots collapsed into this row`}
598
599
  style={{
599
- fontSize: 11,
600
+ fontSize: theme.fontSizes[0],
600
601
  fontWeight: 600,
601
602
  color: '#60a5fa',
602
603
  whiteSpace: 'nowrap',
@@ -616,7 +617,7 @@ function RawEventCard({
616
617
  >
617
618
  {describe(event)}
618
619
  </span>
619
- <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>
620
+ <span style={{ color: '#6b7280', fontSize: theme.fontSizes[0], whiteSpace: 'nowrap' }}>
620
621
  {formatTime(eventTimestamp(event))}
621
622
  </span>
622
623
  </button>
@@ -673,7 +674,7 @@ function NormalizedEventCard({
673
674
  overflow: 'hidden',
674
675
  backgroundColor: expanded ? '#0d1520' : '#101722',
675
676
  fontFamily: theme.fonts.body,
676
- fontSize: 13,
677
+ fontSize: theme.fontSizes[0],
677
678
  }}
678
679
  >
679
680
  <button
@@ -692,10 +693,10 @@ function NormalizedEventCard({
692
693
  textAlign: 'left',
693
694
  }}
694
695
  >
695
- <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>#{seq}</span>
696
+ <span style={{ color: '#6b7280', fontSize: theme.fontSizes[0], whiteSpace: 'nowrap' }}>#{seq}</span>
696
697
  <span
697
698
  style={{
698
- fontSize: 11,
699
+ fontSize: theme.fontSizes[0],
699
700
  fontWeight: 600,
700
701
  textTransform: 'uppercase',
701
702
  letterSpacing: 0.5,
@@ -716,7 +717,7 @@ function NormalizedEventCard({
716
717
  >
717
718
  {normalizedDescribe(event)}
718
719
  </span>
719
- <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>
720
+ <span style={{ color: '#6b7280', fontSize: theme.fontSizes[0], whiteSpace: 'nowrap' }}>
720
721
  {formatTime(event.timestamp)}
721
722
  </span>
722
723
  </button>
@@ -734,7 +735,7 @@ function NormalizedEventCard({
734
735
  />
735
736
  {event.files && event.files.length > 0 ? (
736
737
  <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
737
- <div style={{ fontSize: 11, color: '#6b7280' }}>files ({event.files.length})</div>
738
+ <div style={{ fontSize: theme.fontSizes[0], color: '#6b7280' }}>files ({event.files.length})</div>
738
739
  {event.files.map((file, i) => (
739
740
  <FileRow key={i} file={file} />
740
741
  ))}
@@ -756,14 +757,14 @@ function FileRow({ file }: { file: NormalizedPathInfo }) {
756
757
  <div
757
758
  style={{
758
759
  color: '#93c5fd',
759
- fontSize: 12,
760
+ fontSize: theme.fontSizes[0],
760
761
  wordBreak: 'break-word',
761
762
  fontFamily: theme.fonts.monospace,
762
763
  }}
763
764
  >
764
765
  {file.displayPath}
765
766
  </div>
766
- <div style={{ color: '#6b7280', fontSize: 11, wordBreak: 'break-word' }}>
767
+ <div style={{ color: '#6b7280', fontSize: theme.fontSizes[0], wordBreak: 'break-word' }}>
767
768
  {file.context}
768
769
  {file.repository
769
770
  ? ` · ${file.repository.gitRoot} → ${file.repository.relativePath}`
@@ -817,7 +818,7 @@ function AccumulatedEventCard({
817
818
  border: '1px dashed #1f2937',
818
819
  backgroundColor: '#0d1117',
819
820
  fontFamily: theme.fonts.body,
820
- fontSize: 12,
821
+ fontSize: theme.fontSizes[0],
821
822
  }}
822
823
  >
823
824
  <span style={{ color: '#374151', whiteSpace: 'nowrap' }}>#{seq}</span>
@@ -842,7 +843,7 @@ function AccumulatedEventCard({
842
843
  overflow: 'hidden',
843
844
  backgroundColor: expanded ? '#121a13' : '#141b16',
844
845
  fontFamily: theme.fonts.body,
845
- fontSize: 13,
846
+ fontSize: theme.fontSizes[0],
846
847
  }}
847
848
  >
848
849
  <button
@@ -870,10 +871,10 @@ function AccumulatedEventCard({
870
871
  backgroundColor: event.sessionColor,
871
872
  }}
872
873
  />
873
- <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>#{seq}</span>
874
+ <span style={{ color: '#6b7280', fontSize: theme.fontSizes[0], whiteSpace: 'nowrap' }}>#{seq}</span>
874
875
  <span
875
876
  style={{
876
- fontSize: 11,
877
+ fontSize: theme.fontSizes[0],
877
878
  fontWeight: 600,
878
879
  textTransform: 'uppercase',
879
880
  letterSpacing: 0.5,
@@ -894,7 +895,7 @@ function AccumulatedEventCard({
894
895
  >
895
896
  {event.description}
896
897
  </span>
897
- <span style={{ color: '#6b7280', fontSize: 11, whiteSpace: 'nowrap' }}>
898
+ <span style={{ color: '#6b7280', fontSize: theme.fontSizes[0], whiteSpace: 'nowrap' }}>
898
899
  {formatTime(event.timestamp)}
899
900
  </span>
900
901
  </button>
@@ -910,18 +911,18 @@ function AccumulatedEventCard({
910
911
  ['childSessionId', event.childSessionId],
911
912
  ]}
912
913
  />
913
- <div style={{ fontSize: 12, color: '#6b7280' }}>{event.description}</div>
914
+ <div style={{ fontSize: theme.fontSizes[0], color: '#6b7280' }}>{event.description}</div>
914
915
  {event.files.length > 0 ? <PathList label="files" paths={event.files} /> : null}
915
916
  {event.dependencies.length > 0 ? (
916
917
  <PathList label="dependencies" paths={event.dependencies} />
917
918
  ) : null}
918
919
  {event.layers.length > 0 ? (
919
920
  <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
920
- <div style={{ fontSize: 11, color: '#6b7280' }}>layers</div>
921
+ <div style={{ fontSize: theme.fontSizes[0], color: '#6b7280' }}>layers</div>
921
922
  {event.layers.map((layer) => (
922
923
  <div
923
924
  key={layer.id}
924
- style={{ display: 'flex', gap: 6, alignItems: 'center', fontSize: 12 }}
925
+ style={{ display: 'flex', gap: 6, alignItems: 'center', fontSize: theme.fontSizes[0] }}
925
926
  >
926
927
  <span
927
928
  style={{
@@ -949,11 +950,11 @@ function PathList({ label, paths }: { label: string; paths: NormalizedPathInfo[]
949
950
  const { theme } = useTheme();
950
951
  return (
951
952
  <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
952
- <div style={{ fontSize: 11, color: '#6b7280' }}>{label}</div>
953
+ <div style={{ fontSize: theme.fontSizes[0], color: '#6b7280' }}>{label}</div>
953
954
  {paths.map((p, i) => (
954
955
  <div
955
956
  key={i}
956
- style={{ color: '#93c5fd', fontSize: 11.5, fontFamily: theme.fonts.monospace, wordBreak: 'break-word' }}
957
+ style={{ color: '#93c5fd', fontSize: theme.fontSizes[0], fontFamily: theme.fonts.monospace, wordBreak: 'break-word' }}
957
958
  >
958
959
  {p.displayPath}
959
960
  </div>
@@ -963,12 +964,13 @@ function PathList({ label, paths }: { label: string; paths: NormalizedPathInfo[]
963
964
  }
964
965
 
965
966
  function TextView({ text }: { text?: string }) {
966
- if (!text) return <div style={{ color: '#6b7280', fontSize: 12 }}>(empty)</div>;
967
+ const { theme } = useTheme();
968
+ if (!text) return <div style={{ color: '#6b7280', fontSize: theme.fontSizes[0] }}>(empty)</div>;
967
969
  return (
968
970
  <div
969
971
  style={{
970
972
  color: '#d1d5db',
971
- fontSize: 12.5,
973
+ fontSize: theme.fontSizes[0],
972
974
  lineHeight: 1.5,
973
975
  maxHeight: 120,
974
976
  overflowY: 'auto',
@@ -988,11 +990,12 @@ const TODO_STATUS_COLORS: Record<string, string> = {
988
990
  };
989
991
 
990
992
  function TodosView({ todos }: { todos: Array<Record<string, unknown>> }) {
993
+ const { theme } = useTheme();
991
994
  return (
992
995
  <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
993
- <div style={{ fontSize: 11, color: '#6b7280' }}>todos ({todos.length})</div>
996
+ <div style={{ fontSize: theme.fontSizes[0], color: '#6b7280' }}>todos ({todos.length})</div>
994
997
  {todos.map((t, i) => (
995
- <div key={i} style={{ display: 'flex', gap: 8, fontSize: 12, alignItems: 'baseline' }}>
998
+ <div key={i} style={{ display: 'flex', gap: 8, fontSize: theme.fontSizes[0], alignItems: 'baseline' }}>
996
999
  <span
997
1000
  style={{
998
1001
  color: TODO_STATUS_COLORS[String(t.status ?? '')] ?? '#6b7280',
@@ -1013,13 +1016,15 @@ function TodosView({ todos }: { todos: Array<Record<string, unknown>> }) {
1013
1016
  );
1014
1017
  }
1015
1018
 
1016
- function ToolIO({ input, output }: { input?: unknown; output?: unknown }) { const summary = toolSummary(input);
1019
+ function ToolIO({ input, output }: { input?: unknown; output?: unknown }) {
1020
+ const { theme } = useTheme();
1021
+ const summary = toolSummary(input);
1017
1022
  return (
1018
1023
  <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
1019
1024
  {summary ? (
1020
1025
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1021
- <span style={{ fontSize: 11, color: '#60a5fa', fontWeight: 600 }}>IN</span>
1022
- <code style={{ color: '#93c5fd', fontSize: 12, wordBreak: 'break-word' }}>{summary}</code>
1026
+ <span style={{ fontSize: theme.fontSizes[0], color: '#60a5fa', fontWeight: 600 }}>IN</span>
1027
+ <code style={{ color: '#93c5fd', fontSize: theme.fontSizes[0], wordBreak: 'break-word' }}>{summary}</code>
1023
1028
  </div>
1024
1029
  ) : null}
1025
1030
  {input !== undefined && input !== null && typeof input === 'object' ? (
@@ -1028,8 +1033,8 @@ function ToolIO({ input, output }: { input?: unknown; output?: unknown }) { con
1028
1033
  {output !== undefined && output !== null ? (
1029
1034
  <>
1030
1035
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1031
- <span style={{ fontSize: 11, color: '#34d399', fontWeight: 600 }}>OUT</span>
1032
- <span style={{ fontSize: 11, color: '#6b7280' }}>
1036
+ <span style={{ fontSize: theme.fontSizes[0], color: '#34d399', fontWeight: 600 }}>OUT</span>
1037
+ <span style={{ fontSize: theme.fontSizes[0], color: '#6b7280' }}>
1033
1038
  {typeof output === 'string' ? `${output.length.toLocaleString()} chars` : 'object'}
1034
1039
  </span>
1035
1040
  </div>
@@ -1041,6 +1046,7 @@ function ToolIO({ input, output }: { input?: unknown; output?: unknown }) { con
1041
1046
  }
1042
1047
 
1043
1048
  function CodeBlock({ value }: { value: string }) {
1049
+ const { theme } = useTheme();
1044
1050
  return (
1045
1051
  <pre
1046
1052
  style={{
@@ -1049,7 +1055,7 @@ function CodeBlock({ value }: { value: string }) {
1049
1055
  borderRadius: 6,
1050
1056
  backgroundColor: '#0b1220',
1051
1057
  color: '#9ca3af',
1052
- fontSize: 11.5,
1058
+ fontSize: theme.fontSizes[0],
1053
1059
  lineHeight: 1.45,
1054
1060
  maxHeight: 180,
1055
1061
  overflow: 'auto',
@@ -1063,12 +1069,13 @@ function CodeBlock({ value }: { value: string }) {
1063
1069
  }
1064
1070
 
1065
1071
  function MetaRows({ rows }: { rows: Array<[string, unknown]> }) {
1072
+ const { theme } = useTheme();
1066
1073
  const present = rows.filter(([, v]) => v !== undefined && v !== null && v !== '');
1067
1074
  if (present.length === 0) return null;
1068
1075
  return (
1069
1076
  <div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
1070
1077
  {present.map(([label, value]) => (
1071
- <div key={label} style={{ display: 'flex', gap: 8, fontSize: 12 }}>
1078
+ <div key={label} style={{ display: 'flex', gap: 8, fontSize: theme.fontSizes[0] }}>
1072
1079
  <span style={{ color: '#6b7280', minWidth: 76, whiteSpace: 'nowrap' }}>{label}</span>
1073
1080
  <span
1074
1081
  style={{
@@ -1382,8 +1389,8 @@ export function SessionEventFeed({ title, rows }: SessionEventFeedProps) {
1382
1389
  fontFamily: theme.fonts.body,
1383
1390
  }}
1384
1391
  >
1385
- <h1 style={{ margin: 0, color: '#f3f4f6', fontSize: 18, fontWeight: 600 }}>{title}</h1>
1386
- <span style={{ color: '#6b7280', fontSize: 12 }}>
1392
+ <h1 style={{ margin: 0, color: '#f3f4f6', fontSize: theme.fontSizes[3], fontWeight: 600 }}>{title}</h1>
1393
+ <span style={{ color: '#6b7280', fontSize: theme.fontSizes[0] }}>
1387
1394
  {items.length} raw events · {visibleEvents.length} visible · {expandedCount} expanded
1388
1395
  </span>
1389
1396
  </div>
@@ -1456,7 +1463,7 @@ export function SessionEventFeedGrouped({ title, rows }: SessionEventFeedGrouped
1456
1463
  return (
1457
1464
  <div style={{ padding: 24, backgroundColor: '#0d1117', minHeight: '100vh' }}>
1458
1465
  {title ? (
1459
- <h1 style={{ margin: '0 0 12px', color: '#f3f4f6', fontSize: 18, fontWeight: 600, fontFamily: theme.fonts.body }}>
1466
+ <h1 style={{ margin: '0 0 12px', color: '#f3f4f6', fontSize: theme.fontSizes[3], fontWeight: 600, fontFamily: theme.fonts.body }}>
1460
1467
  {title}
1461
1468
  </h1>
1462
1469
  ) : null}
@@ -1494,10 +1501,10 @@ function GroupSection({
1494
1501
  fontFamily: theme.fonts.body,
1495
1502
  }}
1496
1503
  >
1497
- <span style={{ color: kindColor(kind), fontWeight: 600, fontSize: 13 }}>
1504
+ <span style={{ color: kindColor(kind), fontWeight: 600, fontSize: theme.fontSizes[0] }}>
1498
1505
  {kindLabel(kind)}
1499
1506
  </span>
1500
- <span style={{ color: '#6b7280', fontSize: 12 }}>{group.length}</span>
1507
+ <span style={{ color: '#6b7280', fontSize: theme.fontSizes[0] }}>{group.length}</span>
1501
1508
  <span style={{ flex: 1 }} />
1502
1509
  <FilterChip label="Expand all" active={false} onClick={() => setAll(false)} />
1503
1510
  <FilterChip label="Collapse all" active={false} onClick={() => setAll(true)} />
@@ -1546,10 +1553,11 @@ function ColumnHeaders() {
1546
1553
  }
1547
1554
 
1548
1555
  function HeaderLabel({ children }: { children: React.ReactNode }) {
1556
+ const { theme } = useTheme();
1549
1557
  return (
1550
1558
  <span
1551
1559
  style={{
1552
- fontSize: 11,
1560
+ fontSize: theme.fontSizes[0],
1553
1561
  fontWeight: 600,
1554
1562
  textTransform: 'uppercase',
1555
1563
  letterSpacing: 0.5,
@@ -1742,7 +1750,7 @@ function EventRow({
1742
1750
  cursor: 'pointer',
1743
1751
  background: copied ? 'rgba(16,185,129,0.12)' : 'transparent',
1744
1752
  color: copied ? '#10b981' : theme.colors.textTertiary,
1745
- fontSize: 12,
1753
+ fontSize: theme.fontSizes[0],
1746
1754
  }}
1747
1755
  >
1748
1756
  {copied ? <Check size={13} /> : <Copy size={13} />}
@@ -1777,6 +1785,7 @@ function FilterChip({
1777
1785
  color?: string;
1778
1786
  onClick: () => void;
1779
1787
  }) {
1788
+ const { theme } = useTheme();
1780
1789
  return (
1781
1790
  <button
1782
1791
  onClick={onClick}
@@ -1786,7 +1795,7 @@ function FilterChip({
1786
1795
  border: `1px solid ${active ? color ?? '#3b82f6' : '#1f2937'}`,
1787
1796
  backgroundColor: active ? (color ?? '#3b82f6') + '22' : 'transparent',
1788
1797
  color: active ? '#f3f4f6' : '#9ca3af',
1789
- fontSize: 11.5,
1798
+ fontSize: theme.fontSizes[0],
1790
1799
  cursor: 'pointer',
1791
1800
  fontFamily: 'inherit',
1792
1801
  }}
@@ -7,6 +7,11 @@
7
7
 
8
8
  import { useCallback, useEffect, useMemo, useState } from 'react';
9
9
  import { File } from '@pierre/diffs/react';
10
+ import {
11
+ getFiletypeFromFileName,
12
+ getHighlighterOptions,
13
+ preloadHighlighter,
14
+ } from '@pierre/diffs';
10
15
  import { useTheme } from '@principal-ade/industry-theme';
11
16
  import { buildPierreOptions, PIERRE_FILE_STYLE } from './pierreBackground';
12
17
  import { pierreLangForPath } from './pierreFileLang';
@@ -33,6 +38,12 @@ export function PierreFileView({
33
38
  const { theme } = useTheme();
34
39
  const [contents, setContents] = useState<string | null>(null);
35
40
  const [error, setError] = useState<string | null>(null);
41
+ // Pierre highlights on the main thread with no worker pool: a cold first
42
+ // render paints an empty <pre> and never re-renders once Shiki resolves.
43
+ // Warm the shared highlighter for this file before mounting <File> so the
44
+ // very first render produces content.
45
+ const [highlighterReady, setHighlighterReady] = useState(false);
46
+ const lang = pierreLangForPath(filePath) ?? getFiletypeFromFileName(fileName);
36
47
 
37
48
  const fileObject = useMemo(
38
49
  () =>
@@ -60,6 +71,21 @@ export function PierreFileView({
60
71
  };
61
72
  }, [filePath, readFile]);
62
73
 
74
+ useEffect(() => {
75
+ let cancelled = false;
76
+ setHighlighterReady(false);
77
+ void preloadHighlighter(getHighlighterOptions(lang, {}))
78
+ .catch(() => {
79
+ // Fall through: let <File> attempt its own (plain-text) render.
80
+ })
81
+ .then(() => {
82
+ if (!cancelled) setHighlighterReady(true);
83
+ });
84
+ return () => {
85
+ cancelled = true;
86
+ };
87
+ }, [lang]);
88
+
63
89
  const onPostRender = useCallback(
64
90
  (fileContainer: HTMLElement) => {
65
91
  if (focusLine == null) return;
@@ -83,7 +109,7 @@ export function PierreFileView({
83
109
  </div>
84
110
  );
85
111
  }
86
- if (fileObject === null) {
112
+ if (fileObject === null || !highlighterReady) {
87
113
  return (
88
114
  <div style={{ padding: 16, color: theme.colors.textSecondary }}>
89
115
  Loading…
@@ -174,18 +174,6 @@ export function ConstructsCatalog({
174
174
  [onFileSelect],
175
175
  );
176
176
 
177
- const openComponentSource = useCallback(
178
- (c: SubsystemComponent) => {
179
- if (!c.file) return;
180
- const startLine = c.declarationRef?.startLine;
181
- onOpenFile(
182
- c.file,
183
- startLine != null ? { startLine } : undefined,
184
- );
185
- },
186
- [onOpenFile],
187
- );
188
-
189
177
  const onRelatedSelect = useCallback(
190
178
  (ref: string) => {
191
179
  const comp = resolveRelated(components, ref);
@@ -385,7 +373,6 @@ export function ConstructsCatalog({
385
373
  theme={theme}
386
374
  disableScroll={true}
387
375
  disableBasePadding
388
- fontSizeScale={0.9}
389
376
  enableKeyboardScrolling={false}
390
377
  autoFocusOnVisible={false}
391
378
  />
@@ -426,6 +413,7 @@ export function ConstructsCatalog({
426
413
  const focused = c.id === focusedId;
427
414
  const color = componentColor(c, pierreTheme);
428
415
  const badgeColor = constructBadgeColor(c) ?? color;
416
+ const badgeLabel = constructBadgeLabel(c);
429
417
  const displayName = deriveNameFromSymbol(
430
418
  c.symbol,
431
419
  c.construct,
@@ -490,73 +478,45 @@ export function ConstructsCatalog({
490
478
  >
491
479
  {displayName}
492
480
  </span>
493
- <span
494
- style={{
495
- flexShrink: 0,
496
- fontFamily: theme.fonts.monospace,
497
- fontSize: theme.fontSizes[0] * 0.95,
498
- letterSpacing: 0.4,
499
- textTransform: 'uppercase',
500
- color: badgeColor,
501
- }}
502
- >
503
- {constructBadgeLabel(c)}
504
- </span>
481
+ {role && roleColor && (
482
+ <span
483
+ style={{
484
+ flexShrink: 0,
485
+ fontFamily: theme.fonts.monospace,
486
+ fontSize: theme.fontSizes[0],
487
+ letterSpacing: 0.4,
488
+ textTransform: 'uppercase',
489
+ color: roleColor,
490
+ }}
491
+ >
492
+ {role}
493
+ </span>
494
+ )}
505
495
  </span>
506
- {(c.file || role) && (
496
+ {badgeLabel && (
507
497
  <span
508
498
  style={{
509
499
  display: 'flex',
510
500
  alignItems: 'center',
511
- gap: 8,
512
501
  minWidth: 0,
513
502
  }}
514
503
  >
515
- {c.file ? (
516
- <span
517
- title={fileViewer ? `Open ${c.file}` : c.file}
518
- onClick={
519
- fileViewer
520
- ? (e) => {
521
- e.stopPropagation();
522
- openComponentSource(c);
523
- }
524
- : undefined
525
- }
526
- style={{
527
- flex: 1,
528
- minWidth: 0,
529
- overflow: 'hidden',
530
- textOverflow: 'ellipsis',
531
- whiteSpace: 'nowrap',
532
- fontFamily: theme.fonts.body,
533
- fontSize: theme.fontSizes[0],
534
- color: muted,
535
- cursor: fileViewer ? 'pointer' : undefined,
536
- textDecoration: fileViewer ? 'underline' : undefined,
537
- textDecorationStyle: fileViewer ? 'dotted' : undefined,
538
- textUnderlineOffset: 2,
539
- }}
540
- >
541
- {c.file}
542
- </span>
543
- ) : (
544
- <span style={{ flex: 1 }} />
545
- )}
546
- {role && roleColor && (
547
- <span
548
- style={{
549
- flexShrink: 0,
550
- fontFamily: theme.fonts.monospace,
551
- fontSize: theme.fontSizes[0] * 0.9,
552
- letterSpacing: 0.4,
553
- textTransform: 'uppercase',
554
- color: roleColor,
555
- }}
556
- >
557
- {role}
558
- </span>
559
- )}
504
+ <span
505
+ style={{
506
+ flex: 1,
507
+ minWidth: 0,
508
+ overflow: 'hidden',
509
+ textOverflow: 'ellipsis',
510
+ whiteSpace: 'nowrap',
511
+ fontFamily: theme.fonts.monospace,
512
+ fontSize: theme.fontSizes[0],
513
+ letterSpacing: 0.4,
514
+ textTransform: 'uppercase',
515
+ color: badgeColor,
516
+ }}
517
+ >
518
+ {badgeLabel}
519
+ </span>
560
520
  </span>
561
521
  )}
562
522
  </button>
@@ -123,8 +123,34 @@ export function humanizeIssueKind(kind: string): string {
123
123
  return spaced.charAt(0).toUpperCase() + spaced.slice(1);
124
124
  }
125
125
 
126
+ /**
127
+ * Curated short labels, trimmed of prefixes the grouping already implies
128
+ * (`topology_`, `boundary_`) but kept unambiguous within a layer — so
129
+ * `construct_mismatch` reads "Wrong construct", not a bare "Mismatch" that
130
+ * would collide with `signature_mismatch`. Explicit `issue.kindLabel` wins.
131
+ */
132
+ const KIND_LABEL: Record<string, string> = {
133
+ missing_file: 'Missing file',
134
+ missing_symbol: 'Missing symbol',
135
+ stale_declaration: 'Stale declaration',
136
+ construct_mismatch: 'Wrong construct',
137
+ construct_unconfirmed: 'Unconfirmed construct',
138
+ signature_mismatch: 'Signature mismatch',
139
+ signature_unconfirmed: 'Signature unconfirmed',
140
+ anchor: 'Anchor',
141
+ unresolved: 'Unresolved',
142
+ topology_broken_endpoint: 'Broken endpoint',
143
+ topology_import_unconfirmed: 'Import unconfirmed',
144
+ topology_relation_unconfirmed: 'Relation unconfirmed',
145
+ boundary_module_without_file: 'Module without file',
146
+ boundary_module_file_mismatch: 'Module file mismatch',
147
+ boundary_process_nest_disagree: 'Process nest disagree',
148
+ walkthrough: 'Step issue',
149
+ };
150
+
151
+ /** Label for an issue's kind. Explicit `kindLabel` wins; unknown kinds humanize. */
126
152
  export function issueKindLabel(issue: SubsystemIssue): string {
127
- return issue.kindLabel ?? humanizeIssueKind(issue.kind);
153
+ return issue.kindLabel ?? KIND_LABEL[issue.kind] ?? humanizeIssueKind(issue.kind);
128
154
  }
129
155
 
130
156
  export function severityRank(severity: SubsystemIssueSeverity): number {
@@ -300,7 +326,6 @@ export function SubsystemIssueCard({
300
326
  flexDirection: 'column',
301
327
  gap: 5,
302
328
  padding: '9px 12px 10px',
303
- borderLeft: `3px solid ${color}`,
304
329
  borderBottom: `1px solid ${theme.colors.border}`,
305
330
  background: hover ? theme.colors.background : 'transparent',
306
331
  cursor: 'pointer',
@@ -308,25 +333,24 @@ export function SubsystemIssueCard({
308
333
  fontFamily: theme.fonts.body,
309
334
  }}
310
335
  >
311
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
336
+ <div style={{ display: 'flex', alignItems: 'flex-start', gap: 6, minWidth: 0 }}>
312
337
  <div
313
338
  style={{
314
339
  flex: 1,
315
340
  minWidth: 0,
316
341
  display: 'flex',
317
- alignItems: 'center',
318
- gap: 6,
342
+ flexDirection: 'column',
343
+ gap: 1,
319
344
  }}
320
345
  >
321
346
  <span
322
347
  style={{
323
- flexShrink: 0,
324
348
  fontFamily: theme.fonts.monospace,
325
- fontSize: theme.fontSizes[0] * 0.86,
349
+ fontSize: theme.fontSizes[0],
326
350
  fontWeight: 600,
327
351
  letterSpacing: 0.3,
328
352
  textTransform: 'uppercase',
329
- color: muted,
353
+ color,
330
354
  whiteSpace: 'nowrap',
331
355
  }}
332
356
  >
@@ -337,7 +361,7 @@ export function SubsystemIssueCard({
337
361
  style={{
338
362
  minWidth: 0,
339
363
  fontFamily: theme.fonts.monospace,
340
- fontSize: theme.fontSizes[0] * 0.82,
364
+ fontSize: theme.fontSizes[0],
341
365
  color: theme.colors.text,
342
366
  whiteSpace: 'nowrap',
343
367
  overflow: 'hidden',
@@ -349,7 +373,14 @@ export function SubsystemIssueCard({
349
373
  </span>
350
374
  )}
351
375
  </div>
352
- <span style={{ display: 'inline-flex', color: muted, flexShrink: 0 }}>
376
+ <span
377
+ style={{
378
+ display: 'inline-flex',
379
+ color: muted,
380
+ flexShrink: 0,
381
+ marginTop: 1,
382
+ }}
383
+ >
353
384
  {expanded ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
354
385
  </span>
355
386
  </div>