@xpufx/paseo-top 0.4.3 → 0.4.5

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 (130) hide show
  1. package/OVERVIEW.md +17 -0
  2. package/client/host-ui.tsx +184 -0
  3. package/client/multi-host-view.tsx +57 -43
  4. package/client/multi-host.ts +36 -13
  5. package/client/pill-labels.ts +124 -7
  6. package/client/pill.tsx +724 -669
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +33 -0
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +310 -274
  11. package/client/telemetry-copy.ts +8 -2
  12. package/client/telemetry.tsx +372 -321
  13. package/client/turn-panel.tsx +126 -104
  14. package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +21 -61
  15. package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
  16. package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
  17. package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
  18. package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
  19. package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
  20. package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
  21. package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
  22. package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
  23. package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
  24. package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
  25. package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
  26. package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
  27. package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
  28. package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +118 -338
  29. package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
  30. package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
  31. package/client/vendor/paseo-plugin-helper/lifecycle/surface.tsx +140 -0
  32. package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
  33. package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
  34. package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
  35. package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +55 -47
  36. package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
  37. package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
  38. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
  39. package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
  40. package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
  41. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  42. package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
  43. package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
  44. package/client/vendor/permission-audit/client.tsx +706 -0
  45. package/client/vendor/permission-audit/index.tsx +1 -0
  46. package/index.client.tsx +58 -37
  47. package/index.server.ts +256 -227
  48. package/package.json +14 -2
  49. package/paseo-plugin.json +2 -0
  50. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  51. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  52. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  53. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  54. package/screenshots/paseo-top-dashboard.jpg +0 -0
  55. package/screenshots/paseo-top-general-view.jpg +0 -0
  56. package/server/resources.ts +98 -0
  57. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  58. package/server/vendor/paseo-plugin-helper/index.ts +4 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
  61. package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
  62. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  63. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  64. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  65. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  66. package/server/vendor/paseo-plugin-helper/tickets.ts +1232 -0
  67. package/server/vendor/permission-audit/index.ts +1 -0
  68. package/server/vendor/permission-audit/server.ts +839 -0
  69. package/shared/helper-version.ts +20 -0
  70. package/shared/multi-host.ts +156 -155
  71. package/shared/resources.ts +146 -1
  72. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  73. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  74. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  75. package/shared/vendor/paseo-plugin-helper/index.ts +2 -0
  76. package/shared/vendor/paseo-plugin-helper/tickets.ts +1104 -0
  77. package/shared/vendor/permission-audit/filter.ts +61 -0
  78. package/shared/vendor/permission-audit/index.ts +2 -0
  79. package/shared/vendor/permission-audit/shared.ts +119 -0
  80. package/shared/version.ts +1 -1
  81. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  82. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  83. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -156
  84. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -184
  85. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -225
  86. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  87. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  88. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  89. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  90. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  91. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  92. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -75
  93. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  94. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  95. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  96. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  97. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -118
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -80
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -215
  100. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  101. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  102. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  103. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  104. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  105. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -25
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -224
  107. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  108. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  109. package/client/vendor/paseo-plugin-helper/index.ts +0 -29
  110. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  111. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  112. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  113. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -378
  114. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  115. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  117. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  118. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  119. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  120. package/client/vendor/paseo-plugin-helper/surface.tsx +0 -80
  121. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  122. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  123. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  124. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  125. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  126. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  127. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  128. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  130. /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
package/client/pill.tsx CHANGED
@@ -7,48 +7,52 @@ import {
7
7
  useRpc,
8
8
  type PluginClientContext,
9
9
  } from "@getpaseo/plugin/client";
10
+ import { Icon } from "@getpaseo/plugin/client/react-native";
10
11
  import {
11
12
  registerComposerPill,
12
13
  registerSidebarSurface,
13
- type ComposerPillRegistrar,
14
+ triggerHaptic,
14
15
  type SidebarSurfaceRegistrar,
15
16
  type PillLiveContext,
16
17
  type RegisterComposerPillOptions,
17
- ModalBody,
18
- Card,
19
- CardHeader,
20
- Button,
21
- Row,
22
- KeyValue,
23
- KeyValueGroup,
24
- ProgressBar,
25
- MetricGauge,
26
- Tabs,
27
- Toggle,
28
- Badge,
29
- Icon,
30
- AboutSection,
31
- CustomPillBody,
32
- CustomPillModalContent,
18
+ type RenderModalProps,
19
+ } from "./vendor/paseo-plugin-helper/lifecycle/index";
20
+ import {
21
+ getClientHost,
33
22
  useRpcQuery,
34
23
  usePluginSettings,
35
- usePluginTheme,
24
+ type ComposerPillRegistrar,
25
+ type HostPillProps,
26
+ } from "./vendor/paseo-plugin-helper/core/index";
27
+ import {
28
+ HostAboutSection,
29
+ HostBadge,
30
+ HostButton,
31
+ HostCard,
32
+ HostCardHeader,
33
+ HostCopyButton,
34
+ HostKeyValue,
35
+ HostLayoutProvider,
36
+ HostMetricGauge,
37
+ HostModalSection,
38
+ HostProgressBar,
39
+ HostRow,
40
+ HostStack,
41
+ HostTabs,
42
+ HostThemeProvider,
43
+ HostToggle,
36
44
  getStatusColor,
37
- triggerHaptic,
38
- shouldEmitSnapshotUpdate,
39
- type RenderModalProps,
40
- type RenderPillProps,
41
- type KeyValueProps,
42
- type CardHeaderProps,
43
- type BadgeProps,
44
- } from "./vendor/paseo-plugin-helper/index";
45
+ useHostLayout,
46
+ useHostTheme,
47
+ } from "./host-ui";
45
48
  import {
46
49
  formatBytes,
47
50
  formatUptime,
48
51
  resolveMetricStatus,
49
52
  type MetricThresholds,
50
- type CustomPillState,
53
+ type ThemeColors,
51
54
  } from "../shared/vendor/paseo-plugin-helper/index";
55
+ import { PermissionAuditView } from "./vendor/permission-audit";
52
56
  import {
53
57
  getSystemResourcesRpc,
54
58
  topSettingsContract,
@@ -56,14 +60,18 @@ import {
56
60
  listCustomPillsRpc,
57
61
  runCustomPillModalCommandRpc,
58
62
  isPillEnabled,
59
- isProviderDependent,
63
+ isTimelineEnabled,
60
64
  legacyFlagView,
61
65
  isMcpSurfaceEnabled,
62
66
  customPillEffectiveEnabled,
63
67
  METRIC_DEFINITIONS,
64
68
  DEFAULT_METRIC_SURFACES,
65
- checkboxesFromTarget,
66
69
  targetFromCheckboxes,
70
+ CPU_THRESHOLDS,
71
+ MEM_THRESHOLDS,
72
+ MODAL_TABS,
73
+ aboutExtraItems,
74
+ hasAnyLegacyPillFlagEnabled,
67
75
  type SystemResources,
68
76
  type TopSettings,
69
77
  type CustomPillStateOutput,
@@ -72,8 +80,9 @@ import {
72
80
  } from "../shared/resources";
73
81
  import { PLUGIN_VERSION } from "../shared/version";
74
82
  import { TopDashboardSurface } from "./surface";
83
+ import { addSettingsListener, removeSettingsListener, notifySettingsChanged } from "./settings-events";
75
84
  import { useTopResourceQuery, useCustomPillsQuery } from "./resources-query";
76
- import { ChoiceChips } from "./settings-ui";
85
+ import { ChoiceChips, MultiChoiceChips } from "./settings-ui";
77
86
  import {
78
87
  buildAllLabel,
79
88
  describeSegment,
@@ -91,6 +100,11 @@ import {
91
100
 
92
101
  const EMPTY_PARAMS = {};
93
102
 
103
+ const TIMELINE_CADENCE_OPTIONS = Array.from({ length: 11 }, (_, n) => ({
104
+ id: n,
105
+ label: n === 0 ? "Never" : n === 1 ? "Every turn" : `${n}`,
106
+ }));
107
+
94
108
  function formatWorktreeLocation(dir: string | null | undefined): string {
95
109
  if (!dir) return "";
96
110
  // Check for Paseo-managed worktree: ~/.paseo/worktrees/<hash>/<slug>
@@ -134,24 +148,9 @@ function formatIdleDuration(isoString: string | null | undefined): string {
134
148
  return `${days}d ${hours % 24}h`;
135
149
  }
136
150
 
137
- const CPU_THRESHOLDS: MetricThresholds = { warning: 60, danger: 85 };
138
- const MEM_THRESHOLDS: MetricThresholds = { warning: 70, danger: 85 };
139
-
140
- const TABS = [
141
- { id: "system", label: "System", shortLabel: "System", icon: "Activity" },
142
- { id: "context", label: "Workspace", shortLabel: "Workspace", icon: "GitBranch" },
143
- { id: "settings", label: "Settings", shortLabel: "Settings", icon: "Sliders" },
144
- { id: "about", label: "About", shortLabel: "About", icon: "Info" },
145
- ];
146
-
147
- const TIMELINE_CADENCE_OPTIONS = Array.from({ length: 11 }, (_, n) => ({
148
- id: n,
149
- label: n === 0 ? "Never" : n === 1 ? "Every turn" : `${n}`,
150
- }));
151
-
152
151
  function getMetricColors(
153
152
  data: SystemResources | undefined,
154
- colors: ReturnType<typeof usePluginTheme>["colors"],
153
+ colors: ThemeColors,
155
154
  ) {
156
155
  if (
157
156
  !data ||
@@ -177,7 +176,7 @@ function getMetricColors(
177
176
  // so pill call sites keep a single import path.
178
177
  export type { PillItemType } from "./pill-labels";
179
178
 
180
- export type ModalTab = "system" | "context" | "settings" | "about";
179
+ export type ModalTab = "system" | "context" | "permissions" | "settings" | "about";
181
180
 
182
181
  export function getItemTab(item: PillItemType): "system" | "context" {
183
182
  switch (item) {
@@ -189,6 +188,7 @@ export function getItemTab(item: PillItemType): "system" | "context" {
189
188
  case "tokens":
190
189
  case "tools":
191
190
  case "turns":
191
+ case "velocity":
192
192
  return "system";
193
193
  case "branch":
194
194
  case "worktree":
@@ -203,28 +203,15 @@ export function getItemTab(item: PillItemType): "system" | "context" {
203
203
 
204
204
  const currentCycleTabByAgent = new Map<string, ModalTab>();
205
205
 
206
- // The 0.8 host popover is a narrow column, so every helper text component in
207
- // the modal path renders through these compact wrappers (2-3pt under helper
208
- // defaults). Call-site props still win via spread order.
209
- function CompactKeyValue(props: KeyValueProps) {
210
- return (
211
- <KeyValue
212
- labelStyle={styles.compactKvLabel}
213
- valueStyle={styles.compactKvValue}
214
- {...props}
215
- />
216
- );
217
- }
218
-
219
- function CompactCardHeader(props: CardHeaderProps) {
220
- return (
221
- <CardHeader titleStyle={styles.compactCardTitle} {...props} />
222
- );
223
- }
224
-
225
- function CompactBadge(props: BadgeProps) {
226
- return <Badge textStyle={styles.compactBadgeText} {...props} />;
227
- }
206
+ // The 0.8 host popover is a narrow column, so modal-path text renders a few
207
+ // points under the ui/ adapter defaults. These spread props apply that
208
+ // compact rhythm at each call site.
209
+ const compactKvProps = {
210
+ labelStyle: { fontSize: 9 },
211
+ valueStyle: { fontSize: 10 },
212
+ } as const;
213
+ const compactCardTitleProp = { titleStyle: { fontSize: 11 } } as const;
214
+ const compactBadgeProp = { textStyle: { fontSize: 9 } } as const;
228
215
 
229
216
  interface LiveSnapshot extends SegmentSnapshot {
230
217
  workspaceDirectory?: string | null;
@@ -352,7 +339,16 @@ async function customPillSnapshot(): Promise<CustomPillStateOutput[]> {
352
339
  * per-metric pills, custom metric pills) so the presentation model cannot
353
340
  * diverge: all are centered surfaces with a pinned popover width. Call sites
354
341
  * only describe what is pill-specific.
342
+ *
343
+ * `onError` is wired here once for every variant: the helper reports
344
+ * registration and resolveLabel failures through `options.onError` in addition
345
+ * to its own console warning, so without this default a failing pill RPC left
346
+ * the pill missing with no trace outside the helper log. The toast is
347
+ * throttled per pill+agent so a persistently failing RPC cannot spam the host.
355
348
  */
349
+ const PILL_ERROR_TOAST_THROTTLE_MS = 60_000;
350
+ const lastPillErrorToastAt = new Map<string, number>();
351
+
356
352
  function registerTopPill(
357
353
  client: ComposerPillRegistrar | PluginClientContext,
358
354
  options: Omit<RegisterComposerPillOptions<ModalTab>, "presentation" | "popoverWidth"> & {
@@ -362,8 +358,29 @@ function registerTopPill(
362
358
  ) {
363
359
  return registerComposerPill<ModalTab>(client as ComposerPillRegistrar, {
364
360
  popoverWidth: 360,
361
+ // The host `<Modal.Content scrollable>` is the one scroll owner (#219,
362
+ // #975, #1054); the plugin body renders fluid content with no nested
363
+ // scroller so the host owns the mobile gesture.
364
+ hostScroll: true,
365
365
  ...options,
366
366
  presentation: options.presentation ?? "centered",
367
+ onError:
368
+ options.onError ??
369
+ (({ agentId, workspaceId, error }) => {
370
+ console.error(
371
+ `[top] pill "${options.id}" failed (agent ${agentId}, workspace ${workspaceId}): ${error.message}`,
372
+ );
373
+ const key = `${options.id}:${agentId}`;
374
+ const now = Date.now();
375
+ if (now - (lastPillErrorToastAt.get(key) ?? 0) >= PILL_ERROR_TOAST_THROTTLE_MS) {
376
+ lastPillErrorToastAt.set(key, now);
377
+ try {
378
+ getClientHost().useToast().error?.(`top pill failed: ${error.message}`);
379
+ } catch {
380
+ // No host toast wired; the console error above is the trace.
381
+ }
382
+ }
383
+ }),
367
384
  });
368
385
  }
369
386
 
@@ -390,7 +407,7 @@ function PillItemContent({
390
407
  worktreeLocationText,
391
408
  isOpen,
392
409
  }: PillItemContentProps) {
393
- const { colors } = usePluginTheme();
410
+ const { colors } = useHostTheme();
394
411
  const { cpuColor, memColor } = getMetricColors(data, colors);
395
412
  const descriptor = describeSegment(item, { data, agent, agentId, worktreeLocationText });
396
413
  const toneColor = (tone: SegmentTone): string => {
@@ -415,7 +432,7 @@ function PillItemContent({
415
432
  };
416
433
 
417
434
  return (
418
- <Row gap={4} align="center" style={styles.pillContainer}>
435
+ <HostRow gap={4} align="center" style={styles.pillContainer}>
419
436
  {descriptor.iconTone !== "none" ? (
420
437
  <Icon name={descriptor.icon} size={12} color={toneColor(descriptor.iconTone)} />
421
438
  ) : null}
@@ -444,45 +461,27 @@ function PillItemContent({
444
461
  </Text>
445
462
  ) : null}
446
463
  </Text>
447
- </Row>
464
+ </HostRow>
448
465
  );
449
466
  }
450
467
 
451
- type SettingsListener = (settings: TopSettings) => void;
452
- const settingsListeners = new Set<SettingsListener>();
453
- let lastNotifiedSettings: TopSettings | null = null;
454
-
455
- export function notifySettingsChanged(settings: TopSettings) {
456
- if (!shouldEmitSnapshotUpdate(
457
- lastNotifiedSettings as unknown as Record<string, unknown> | null,
458
- settings as unknown as Record<string, unknown>,
459
- )) {
460
- return;
461
- }
462
- lastNotifiedSettings = settings;
463
- for (const listener of settingsListeners) {
464
- try {
465
- listener(settings);
466
- } catch {
467
- // Ignore listener errors
468
- }
469
- }
470
- }
468
+ export { notifySettingsChanged } from "./settings-events";
469
+ type SettingsListener = import("./settings-events").SettingsListener;
471
470
 
472
471
  function PillOffline() {
473
- const { colors } = usePluginTheme();
472
+ const { colors } = useHostTheme();
474
473
  return (
475
- <Row gap={4} align="center" style={styles.pillContainer}>
474
+ <HostRow gap={4} align="center" style={styles.pillContainer}>
476
475
  <Icon name="Ghost" size={13} color={colors.statusDanger} />
477
476
  <Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
478
477
  Offline
479
478
  </Text>
480
- </Row>
479
+ </HostRow>
481
480
  );
482
481
  }
483
482
 
484
483
  function PillLoading() {
485
- const { colors } = usePluginTheme();
484
+ const { colors } = useHostTheme();
486
485
  return (
487
486
  <Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
488
487
  top...
@@ -491,19 +490,19 @@ function PillLoading() {
491
490
  }
492
491
 
493
492
  function PillEmpty() {
494
- const { colors } = usePluginTheme();
493
+ const { colors } = useHostTheme();
495
494
  return (
496
- <Row gap={4} align="center" style={styles.pillContainer}>
495
+ <HostRow gap={4} align="center" style={styles.pillContainer}>
497
496
  <Icon name="Activity" size={12} color={colors.accent} />
498
497
  <Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
499
498
  top
500
499
  </Text>
501
- </Row>
500
+ </HostRow>
502
501
  );
503
502
  }
504
503
 
505
504
  function McpLoadingPill() {
506
- const { colors } = usePluginTheme();
505
+ const { colors } = useHostTheme();
507
506
  return (
508
507
  <Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
509
508
  MCP…
@@ -521,9 +520,8 @@ interface PillSegmentProps {
521
520
  }
522
521
 
523
522
  /**
524
- * The composer pill body is host-pressed: legacy hosts wrap `renderPill` in
525
- * their own pressable and route the tap through `resolveDefaultPayload`, so the
526
- * segment must not nest a second interaction handler.
523
+ * The segment must not nest a second interaction handler: on the host-rendered
524
+ * pill it sits inside the host's own pressable.
527
525
  */
528
526
  function PillSegment({
529
527
  item,
@@ -562,9 +560,9 @@ function AllInOnePill({
562
560
  worktreeLocationText,
563
561
  isOpen,
564
562
  }: AllInOnePillProps) {
565
- const { colors } = usePluginTheme();
563
+ const { colors } = useHostTheme();
566
564
  return (
567
- <Row gap={6} align="center" style={styles.allInOneContainer}>
565
+ <HostRow gap={6} align="center" style={styles.allInOneContainer}>
568
566
  {items.map((item, idx) => (
569
567
  <React.Fragment key={item}>
570
568
  {idx > 0 && (
@@ -580,16 +578,41 @@ function AllInOnePill({
580
578
  />
581
579
  </React.Fragment>
582
580
  ))}
583
- </Row>
581
+ </HostRow>
584
582
  );
585
583
  }
586
584
 
587
- export interface SingleItemPillViewProps extends RenderPillProps<ModalTab> {
585
+ /**
586
+ * Props the pill body views used to receive from the helper's `renderPill`
587
+ * option, which the Paseo 0.9 host shape removed: the host renders the pill
588
+ * body itself, so nothing wires these views any more. The shape is kept
589
+ * because `pill-render.test.tsx` still regression-tests their mobile layout
590
+ * (#507) and that coverage is worth more than the dead wiring.
591
+ */
592
+ interface PillBodyProps extends HostPillProps {
593
+ isOpen: boolean;
594
+ open(payload?: ModalTab): void;
595
+ close(): void;
596
+ toggle(payload?: ModalTab): void;
597
+ }
598
+
599
+ export interface SingleItemPillViewProps extends PillBodyProps {
588
600
  item: PillItemType;
589
601
  defaultTab?: ModalTab;
590
602
  }
591
603
 
592
- export function SingleItemPillView({
604
+ export function SingleItemPillView(props: SingleItemPillViewProps) {
605
+ // Legacy pill body view (see PillBodyProps): kept for the #507 render
606
+ // regression tests. The host theme prop flows through HostThemeProvider so
607
+ // the ui/ adapters inside resolve real host colors.
608
+ return (
609
+ <HostThemeProvider theme={props.theme}>
610
+ <SingleItemPillViewBody {...props} />
611
+ </HostThemeProvider>
612
+ );
613
+ }
614
+
615
+ function SingleItemPillViewBody({
593
616
  item,
594
617
  workspaceId,
595
618
  agentId,
@@ -669,7 +692,18 @@ export function SingleItemPillView({
669
692
  );
670
693
  }
671
694
 
672
- function PillView({ isOpen, open, workspaceId, agentId }: RenderPillProps<ModalTab>) {
695
+ function PillView(props: PillBodyProps) {
696
+ // Legacy pill body view (see PillBodyProps): kept for the #507 render
697
+ // regression tests. The host theme prop flows through HostThemeProvider so
698
+ // the ui/ adapters inside resolve real host colors.
699
+ return (
700
+ <HostThemeProvider theme={props.theme}>
701
+ <PillViewBody {...props} />
702
+ </HostThemeProvider>
703
+ );
704
+ }
705
+
706
+ function PillViewBody({ isOpen, open, workspaceId, agentId }: PillBodyProps) {
673
707
  // No background settings poll here: the hook re-verifies on mount and
674
708
  // window focus, and mutations invalidate the shared settings cache.
675
709
  // Polling from every mounted pill/modal/surface multiplied daemon reads
@@ -691,18 +725,7 @@ function PillView({ isOpen, open, workspaceId, agentId }: RenderPillProps<ModalT
691
725
  lastUsage: a?.lastUsage,
692
726
  }));
693
727
 
694
- const hasAnyEnabled =
695
- flags.showCpuRam ||
696
- flags.showBranch ||
697
- flags.showWorktree ||
698
- flags.showAgentTitle ||
699
- flags.showAgent ||
700
- flags.showAgentProvider ||
701
- flags.showAgentActivity ||
702
- flags.showAgentId ||
703
- flags.showLoad ||
704
- flags.showUptime ||
705
- (flags.showMcp);
728
+ const hasAnyEnabled = hasAnyLegacyPillFlagEnabled(flags) || flags.showMcp;
706
729
 
707
730
  // If no items are selected, fallback to CPU & RAM without mutating saved settings
708
731
  const effectiveShowCpuRam = flags.showCpuRam || !hasAnyEnabled;
@@ -789,6 +812,9 @@ function PillView({ isOpen, open, workspaceId, agentId }: RenderPillProps<ModalT
789
812
  if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.turns)) {
790
813
  items.push("turns");
791
814
  }
815
+ if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.velocity)) {
816
+ items.push("velocity");
817
+ }
792
818
 
793
819
  // Legacy render fallback only. On the supported 0.8 host the pill is
794
820
  // host-rendered from `resolveLabel`, which advances the cycle on the shared
@@ -844,12 +870,11 @@ interface ResourceModalProps extends RenderModalProps<ModalTab> {
844
870
  initialTab?: ModalTab;
845
871
  }
846
872
 
847
- function MetricSurfaceMatrix({
873
+ function MetricPillSelector({
848
874
  settings,
849
875
  updateSettings,
850
876
  notifySettingsChanged,
851
877
  mcpInstalled,
852
- mcpRunning,
853
878
  customPills,
854
879
  customOverrides,
855
880
  onCustomToggle,
@@ -858,141 +883,119 @@ function MetricSurfaceMatrix({
858
883
  updateSettings: (updates: Partial<TopSettings>) => void;
859
884
  notifySettingsChanged: (s: TopSettings) => void;
860
885
  mcpInstalled: boolean;
861
- mcpRunning?: boolean | null;
862
886
  customPills: Array<{ id: string; title: string; sourceFile?: string; enabled: boolean }>;
863
887
  customOverrides: Record<string, boolean> | undefined;
864
888
  onCustomToggle: (id: string, val: boolean) => void;
865
889
  }) {
866
- const { colors, isCompact } = usePluginTheme();
890
+ const { colors } = useHostTheme();
867
891
  const surfaces = settings.metricSurfaces ?? DEFAULT_METRIC_SURFACES;
868
- const provisioned = new Set(settings.provisionedMetrics ?? []);
869
- const matrixRowMinHeight = isCompact ? 44 : 32;
870
- const setTarget = (id: MetricId, target: SurfaceTarget) => {
871
- const next = { ...surfaces, [id]: target };
872
- const s = { ...settings, metricSurfaces: next };
892
+ const masterOn = settings.showCustomPills ?? true;
893
+
894
+ const builtInOptions = METRIC_DEFINITIONS.map((def) => ({
895
+ id: def.id,
896
+ label: def.title,
897
+ icon: def.icon,
898
+ disabled: def.id === "mcp" && !mcpInstalled,
899
+ }));
900
+
901
+ const selectedBuiltIns = METRIC_DEFINITIONS
902
+ .filter((def) => isPillEnabled(surfaces[def.id]))
903
+ .map((def) => def.id);
904
+
905
+ const toggleMetric = (metricId: MetricId) => {
906
+ triggerHaptic("light");
907
+ const currentPill = isPillEnabled(surfaces[metricId]);
908
+ const currentTimeline = isTimelineEnabled(surfaces[metricId]);
909
+ const next = {
910
+ ...surfaces,
911
+ [metricId]: targetFromCheckboxes(!currentPill, currentTimeline),
912
+ };
913
+ const nextSettings = { ...settings, metricSurfaces: next };
873
914
  updateSettings({ metricSurfaces: next });
874
- notifySettingsChanged(s);
915
+ notifySettingsChanged(nextSettings);
875
916
  };
917
+
918
+ const selectAll = () => {
919
+ triggerHaptic("medium");
920
+ const next = { ...surfaces };
921
+ for (const def of METRIC_DEFINITIONS) {
922
+ if (def.id === "mcp" && !mcpInstalled) continue;
923
+ const currentTimeline = isTimelineEnabled(surfaces[def.id]);
924
+ next[def.id] = targetFromCheckboxes(true, currentTimeline);
925
+ }
926
+ const nextSettings = { ...settings, metricSurfaces: next };
927
+ updateSettings({ metricSurfaces: next });
928
+ notifySettingsChanged(nextSettings);
929
+ };
930
+
931
+ const clearAll = () => {
932
+ triggerHaptic("medium");
933
+ const next = { ...surfaces };
934
+ for (const def of METRIC_DEFINITIONS) {
935
+ const currentTimeline = isTimelineEnabled(surfaces[def.id]);
936
+ next[def.id] = targetFromCheckboxes(false, currentTimeline);
937
+ }
938
+ const nextSettings = { ...settings, metricSurfaces: next };
939
+ updateSettings({ metricSurfaces: next });
940
+ notifySettingsChanged(nextSettings);
941
+ };
942
+
876
943
  return (
877
- <View style={[styles.metricMatrix, isCompact && styles.metricMatrixCompact]}>
878
- <View style={[styles.metricMatrixHeader, isCompact && styles.metricMatrixHeaderCompact]}>
879
- <Text style={[styles.metricMatrixHeaderText, { color: colors.foregroundMuted }]}>
880
- Metric
881
- </Text>
882
- <View style={[styles.metricMatrixTargetHeader, isCompact && styles.metricMatrixTargetHeaderCompact]}>
883
- <Text style={[styles.metricMatrixHeaderText, { color: colors.foregroundMuted }]}>
884
- Pill
885
- </Text>
886
- <Text style={[styles.metricMatrixHeaderText, { color: colors.foregroundMuted }]}>
887
- Timeline
944
+ <HostStack gap={10}>
945
+ <MultiChoiceChips
946
+ options={builtInOptions}
947
+ selected={selectedBuiltIns}
948
+ onToggle={toggleMetric}
949
+ onSelectAll={selectAll}
950
+ onClearAll={clearAll}
951
+ />
952
+ {customPills.length > 0 && (
953
+ <HostStack gap={6}>
954
+ <Text style={{ fontSize: 10, fontWeight: "600", color: colors.foregroundMuted }}>
955
+ Custom Metric Pills
888
956
  </Text>
889
- </View>
890
- </View>
891
- {METRIC_DEFINITIONS.map((def, index) => {
892
- const boxes = checkboxesFromTarget(surfaces[def.id]);
893
- const unprovisioned =
894
- isProviderDependent(def.id) && !provisioned.has(def.id);
895
- const timelineDisabled = !!def.pillOnly;
896
- const disabled = def.id === "mcp" && !mcpInstalled;
897
- const note =
898
- def.pillOnly
899
- ? "live only"
900
- : def.id === "mcp" && !mcpInstalled
901
- ? "mcp-tools not installed"
902
- : def.id === "mcp" && mcpRunning === false
903
- ? "mcp-tools disabled"
904
- : unprovisioned
905
- ? "waiting for provider"
906
- : null;
907
- const setBox = (which: "pill" | "timeline", val: boolean) => {
908
- const nextBoxes = { ...boxes, [which]: val };
909
- if (def.pillOnly) nextBoxes.timeline = false;
910
- setTarget(
911
- def.id,
912
- targetFromCheckboxes(nextBoxes.pill, nextBoxes.timeline),
913
- );
914
- };
915
- return (
916
- <View
917
- key={def.id}
918
- style={[
919
- styles.metricMatrixRow,
920
- isCompact && styles.metricMatrixRowCompact,
921
- { minHeight: matrixRowMinHeight },
922
- index < METRIC_DEFINITIONS.length - 1
923
- ? {
924
- borderBottomWidth: 1,
925
- borderBottomColor: colors.border,
926
- }
927
- : undefined,
928
- ]}
929
- >
930
- <View style={styles.metricMatrixLabel}>
931
- <Text style={[styles.metricMatrixTitle, { color: colors.foreground }]}>
932
- {def.title}
933
- </Text>
934
- {note ? (
935
- <Text style={[styles.metricMatrixNote, { color: colors.foregroundMuted }]}>
936
- {note}
937
- </Text>
938
- ) : null}
939
- </View>
940
- <View style={[styles.metricMatrixTargets, isCompact && styles.metricMatrixTargetsCompact]}>
941
- <Toggle
942
- value={boxes.pill}
943
- disabled={disabled}
944
- onValueChange={(val) => setBox("pill", val)}
945
- style={styles.matrixToggle}
946
- />
947
- <Toggle
948
- value={boxes.timeline && !def.pillOnly}
949
- disabled={disabled || timelineDisabled}
950
- onValueChange={(val) => setBox("timeline", val)}
951
- style={styles.matrixToggle}
952
- />
953
- </View>
954
- </View>
955
- );
956
- })}
957
- {customPills.map((pill) => {
958
- const masterOn = settings.showCustomPills ?? true;
959
- const enabled = customPillEffectiveEnabled(masterOn, customOverrides, pill);
960
- return (
961
- <View
962
- key={`custom-${pill.id}`}
963
- style={styles.metricMatrixRow}
964
- >
965
- <View style={styles.metricMatrixLabel}>
966
- <Text style={[styles.metricMatrixTitle, { color: colors.foreground }]}>
967
- {pill.title}
968
- </Text>
969
- <Text style={[styles.metricMatrixNote, { color: colors.foregroundMuted }]}>
970
- custom pill
971
- </Text>
972
- </View>
973
- <View style={[styles.metricMatrixTargets, isCompact && styles.metricMatrixTargetsCompact]}>
974
- <Toggle
975
- value={enabled}
976
- disabled={!masterOn}
977
- onValueChange={(val) => onCustomToggle(pill.id, val)}
978
- style={styles.matrixToggle}
979
- />
980
- <Toggle
981
- value={false}
982
- disabled={true}
983
- onValueChange={() => {}}
984
- style={styles.matrixToggle}
985
- />
986
- </View>
987
- </View>
988
- );
989
- })}
990
- </View>
957
+ <MultiChoiceChips
958
+ options={customPills.map((pill) => ({
959
+ id: pill.id,
960
+ label: pill.title,
961
+ icon: "Sliders",
962
+ disabled: !masterOn,
963
+ }))}
964
+ selected={customPills
965
+ .filter((pill) => customPillEffectiveEnabled(masterOn, customOverrides, pill))
966
+ .map((pill) => pill.id)}
967
+ onToggle={(pillId) => {
968
+ triggerHaptic("light");
969
+ const pill = customPills.find((p) => p.id === pillId);
970
+ if (!pill) return;
971
+ const current = customPillEffectiveEnabled(masterOn, customOverrides, pill);
972
+ onCustomToggle(pillId, !current);
973
+ }}
974
+ />
975
+ </HostStack>
976
+ )}
977
+ </HostStack>
991
978
  );
992
979
  }
993
980
 
994
- function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: ResourceModalProps) {
995
- const { colors, padding } = usePluginTheme();
981
+ function ResourceModal(props: ResourceModalProps) {
982
+ return (
983
+ <HostThemeProvider theme={props.theme}>
984
+ <HostLayoutProvider layout={props.layout}>
985
+ <ResourceModalBody {...props} />
986
+ </HostLayoutProvider>
987
+ </HostThemeProvider>
988
+ );
989
+ }
990
+
991
+ function ResourceModalBody({ workspaceId, agentId, initialTab, payload, theme, layout }: ResourceModalProps) {
992
+ const { colors } = useHostTheme();
993
+ const { compact } = useHostLayout();
994
+ // Host-density padding rhythm (the helper's "comfortable" scale): the host
995
+ // owns density now, so the modal keeps the same numeric values locally.
996
+ const padding = compact
997
+ ? { horizontal: 12, vertical: 10, gap: 8 }
998
+ : { horizontal: 16, vertical: 14, gap: 12 };
996
999
  // Settings re-verify on mount/focus via hook defaults; the settings tab
997
1000
  // refetches explicitly below. No background poll: the modal only lives
998
1001
  // while open. Custom-pill definitions are a separate data source and keep
@@ -1068,18 +1071,7 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1068
1071
  });
1069
1072
  }, [agentId, data, agent, workspace?.directory]);
1070
1073
 
1071
- const hasAnyPillEnabled =
1072
- flags.showCpuRam ||
1073
- flags.showBranch ||
1074
- flags.showWorktree ||
1075
- flags.showAgentTitle ||
1076
- flags.showAgent ||
1077
- flags.showAgentProvider ||
1078
- flags.showAgentActivity ||
1079
- flags.showAgentId ||
1080
- flags.showLoad ||
1081
- flags.showUptime ||
1082
- isMcpEnabled;
1074
+ const hasAnyPillEnabled = hasAnyLegacyPillFlagEnabled(flags) || isMcpEnabled;
1083
1075
 
1084
1076
  const handleRefresh = () => {
1085
1077
  triggerHaptic("light");
@@ -1093,8 +1085,8 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1093
1085
  };
1094
1086
 
1095
1087
  const navbar = (
1096
- <Tabs
1097
- tabs={TABS}
1088
+ <HostTabs
1089
+ tabs={[...MODAL_TABS]}
1098
1090
  activeTab={activeTab}
1099
1091
  onTabChange={handleTabChange}
1100
1092
  />
@@ -1104,235 +1096,251 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1104
1096
  paddingHorizontal: padding.horizontal,
1105
1097
  paddingTop: padding.vertical,
1106
1098
  paddingBottom: Math.round(padding.gap / 2),
1107
- };
1099
+ position: "sticky" as const,
1100
+ top: 0,
1101
+ zIndex: 10,
1102
+ } as any;
1108
1103
 
1109
1104
  if (isError && !data) {
1110
1105
  return (
1111
- <ModalBody
1112
- style={{ backgroundColor: colors.surface0 }}
1113
- headerMode="pinned"
1114
- scrollMode="always"
1115
- refreshing={isRefetching}
1116
- onRefresh={handleRefresh}
1117
- header={navbar}
1118
- headerStyle={navbarStyle}
1119
- contentContainerStyle={[{ paddingTop: Math.round(padding.gap / 2) }]}
1120
- >
1121
- <Card variant="elevated">
1122
- <View style={styles.errorBox}>
1123
- <Icon name="Ghost" size={24} color={colors.statusDanger} />
1124
- <Text style={[styles.errorText, { color: colors.statusDanger }]}>
1125
- {error instanceof Error ? error.message : "Failed to load metrics"}
1126
- </Text>
1106
+ <HostModalSection>
1107
+ <View style={styles.modalColumn}>
1108
+ <View style={navbarStyle}>{navbar}</View>
1109
+ <View style={styles.modalContent}>
1110
+ <HostCard variant="elevated">
1111
+ <View style={styles.errorBox}>
1112
+ <Icon name="Ghost" size={24} color={colors.statusDanger} />
1113
+ <Text style={[styles.errorText, { color: colors.statusDanger }]}>
1114
+ {error instanceof Error ? error.message : "Failed to load metrics"}
1115
+ </Text>
1116
+ </View>
1117
+ </HostCard>
1127
1118
  </View>
1128
- </Card>
1129
- </ModalBody>
1119
+ </View>
1120
+ </HostModalSection>
1130
1121
  );
1131
1122
  }
1132
1123
 
1133
1124
  const { cpuColor, memColor } = getMetricColors(data, colors);
1134
1125
 
1135
1126
  return (
1136
- <ModalBody
1137
- style={{ backgroundColor: colors.surface0 }}
1138
- headerMode="pinned"
1139
- scrollMode="always"
1140
- refreshing={isLoading || isRefetching}
1141
- onRefresh={handleRefresh}
1142
- header={navbar}
1143
- headerStyle={navbarStyle}
1144
- contentContainerStyle={[
1145
- { paddingTop: Math.round(padding.gap / 2) },
1146
- ]}
1147
- >
1148
- {activeTab === "system" && (
1149
- <>
1150
- {/* Dual Metric Gauges Hero */}
1151
- <Card variant="elevated">
1152
- <View style={styles.gaugeContainer}>
1153
- <MetricGauge
1154
- value={data?.cpuUsagePercent ?? 0}
1155
- thresholds={CPU_THRESHOLDS}
1156
- label="CPU Load"
1157
- size={72}
1158
- />
1159
- <MetricGauge
1160
- value={data?.memoryUsedPercent ?? 0}
1161
- thresholds={MEM_THRESHOLDS}
1162
- label="RAM Used"
1163
- size={72}
1164
- />
1165
- </View>
1166
- </Card>
1167
-
1168
- {/* Host Meta Card */}
1169
- <Card variant="elevated">
1170
- <KeyValueGroup columns={1}>
1171
- <CompactKeyValue label="Host" value={data?.hostname ?? "Unknown"} copyable />
1172
- <CompactKeyValue
1173
- label="Uptime"
1174
- value={data?.uptimeSeconds ? formatUptime(data?.uptimeSeconds) : "--"}
1175
- />
1176
- </KeyValueGroup>
1177
- <CompactKeyValue
1178
- label="Processor"
1179
- value={data?.cpuModel ?? "--"}
1180
- subValue={data?.cpuCores ? `(${data?.cpuCores} cores)` : undefined}
1181
- />
1182
- </Card>
1183
-
1184
- {/* CPU Utilization Card */}
1185
- <Card variant="elevated">
1186
- <CompactCardHeader
1187
- title="CPU Details"
1188
- value={
1189
- <Text style={[styles.metricHighlight, { color: cpuColor }]}>
1190
- {data?.cpuUsagePercent ?? 0}%
1191
- </Text>
1192
- }
1193
- />
1194
-
1195
- <ProgressBar
1196
- value={data?.cpuUsagePercent ?? 0}
1197
- thresholds={CPU_THRESHOLDS}
1198
- height={8}
1199
- />
1127
+ <HostModalSection>
1128
+ <View style={styles.modalColumn}>
1129
+ <View style={navbarStyle}>{navbar}</View>
1130
+ <View style={styles.modalContent}>
1131
+ {activeTab === "system" && (
1132
+ <>
1133
+ {/* Dual Metric Gauges Hero */}
1134
+ <HostCard variant="elevated">
1135
+ <HostRow justify="between" align="center" style={styles.gaugeContainer}>
1136
+ <HostMetricGauge
1137
+ value={data?.cpuUsagePercent ?? 0}
1138
+ thresholds={CPU_THRESHOLDS}
1139
+ label="CPU Load"
1140
+ size={72}
1141
+ />
1142
+ <HostMetricGauge
1143
+ value={data?.memoryUsedPercent ?? 0}
1144
+ thresholds={MEM_THRESHOLDS}
1145
+ label="RAM Used"
1146
+ size={72}
1147
+ />
1148
+ </HostRow>
1149
+ </HostCard>
1150
+
1151
+ {/* Host Meta Card */}
1152
+ <HostCard variant="elevated">
1153
+ <HostStack gap={12}>
1154
+ <HostKeyValue
1155
+ label="Host"
1156
+ value={data?.hostname ?? "Unknown"}
1157
+ copyable
1158
+ {...compactKvProps}
1159
+ />
1160
+ <HostKeyValue
1161
+ label="Uptime"
1162
+ value={data?.uptimeSeconds ? formatUptime(data?.uptimeSeconds) : "--"}
1163
+ {...compactKvProps}
1164
+ />
1165
+ </HostStack>
1166
+ <HostKeyValue
1167
+ label="Processor"
1168
+ value={data?.cpuModel ?? "--"}
1169
+ subValue={data?.cpuCores ? `(${data?.cpuCores} cores)` : undefined}
1170
+ {...compactKvProps}
1171
+ />
1172
+ </HostCard>
1173
+
1174
+ {/* CPU Utilization Card */}
1175
+ <HostCard variant="elevated">
1176
+ <HostCardHeader
1177
+ title="CPU Details"
1178
+ {...compactCardTitleProp}
1179
+ value={
1180
+ <Text style={[styles.metricHighlight, { color: cpuColor }]}>
1181
+ {data?.cpuUsagePercent ?? 0}%
1182
+ </Text>
1183
+ }
1184
+ />
1200
1185
 
1201
- <CompactKeyValue
1202
- label="Load Average (1m, 5m, 15m)"
1203
- value={data?.loadAvg ? data?.loadAvg.map((n) => n.toFixed(2)).join(" ") : "-- -- --"}
1204
- mono
1205
- />
1206
- </Card>
1186
+ <HostProgressBar
1187
+ value={data?.cpuUsagePercent ?? 0}
1188
+ thresholds={CPU_THRESHOLDS}
1189
+ height={8}
1190
+ />
1207
1191
 
1208
- {/* Memory Card */}
1209
- <Card variant="elevated">
1210
- <CompactCardHeader
1211
- title="Memory Details"
1212
- value={
1213
- <Text style={[styles.metricHighlight, { color: memColor }]}>
1214
- {data?.memoryUsedPercent ?? 0}%
1215
- </Text>
1216
- }
1217
- />
1192
+ <HostKeyValue
1193
+ label="Load Average (1m, 5m, 15m)"
1194
+ value={data?.loadAvg ? data?.loadAvg.map((n) => n.toFixed(2)).join(" ") : "-- -- --"}
1195
+ mono
1196
+ {...compactKvProps}
1197
+ />
1198
+ </HostCard>
1199
+
1200
+ {/* Memory Card */}
1201
+ <HostCard variant="elevated">
1202
+ <HostCardHeader
1203
+ title="Memory Details"
1204
+ {...compactCardTitleProp}
1205
+ value={
1206
+ <Text style={[styles.metricHighlight, { color: memColor }]}>
1207
+ {data?.memoryUsedPercent ?? 0}%
1208
+ </Text>
1209
+ }
1210
+ />
1218
1211
 
1219
- <ProgressBar
1220
- value={data?.memoryUsedPercent ?? 0}
1221
- thresholds={MEM_THRESHOLDS}
1222
- height={8}
1223
- />
1212
+ <HostProgressBar
1213
+ value={data?.memoryUsedPercent ?? 0}
1214
+ thresholds={MEM_THRESHOLDS}
1215
+ height={8}
1216
+ />
1224
1217
 
1225
- <CompactKeyValue
1226
- label="Used / Total"
1227
- value={`${formatBytes(data?.memoryUsedBytes ?? 0)} / ${formatBytes(data?.memoryTotalBytes ?? 0)}`}
1228
- />
1229
- </Card>
1230
-
1231
- {/* MCP Servers Card */}
1232
- {Boolean(data?.mcpInstalled) && (
1233
- <Card variant="elevated">
1234
- <CompactCardHeader
1235
- title="MCP Servers"
1236
- subtitle="Source: paseo-mcp-tools"
1237
- icon="Server"
1238
- value={
1239
- data?.mcp ? (
1240
- <CompactBadge
1241
- label={data?.mcp.isStale ? "Stale Snapshot" : "Live"}
1242
- variant={data?.mcp.isStale ? "warning" : "success"}
1243
- dot
1218
+ <HostKeyValue
1219
+ label="Used / Total"
1220
+ value={`${formatBytes(data?.memoryUsedBytes ?? 0)} / ${formatBytes(data?.memoryTotalBytes ?? 0)}`}
1221
+ {...compactKvProps}
1222
+ />
1223
+ </HostCard>
1224
+
1225
+ {/* MCP Servers Card */}
1226
+ {Boolean(data?.mcpInstalled) && (
1227
+ <HostCard variant="elevated">
1228
+ <HostCardHeader
1229
+ title="MCP Servers"
1230
+ subtitle="Source: paseo-mcp-tools"
1231
+ icon="Server"
1232
+ {...compactCardTitleProp}
1233
+ value={
1234
+ data?.mcp ? (
1235
+ <HostBadge
1236
+ label={data?.mcp.isStale ? "Stale Snapshot" : "Live"}
1237
+ variant={data?.mcp.isStale ? "warning" : "success"}
1238
+ dot
1239
+ {...compactBadgeProp}
1240
+ />
1241
+ ) : (
1242
+ <HostBadge
1243
+ label="No Data"
1244
+ variant="neutral"
1245
+ {...compactBadgeProp}
1246
+ />
1247
+ )
1248
+ }
1244
1249
  />
1245
- ) : (
1246
- <CompactBadge label="No Data" variant="neutral" />
1247
- )
1248
- }
1249
- />
1250
1250
 
1251
- {data?.mcp ? (
1252
- <>
1253
- <KeyValueGroup columns={1}>
1254
- <CompactKeyValue
1255
- label="Health"
1256
- value={`${data?.mcp.healthy} healthy / ${data?.mcp.total} total`}
1257
- />
1258
- <CompactKeyValue
1259
- label="Snapshot Updated"
1260
- value={formatTimeAgo(data?.mcp.updatedAt)}
1261
- />
1262
- <CompactKeyValue
1263
- label="Data Provider"
1264
- value="paseo-mcp-tools"
1265
- />
1266
- </KeyValueGroup>
1267
-
1268
- {data?.mcp.servers && data?.mcp.servers.length > 0 ? (
1269
- <View style={styles.mcpList}>
1270
- {data?.mcp.servers.map((srv) => {
1271
- const badgeVariant: "success" | "warning" | "danger" | "neutral" =
1272
- srv.status === "healthy"
1273
- ? "success"
1274
- : srv.status === "degraded"
1275
- ? "warning"
1276
- : srv.status === "down"
1277
- ? "danger"
1278
- : "neutral";
1279
- return (
1280
- <View key={srv.name} style={styles.mcpRow}>
1281
- <View style={styles.mcpInfo}>
1282
- <Text
1283
- numberOfLines={1}
1284
- style={[styles.mcpName, { color: colors.foreground }]}
1285
- >
1286
- {srv.name}
1287
- </Text>
1288
- <Text style={[styles.mcpLatency, { color: colors.foregroundMuted }]}>
1289
- {srv.latencyMs >= 0 ? `${srv.latencyMs}ms` : "timeout"}
1290
- </Text>
1291
- </View>
1292
- <CompactBadge label={srv.status} variant={badgeVariant} />
1251
+ {data?.mcp ? (
1252
+ <>
1253
+ <HostStack gap={12}>
1254
+ <HostKeyValue
1255
+ label="Health"
1256
+ value={`${data?.mcp.healthy} healthy / ${data?.mcp.total} total`}
1257
+ {...compactKvProps}
1258
+ />
1259
+ <HostKeyValue
1260
+ label="Snapshot Updated"
1261
+ value={formatTimeAgo(data?.mcp.updatedAt)}
1262
+ {...compactKvProps}
1263
+ />
1264
+ <HostKeyValue
1265
+ label="Data Provider"
1266
+ value="paseo-mcp-tools"
1267
+ {...compactKvProps}
1268
+ />
1269
+ </HostStack>
1270
+
1271
+ {data?.mcp.servers && data?.mcp.servers.length > 0 ? (
1272
+ <View style={styles.mcpList}>
1273
+ {data?.mcp.servers.map((srv) => {
1274
+ const badgeVariant: "success" | "warning" | "danger" | "neutral" =
1275
+ srv.status === "healthy"
1276
+ ? "success"
1277
+ : srv.status === "degraded"
1278
+ ? "warning"
1279
+ : srv.status === "down"
1280
+ ? "danger"
1281
+ : "neutral";
1282
+ return (
1283
+ <HostRow
1284
+ key={srv.name}
1285
+ justify="between"
1286
+ align="center"
1287
+ style={styles.mcpRow}
1288
+ >
1289
+ <HostRow gap={8} align="center" style={styles.mcpInfo}>
1290
+ <Text
1291
+ numberOfLines={1}
1292
+ style={[styles.mcpName, { color: colors.foreground }]}
1293
+ >
1294
+ {srv.name}
1295
+ </Text>
1296
+ <Text style={[styles.mcpLatency, { color: colors.foregroundMuted }]}>
1297
+ {srv.latencyMs >= 0 ? `${srv.latencyMs}ms` : "timeout"}
1298
+ </Text>
1299
+ </HostRow>
1300
+ <HostBadge {...compactBadgeProp} label={srv.status} variant={badgeVariant} />
1301
+ </HostRow>
1302
+ );
1303
+ })}
1293
1304
  </View>
1294
- );
1295
- })}
1296
- </View>
1297
- ) : (
1298
- <Text style={[styles.mcpEmpty, { color: colors.foregroundMuted }]}>
1299
- No MCP servers configured
1300
- </Text>
1301
- )}
1302
- </>
1303
- ) : (
1304
- <Text style={[styles.mcpEmpty, { color: colors.foregroundMuted }]}>
1305
- Waiting for status snapshot from paseo-mcp-tools...
1306
- </Text>
1307
- )}
1308
- </Card>
1309
- )}
1305
+ ) : (
1306
+ <Text style={[styles.mcpEmpty, { color: colors.foregroundMuted }]}>
1307
+ No MCP servers configured
1308
+ </Text>
1309
+ )}
1310
+ </>
1311
+ ) : (
1312
+ <Text style={[styles.mcpEmpty, { color: colors.foregroundMuted }]}>
1313
+ Waiting for status snapshot from paseo-mcp-tools...
1314
+ </Text>
1315
+ )}
1316
+ </HostCard>
1317
+ )}
1310
1318
 
1311
1319
  {/* Custom Metric Pills Card */}
1312
1320
  {Boolean(data?.customPills && data?.customPills.length > 0) && (
1313
- <Card variant="elevated">
1314
- <CompactCardHeader
1321
+ <HostCard variant="elevated">
1322
+ <HostCardHeader {...compactCardTitleProp}
1315
1323
  title="Custom Metric Pills"
1316
1324
  subtitle="Discovered from ~/.paseo/top/pills"
1317
1325
  icon="Sliders"
1318
1326
  value={
1319
- <CompactBadge
1327
+ <HostBadge {...compactBadgeProp}
1320
1328
  label={`${data?.customPills?.length ?? 0} active`}
1321
1329
  variant="accent"
1322
1330
  />
1323
1331
  }
1324
1332
  />
1325
- <KeyValueGroup columns={1}>
1333
+ <HostStack gap={12}>
1326
1334
  {(data?.customPills ?? []).map((cp) => (
1327
- <CompactKeyValue
1335
+ <HostKeyValue {...compactKvProps}
1328
1336
  key={cp.id}
1329
1337
  label={cp.title}
1330
1338
  value={cp.displayValue}
1331
1339
  subValue={cp.status !== "neutral" ? `(${cp.status})` : undefined}
1332
1340
  />
1333
1341
  ))}
1334
- </KeyValueGroup>
1335
- </Card>
1342
+ </HostStack>
1343
+ </HostCard>
1336
1344
  )}
1337
1345
  </>
1338
1346
  )}
@@ -1340,48 +1348,48 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1340
1348
  {activeTab === "context" && (
1341
1349
  <>
1342
1350
  {/* Workspace Information */}
1343
- <Card variant="elevated">
1344
- <CompactCardHeader
1351
+ <HostCard variant="elevated">
1352
+ <HostCardHeader {...compactCardTitleProp}
1345
1353
  title="Workspace & Git"
1346
1354
  icon="GitBranch"
1347
1355
  value={
1348
1356
  workspace?.status ? (
1349
- <CompactBadge label={workspace.status} variant="info" />
1357
+ <HostBadge {...compactBadgeProp} label={workspace.status} variant="info" />
1350
1358
  ) : undefined
1351
1359
  }
1352
1360
  />
1353
- <KeyValueGroup columns={1}>
1354
- <CompactKeyValue label="Git Branch" value={data?.branch || "Unknown"} />
1355
- <CompactKeyValue label="Kind" value={workspace?.kind || "Unknown"} />
1356
- </KeyValueGroup>
1361
+ <HostStack gap={12}>
1362
+ <HostKeyValue {...compactKvProps} label="Git Branch" value={data?.branch || "Unknown"} />
1363
+ <HostKeyValue {...compactKvProps} label="Kind" value={workspace?.kind || "Unknown"} />
1364
+ </HostStack>
1357
1365
  {workspace?.directory ? (
1358
- <CompactKeyValue label="Worktree Location" value={workspace.directory} copyable mono />
1366
+ <HostKeyValue {...compactKvProps} label="Worktree Location" value={workspace.directory} copyable mono />
1359
1367
  ) : null}
1360
1368
  {workspace?.name ? (
1361
- <CompactKeyValue label="Workspace Name" value={workspace.name} />
1369
+ <HostKeyValue {...compactKvProps} label="Workspace Name" value={workspace.name} />
1362
1370
  ) : null}
1363
1371
  {workspace?.title && workspace.title !== workspace.name ? (
1364
- <CompactKeyValue label="Workspace Title" value={workspace.title} />
1372
+ <HostKeyValue {...compactKvProps} label="Workspace Title" value={workspace.title} />
1365
1373
  ) : null}
1366
1374
  {workspace?.projectDisplayName ? (
1367
- <CompactKeyValue label="Project" value={workspace.projectDisplayName} />
1375
+ <HostKeyValue {...compactKvProps} label="Project" value={workspace.projectDisplayName} />
1368
1376
  ) : null}
1369
1377
  {workspace?.diffStat ? (
1370
- <CompactKeyValue
1378
+ <HostKeyValue {...compactKvProps}
1371
1379
  label="Git Changes"
1372
1380
  value={`+${workspace.diffStat.additions} -${workspace.diffStat.deletions}`}
1373
1381
  />
1374
1382
  ) : null}
1375
- </Card>
1383
+ </HostCard>
1376
1384
 
1377
1385
  {/* Agent Information */}
1378
- <Card variant="elevated">
1379
- <CompactCardHeader
1386
+ <HostCard variant="elevated">
1387
+ <HostCardHeader {...compactCardTitleProp}
1380
1388
  title={agent?.title ? `Agent: ${agent.title}` : "Agent Session"}
1381
1389
  icon="Bot"
1382
1390
  value={
1383
1391
  agent?.status ? (
1384
- <CompactBadge
1392
+ <HostBadge {...compactBadgeProp}
1385
1393
  label={agent.status}
1386
1394
  variant={agent.status === "running" ? "success" : "info"}
1387
1395
  />
@@ -1389,17 +1397,17 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1389
1397
  }
1390
1398
  />
1391
1399
  {agent?.title ? (
1392
- <CompactKeyValue label="Agent Tab" value={agent.title} />
1400
+ <HostKeyValue {...compactKvProps} label="Agent Tab" value={agent.title} />
1393
1401
  ) : null}
1394
1402
  {agentId ? (
1395
- <CompactKeyValue label="Agent ID" value={agentId} copyable mono />
1403
+ <HostKeyValue {...compactKvProps} label="Agent ID" value={agentId} copyable mono />
1396
1404
  ) : null}
1397
- <KeyValueGroup columns={1}>
1398
- <CompactKeyValue label="Model" value={agent?.model || "Standard"} />
1399
- <CompactKeyValue label="Provider" value={agent?.provider || "Default"} />
1400
- </KeyValueGroup>
1401
- <KeyValueGroup columns={1}>
1402
- <CompactKeyValue
1405
+ <HostStack gap={12}>
1406
+ <HostKeyValue {...compactKvProps} label="Model" value={agent?.model || "Standard"} />
1407
+ <HostKeyValue {...compactKvProps} label="Provider" value={agent?.provider || "Default"} />
1408
+ </HostStack>
1409
+ <HostStack gap={12}>
1410
+ <HostKeyValue {...compactKvProps}
1403
1411
  label="Last Worked"
1404
1412
  value={
1405
1413
  agent?.status === "running"
@@ -1407,7 +1415,7 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1407
1415
  : formatTimeAgo(agent?.lastActivityAt)
1408
1416
  }
1409
1417
  />
1410
- <CompactKeyValue
1418
+ <HostKeyValue {...compactKvProps}
1411
1419
  label="Inactivity"
1412
1420
  value={
1413
1421
  agent?.status === "running"
@@ -1415,25 +1423,25 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1415
1423
  : formatIdleDuration(agent?.lastActivityAt)
1416
1424
  }
1417
1425
  />
1418
- </KeyValueGroup>
1426
+ </HostStack>
1419
1427
  {agent?.cwd ? (
1420
- <CompactKeyValue label="Working Directory" value={agent.cwd} copyable mono />
1428
+ <HostKeyValue {...compactKvProps} label="Working Directory" value={agent.cwd} copyable mono />
1421
1429
  ) : null}
1422
- </Card>
1430
+ </HostCard>
1423
1431
 
1424
1432
  {/* Token Usage & Context Window */}
1425
- <Card variant="elevated">
1426
- <CompactCardHeader
1433
+ <HostCard variant="elevated">
1434
+ <HostCardHeader {...compactCardTitleProp}
1427
1435
  title="Tokens & Context Window"
1428
1436
  icon="Coins"
1429
1437
  value={
1430
1438
  contextPercent != null ? (
1431
- <CompactBadge
1439
+ <HostBadge {...compactBadgeProp}
1432
1440
  label={`${contextPercent}% ctx`}
1433
1441
  variant={contextPercent >= 85 ? "danger" : contextPercent >= 70 ? "warning" : "success"}
1434
1442
  />
1435
1443
  ) : tokenMetrics?.totalTokens != null ? (
1436
- <CompactBadge
1444
+ <HostBadge {...compactBadgeProp}
1437
1445
  label={`${formatCompactTokens(tokenMetrics.totalTokens)} tok`}
1438
1446
  variant="neutral"
1439
1447
  />
@@ -1443,64 +1451,64 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1443
1451
 
1444
1452
  {tokenMetrics?.contextMaxTokens != null && tokenMetrics.contextMaxTokens > 0 ? (
1445
1453
  <View style={styles.contextUsageBlock}>
1446
- <View style={styles.contextUsageRow}>
1454
+ <HostRow justify="between" align="center" style={styles.contextUsageRow}>
1447
1455
  <Text style={[styles.compactKvLabel, { color: colors.foregroundMuted }]}>Context Utilization</Text>
1448
1456
  <Text style={[styles.compactKvValue, { color: colors.foreground, fontWeight: "600" }]}>
1449
1457
  {formatCompactTokens(tokenMetrics.contextUsedTokens ?? 0)} / {formatCompactTokens(tokenMetrics.contextMaxTokens)} ({contextPercent}%)
1450
1458
  </Text>
1451
- </View>
1452
- <ProgressBar
1459
+ </HostRow>
1460
+ <HostProgressBar
1453
1461
  value={contextPercent ?? 0}
1454
1462
  thresholds={{ warning: 70, danger: 85 }}
1455
1463
  height={8}
1456
1464
  />
1457
1465
  </View>
1458
1466
  ) : tokenMetrics?.contextUsedTokens != null ? (
1459
- <CompactKeyValue
1467
+ <HostKeyValue {...compactKvProps}
1460
1468
  label="Context Used"
1461
- value={`${tokenMetrics.contextUsedTokens.toLocaleString()} (${formatCompactTokens(tokenMetrics.contextUsedTokens)})`}
1469
+ value={`${tokenMetrics.contextUsedTokens.toLocaleString('en-US')} (${formatCompactTokens(tokenMetrics.contextUsedTokens)})`}
1462
1470
  />
1463
1471
  ) : null}
1464
1472
 
1465
- <KeyValueGroup columns={1}>
1466
- <CompactKeyValue
1473
+ <HostStack gap={12}>
1474
+ <HostKeyValue {...compactKvProps}
1467
1475
  label="Input Tokens"
1468
- value={tokenMetrics?.inputTokens != null ? tokenMetrics.inputTokens.toLocaleString() : "--"}
1476
+ value={tokenMetrics?.inputTokens != null ? tokenMetrics.inputTokens.toLocaleString('en-US') : "--"}
1469
1477
  subValue={tokenMetrics?.inputTokens != null ? formatCompactTokens(tokenMetrics.inputTokens) : undefined}
1470
1478
  />
1471
- <CompactKeyValue
1479
+ <HostKeyValue {...compactKvProps}
1472
1480
  label="Output Tokens"
1473
- value={tokenMetrics?.outputTokens != null ? tokenMetrics.outputTokens.toLocaleString() : "--"}
1481
+ value={tokenMetrics?.outputTokens != null ? tokenMetrics.outputTokens.toLocaleString('en-US') : "--"}
1474
1482
  subValue={tokenMetrics?.outputTokens != null ? formatCompactTokens(tokenMetrics.outputTokens) : undefined}
1475
1483
  />
1476
1484
  {tokenMetrics?.cachedTokens != null && (
1477
- <CompactKeyValue
1485
+ <HostKeyValue {...compactKvProps}
1478
1486
  label="Cached Tokens"
1479
- value={tokenMetrics.cachedTokens.toLocaleString()}
1487
+ value={tokenMetrics.cachedTokens.toLocaleString('en-US')}
1480
1488
  subValue={formatCompactTokens(tokenMetrics.cachedTokens)}
1481
1489
  />
1482
1490
  )}
1483
1491
  {tokenMetrics?.costUsd != null && (
1484
- <CompactKeyValue
1492
+ <HostKeyValue {...compactKvProps}
1485
1493
  label="Session / Turn Cost"
1486
1494
  value={`$${tokenMetrics.costUsd < 0.01 ? tokenMetrics.costUsd.toFixed(4) : tokenMetrics.costUsd.toFixed(2)}`}
1487
1495
  />
1488
1496
  )}
1489
- </KeyValueGroup>
1497
+ </HostStack>
1490
1498
  {!tokenMetrics && (
1491
1499
  <Text style={[styles.mcpEmpty, { color: colors.foregroundMuted, marginTop: 4 }]}>
1492
1500
  No token usage recorded for this agent session yet
1493
1501
  </Text>
1494
1502
  )}
1495
- </Card>
1503
+ </HostCard>
1496
1504
  </>
1497
1505
  )}
1498
1506
 
1499
1507
  {activeTab === "settings" && (
1500
1508
  <>
1501
1509
  {/* Pill Display Mode */}
1502
- <Card variant="elevated">
1503
- <CompactCardHeader
1510
+ <HostCard variant="elevated">
1511
+ <HostCardHeader {...compactCardTitleProp}
1504
1512
  title="Pill Display Mode"
1505
1513
  icon="LayoutGrid"
1506
1514
  subtitle="How active items appear in the composer trackbar"
@@ -1520,7 +1528,7 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1520
1528
  }}
1521
1529
  />
1522
1530
  <View style={styles.settingsSpacer}>
1523
- <Toggle
1531
+ <HostToggle
1524
1532
  label="Show Composer Pill"
1525
1533
  description="Hide the composer pill entirely; the dashboard stays available from the sidebar"
1526
1534
  style={styles.toggleFullWidth}
@@ -1532,13 +1540,18 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1532
1540
  }}
1533
1541
  />
1534
1542
  </View>
1535
- </Card>
1543
+ </HostCard>
1536
1544
 
1537
1545
  {/* Active Pill Items Selectors */}
1538
- <Card variant="elevated">
1539
- <CompactCardHeader
1546
+ <HostCard variant="elevated">
1547
+ <HostCardHeader {...compactCardTitleProp}
1540
1548
  title="Active Pill Items"
1541
1549
  icon="Sliders"
1550
+ value={
1551
+ <Text style={[styles.accentValueText, { color: colors.accent }]}>
1552
+ {`${METRIC_DEFINITIONS.filter((d) => isPillEnabled((settings.metricSurfaces ?? DEFAULT_METRIC_SURFACES)[d.id])).length} active`}
1553
+ </Text>
1554
+ }
1542
1555
  subtitle={
1543
1556
  !hasAnyPillEnabled
1544
1557
  ? "None selected -- automatically showing CPU & RAM"
@@ -1550,12 +1563,11 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1550
1563
  }
1551
1564
  />
1552
1565
  <View style={styles.settingsToggles}>
1553
- <MetricSurfaceMatrix
1566
+ <MetricPillSelector
1554
1567
  settings={settings}
1555
1568
  updateSettings={updateSettings}
1556
1569
  notifySettingsChanged={notifySettingsChanged}
1557
1570
  mcpInstalled={Boolean(data?.mcpInstalled)}
1558
- mcpRunning={data?.mcpRunning ?? null}
1559
1571
  customPills={(customPillList?.pills ?? []).map((pill) => ({
1560
1572
  id: pill.id,
1561
1573
  title: pill.title,
@@ -1571,17 +1583,17 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1571
1583
  }}
1572
1584
  />
1573
1585
  </View>
1574
- </Card>
1586
+ </HostCard>
1575
1587
 
1576
1588
  {/* Custom Metric Pills */}
1577
- <Card variant="elevated">
1578
- <CompactCardHeader
1589
+ <HostCard variant="elevated">
1590
+ <HostCardHeader {...compactCardTitleProp}
1579
1591
  title="Custom Metric Pills"
1580
1592
  icon="Sliders"
1581
1593
  subtitle="Standalone pills discovered from ~/.paseo/top/pills"
1582
1594
  />
1583
1595
  <View style={styles.settingsToggles}>
1584
- <Toggle
1596
+ <HostToggle
1585
1597
  label="Show Custom Metric Pills"
1586
1598
  description="Display pills defined in ~/.paseo/top/pills as standalone composer pills"
1587
1599
  style={styles.toggleFullWidth}
@@ -1595,12 +1607,12 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1595
1607
  />
1596
1608
 
1597
1609
  </View>
1598
- </Card>
1610
+ </HostCard>
1599
1611
 
1600
1612
  {/* Rotation Speed Setting (Cycle mode only) */}
1601
1613
  {(settings.pillMode ?? "cycle") === "cycle" && (
1602
- <Card variant="elevated">
1603
- <CompactCardHeader
1614
+ <HostCard variant="elevated">
1615
+ <HostCardHeader {...compactCardTitleProp}
1604
1616
  title="Rotation Speed"
1605
1617
  icon="Clock"
1606
1618
  value={
@@ -1617,12 +1629,12 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1617
1629
  updateSettings({ intervalSeconds });
1618
1630
  }}
1619
1631
  />
1620
- </Card>
1632
+ </HostCard>
1621
1633
  )}
1622
1634
 
1623
1635
  {/* Timeline Cadence Setting */}
1624
- <Card variant="elevated">
1625
- <CompactCardHeader
1636
+ <HostCard variant="elevated">
1637
+ <HostCardHeader {...compactCardTitleProp}
1626
1638
  title="Timeline Cadence"
1627
1639
  icon="Clock"
1628
1640
  value={
@@ -1648,30 +1660,92 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1648
1660
  <Text style={[styles.modeDesc, styles.modeDescSpaced, { color: colors.foregroundMuted }]}>
1649
1661
  0 behaves as never; N above 1 stamps every Nth turn
1650
1662
  </Text>
1651
- </Card>
1663
+ </HostCard>
1664
+
1665
+ {/* Timeline Card Metrics Setting */}
1666
+ {(settings.timelineCadence ?? 1) > 0 && (
1667
+ <HostCard variant="elevated">
1668
+ <HostCardHeader {...compactCardTitleProp}
1669
+ title="Timeline Card Metrics"
1670
+ icon="GitCommitHorizontal"
1671
+ value={
1672
+ <Text style={[styles.accentValueText, { color: colors.accent }]}>
1673
+ {`${METRIC_DEFINITIONS.filter((d) => !d.pillOnly && isTimelineEnabled((settings.metricSurfaces ?? DEFAULT_METRIC_SURFACES)[d.id])).length} active`}
1674
+ </Text>
1675
+ }
1676
+ subtitle="Choose which metrics appear on turn summary cards"
1677
+ />
1678
+ <MultiChoiceChips
1679
+ options={METRIC_DEFINITIONS.filter((d) => !d.pillOnly).map((def) => ({
1680
+ id: def.id,
1681
+ label: def.title,
1682
+ icon: def.icon,
1683
+ disabled: def.id === "mcp" && !data?.mcpInstalled,
1684
+ }))}
1685
+ selected={METRIC_DEFINITIONS.filter((d) => !d.pillOnly && isTimelineEnabled((settings.metricSurfaces ?? DEFAULT_METRIC_SURFACES)[d.id])).map((d) => d.id)}
1686
+ onToggle={(metricId) => {
1687
+ triggerHaptic("light");
1688
+ const surfaces = settings.metricSurfaces ?? DEFAULT_METRIC_SURFACES;
1689
+ const currentPill = isPillEnabled(surfaces[metricId]);
1690
+ const currentTimeline = isTimelineEnabled(surfaces[metricId]);
1691
+ const next = {
1692
+ ...surfaces,
1693
+ [metricId]: targetFromCheckboxes(currentPill, !currentTimeline),
1694
+ };
1695
+ updateSettings({ metricSurfaces: next });
1696
+ notifySettingsChanged({ ...settings, metricSurfaces: next });
1697
+ }}
1698
+ onSelectAll={() => {
1699
+ triggerHaptic("medium");
1700
+ const surfaces = settings.metricSurfaces ?? DEFAULT_METRIC_SURFACES;
1701
+ const next = { ...surfaces };
1702
+ for (const def of METRIC_DEFINITIONS) {
1703
+ if (def.pillOnly) continue;
1704
+ if (def.id === "mcp" && !data?.mcpInstalled) continue;
1705
+ const currentPill = isPillEnabled(surfaces[def.id]);
1706
+ next[def.id] = targetFromCheckboxes(currentPill, true);
1707
+ }
1708
+ updateSettings({ metricSurfaces: next });
1709
+ notifySettingsChanged({ ...settings, metricSurfaces: next });
1710
+ }}
1711
+ onClearAll={() => {
1712
+ triggerHaptic("medium");
1713
+ const surfaces = settings.metricSurfaces ?? DEFAULT_METRIC_SURFACES;
1714
+ const next = { ...surfaces };
1715
+ for (const def of METRIC_DEFINITIONS) {
1716
+ if (def.pillOnly) continue;
1717
+ const currentPill = isPillEnabled(surfaces[def.id]);
1718
+ next[def.id] = targetFromCheckboxes(currentPill, false);
1719
+ }
1720
+ updateSettings({ metricSurfaces: next });
1721
+ notifySettingsChanged({ ...settings, metricSurfaces: next });
1722
+ }}
1723
+ />
1724
+ </HostCard>
1725
+ )}
1652
1726
 
1653
1727
  {/* Default Modal Tab Setting */}
1654
- <Card variant="elevated">
1655
- <CompactCardHeader
1728
+ <HostCard variant="elevated">
1729
+ <HostCardHeader {...compactCardTitleProp}
1656
1730
  title="Default Modal Tab"
1657
1731
  icon="Sliders"
1658
1732
  value={
1659
1733
  <Text style={[styles.accentValueText, { color: colors.accent }]}>
1660
- {TABS.find((t) => t.id === settings.defaultTab)?.label || "System"}
1734
+ {MODAL_TABS.find((t) => t.id === settings.defaultTab)?.label || "System"}
1661
1735
  </Text>
1662
1736
  }
1663
1737
  />
1664
1738
  <ChoiceChips
1665
- options={TABS.map((tab) => ({ id: tab.id as ModalTab, label: tab.label }))}
1739
+ options={MODAL_TABS.map((tab) => ({ id: tab.id as ModalTab, label: tab.label }))}
1666
1740
  value={settings.defaultTab ?? "system"}
1667
1741
  onChange={(defaultTab) => {
1668
1742
  triggerHaptic("light");
1669
1743
  updateSettings({ defaultTab });
1670
1744
  }}
1671
1745
  />
1672
- </Card>
1746
+ </HostCard>
1673
1747
 
1674
- <Button
1748
+ <HostButton
1675
1749
  label="Reset to Defaults"
1676
1750
  variant="secondary"
1677
1751
  size="sm"
@@ -1685,8 +1759,12 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1685
1759
  </>
1686
1760
  )}
1687
1761
 
1762
+ {activeTab === "permissions" && (
1763
+ <PermissionAuditView variant="compact" theme={theme} layout={layout} />
1764
+ )}
1765
+
1688
1766
  {activeTab === "about" && (
1689
- <AboutSection
1767
+ <HostAboutSection
1690
1768
  name="paseo-top"
1691
1769
  description="Live host system and workspace monitor for Paseo composer trackbar."
1692
1770
  version={data?.version ?? PLUGIN_VERSION}
@@ -1694,28 +1772,7 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1694
1772
  repository="https://github.com/xpufx/paseo-top"
1695
1773
  issues="https://github.com/xpufx/paseo-top/issues"
1696
1774
  license="MIT"
1697
- density="tiny"
1698
- extraItems={[
1699
- {
1700
- label: "Host Platform",
1701
- value: data?.platform ? `${data?.platform} (${data?.arch ?? "unknown"})` : "Linux",
1702
- copyable: true,
1703
- },
1704
- { label: "Host Name", value: data?.hostname ?? "localhost", copyable: true },
1705
- { label: "CPU Model", value: data?.cpuModel ?? "unknown", copyable: true },
1706
- {
1707
- label: "CPU Cores",
1708
- value: `${data?.cpuCores ?? 0} cores`,
1709
- },
1710
- {
1711
- label: "Total Memory",
1712
- value: data?.memoryTotalBytes ? formatBytes(data?.memoryTotalBytes) : "unknown",
1713
- },
1714
- {
1715
- label: "Host Uptime",
1716
- value: data?.uptimeSeconds ? formatUptime(data?.uptimeSeconds) : "unknown",
1717
- },
1718
- ]}
1775
+ extraItems={aboutExtraItems(data)}
1719
1776
  />
1720
1777
  )}
1721
1778
 
@@ -1727,28 +1784,80 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1727
1784
  </Text>
1728
1785
  </View>
1729
1786
  )}
1730
- </ModalBody>
1787
+ </View>
1788
+ </View>
1789
+ </HostModalSection>
1731
1790
  );
1732
1791
  }
1733
1792
 
1734
- interface LiveCustomPillViewProps {
1793
+ interface LiveCustomPillModalProps {
1735
1794
  pillId: string;
1736
- initial: CustomPillState;
1795
+ initial: CustomPillStateOutput;
1737
1796
  }
1738
1797
 
1739
- function LiveCustomPillView({ pillId, initial }: LiveCustomPillViewProps) {
1740
- const { data } = useCustomPillsQuery();
1741
- const liveState = data?.pills.find((p) => p.id === pillId) ?? initial;
1742
- return <CustomPillBody state={liveState} />;
1743
- }
1798
+ /**
1799
+ * Drilldown modal body for a custom metric pill, built from `ui/` primitives
1800
+ * (the deprecated `client/` `CustomPillModalContent` was dropped in the #847
1801
+ * migration). The host owns the modal frame; this fills it fluidly.
1802
+ */
1803
+ function CustomPillModalContent({
1804
+ state,
1805
+ onRefresh,
1806
+ isRefreshing = false,
1807
+ }: {
1808
+ state: CustomPillStateOutput;
1809
+ onRefresh?: () => Promise<void> | void;
1810
+ isRefreshing?: boolean;
1811
+ }) {
1812
+ const { colors } = useHostTheme();
1813
+ const displayText =
1814
+ state.modalOutput || state.rawValue || (state.error ? `Error: ${state.error}` : "No output");
1744
1815
 
1745
- interface LiveCustomPillModalProps {
1746
- pillId: string;
1747
- initial: CustomPillStateOutput;
1816
+ return (
1817
+ <View style={styles.customPillModal}>
1818
+ <HostCard>
1819
+ <HostCardHeader
1820
+ title={state.modalTitle ?? state.title}
1821
+ subtitle={state.modalDescription}
1822
+ icon={state.icon}
1823
+ badge={<HostBadge label={state.displayValue} variant={state.status} />}
1824
+ action={
1825
+ onRefresh ? (
1826
+ <HostButton
1827
+ variant="secondary"
1828
+ size="sm"
1829
+ icon="RefreshCw"
1830
+ loading={isRefreshing}
1831
+ label="Refresh"
1832
+ onPress={() => onRefresh()}
1833
+ />
1834
+ ) : undefined
1835
+ }
1836
+ />
1837
+ <View style={styles.codeBlock}>
1838
+ <Text
1839
+ selectable
1840
+ numberOfLines={8}
1841
+ style={[styles.codeText, { color: colors.foreground }]}
1842
+ >
1843
+ {displayText}
1844
+ </Text>
1845
+ <HostCopyButton
1846
+ text={displayText}
1847
+ accessibilityLabel="Copy pill output"
1848
+ toastMessage="pill output"
1849
+ />
1850
+ </View>
1851
+ <Text style={[styles.timestampText, { color: colors.foregroundMuted }]}>
1852
+ Last updated: {new Date(state.lastUpdated).toLocaleTimeString()}
1853
+ </Text>
1854
+ </HostCard>
1855
+ </View>
1856
+ );
1748
1857
  }
1749
1858
 
1750
1859
  function LiveCustomPillModal({ pillId, initial }: LiveCustomPillModalProps) {
1751
- const { colors } = usePluginTheme();
1860
+ const { colors } = useHostTheme();
1752
1861
  const [refreshing, setRefreshing] = useState(false);
1753
1862
  const [outputOverride, setOutputOverride] = useState<string | undefined>(undefined);
1754
1863
  const { data, refetch } = useCustomPillsQuery();
@@ -1856,12 +1965,6 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
1856
1965
  modalTitle: "Host System Resources",
1857
1966
  modalIcon: "Activity",
1858
1967
  icon: "Cpu",
1859
- resolveDefaultPayload: ({ agentId }) => {
1860
- if (latestSettings.pillMode === "all") {
1861
- return latestSettings.defaultTab;
1862
- }
1863
- return currentCycleTabByAgent.get(agentId) ?? latestSettings.defaultTab;
1864
- },
1865
1968
  resolveLabel: async (ctx) => {
1866
1969
  const snap = await liveSnapshotFor(ctx);
1867
1970
  const items = enabledItemsForSettings(latestSettings);
@@ -1878,7 +1981,6 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
1878
1981
  };
1879
1982
  },
1880
1983
  refreshIntervalMs: 3000,
1881
- renderPill: (props) => <PillView {...props} />,
1882
1984
  renderModal: (props) => <ResourceModal {...props} />,
1883
1985
  });
1884
1986
  activePills.set("paseo-top", cleanup);
@@ -1890,17 +1992,7 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
1890
1992
  activePills.delete("paseo-top");
1891
1993
  }
1892
1994
 
1893
- const hasAny =
1894
- flags.showCpuRam ||
1895
- flags.showBranch ||
1896
- flags.showWorktree ||
1897
- flags.showAgentTitle ||
1898
- flags.showAgent ||
1899
- flags.showAgentProvider ||
1900
- flags.showAgentActivity ||
1901
- flags.showAgentId ||
1902
- flags.showLoad ||
1903
- flags.showUptime;
1995
+ const hasAny = hasAnyLegacyPillFlagEnabled(flags);
1904
1996
 
1905
1997
  const effectiveCpu = flags.showCpuRam || !hasAny;
1906
1998
 
@@ -2063,6 +2155,16 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
2063
2155
  defaultTab: "system",
2064
2156
  });
2065
2157
  }
2158
+ if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.velocity)) {
2159
+ desiredPills.push({
2160
+ id: "paseo-top-velocity",
2161
+ item: "velocity",
2162
+ icon: "Zap",
2163
+ title: "Generation Velocity",
2164
+ modalTitle: "Host System Resources",
2165
+ defaultTab: "system",
2166
+ });
2167
+ }
2066
2168
 
2067
2169
  const desiredIds = new Set(desiredPills.map((p) => p.id));
2068
2170
 
@@ -2083,16 +2185,8 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
2083
2185
  modalTitle: pillDef.modalTitle,
2084
2186
  icon: pillDef.icon,
2085
2187
  modalIcon: pillDef.icon,
2086
- resolveDefaultPayload: () => pillDef.defaultTab,
2087
2188
  resolveLabel: singleItemLabelResolver(pillDef.item),
2088
2189
  refreshIntervalMs: 5000,
2089
- renderPill: (props) => (
2090
- <SingleItemPillView
2091
- item={pillDef.item}
2092
- defaultTab={pillDef.defaultTab}
2093
- {...props}
2094
- />
2095
- ),
2096
2190
  renderModal: (props) => (
2097
2191
  <ResourceModal initialTab={pillDef.defaultTab} {...props} />
2098
2192
  ),
@@ -2104,7 +2198,7 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
2104
2198
  }
2105
2199
 
2106
2200
  // Register settings listener
2107
- settingsListeners.add(syncPills);
2201
+ addSettingsListener(syncPills);
2108
2202
 
2109
2203
  // Initial sync synchronously
2110
2204
  syncPills(topSettingsContract.defaultSettings);
@@ -2185,9 +2279,12 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
2185
2279
  );
2186
2280
  },
2187
2281
  refreshIntervalMs: 5000,
2188
- renderPill: () => <LiveCustomPillView pillId={pill.id} initial={pill} />,
2189
- renderModal: () => (
2190
- <LiveCustomPillModal pillId={pill.id} initial={pill} />
2282
+ renderModal: (props) => (
2283
+ <HostThemeProvider theme={props.theme}>
2284
+ <HostLayoutProvider layout={props.layout}>
2285
+ <LiveCustomPillModal pillId={pill.id} initial={pill} />
2286
+ </HostLayoutProvider>
2287
+ </HostThemeProvider>
2191
2288
  ),
2192
2289
  });
2193
2290
  activeCustomPills.set(pill.id, cleanup);
@@ -2206,7 +2303,7 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
2206
2303
  const onSettingsChanged = () => {
2207
2304
  void syncCustomPills();
2208
2305
  };
2209
- settingsListeners.add(onSettingsChanged);
2306
+ addSettingsListener(onSettingsChanged);
2210
2307
 
2211
2308
  let disposed = false;
2212
2309
  const cleanup = () => {
@@ -2218,8 +2315,8 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
2218
2315
  cleanupSidebar = null;
2219
2316
  }
2220
2317
  clearInterval(customPillInterval);
2221
- settingsListeners.delete(syncPills);
2222
- settingsListeners.delete(onSettingsChanged);
2318
+ removeSettingsListener(syncPills);
2319
+ removeSettingsListener(onSettingsChanged);
2223
2320
  for (const cleanup of activePills.values()) {
2224
2321
  cleanup();
2225
2322
  }
@@ -2241,8 +2338,6 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
2241
2338
  // primitive covers.
2242
2339
  const styles = {
2243
2340
  modalRoot: {
2244
- flex: 1,
2245
- minHeight: 0,
2246
2341
  width: "100%",
2247
2342
  },
2248
2343
  pillContainer: {
@@ -2252,6 +2347,11 @@ const styles = {
2252
2347
  overflow: "hidden",
2253
2348
  flexShrink: 1,
2254
2349
  minWidth: 0,
2350
+ // The pill label is one short line; without a height floor a body that
2351
+ // momentarily renders no text (loading, or a blank host field) collapses to
2352
+ // an unreadable skinny line on mobile (xpufx-org/paseo#507). The host
2353
+ // composer pill is 32pt; keep the inner body at least a line tall.
2354
+ minHeight: 20,
2255
2355
  },
2256
2356
  pillText: {
2257
2357
  fontSize: 11,
@@ -2336,80 +2436,6 @@ const styles = {
2336
2436
  accentValueText: {
2337
2437
  fontWeight: "600",
2338
2438
  },
2339
- metricMatrix: {
2340
- gap: 0,
2341
- width: "100%",
2342
- },
2343
- metricMatrixCompact: {
2344
- minWidth: 0,
2345
- },
2346
- metricMatrixHeader: {
2347
- flexDirection: "row",
2348
- alignItems: "center",
2349
- justifyContent: "space-between",
2350
- paddingLeft: 4,
2351
- paddingRight: 4,
2352
- paddingBottom: 2,
2353
- },
2354
- metricMatrixHeaderCompact: {
2355
- paddingLeft: 2,
2356
- paddingRight: 2,
2357
- },
2358
- metricMatrixHeaderText: {
2359
- fontSize: 9,
2360
- fontWeight: "700",
2361
- textTransform: "uppercase",
2362
- letterSpacing: 0.5,
2363
- },
2364
- metricMatrixTargetHeader: {
2365
- flexDirection: "row",
2366
- alignItems: "center",
2367
- gap: 28,
2368
- paddingRight: 10,
2369
- },
2370
- metricMatrixTargetHeaderCompact: {
2371
- gap: 16,
2372
- paddingRight: 8,
2373
- },
2374
- metricMatrixRow: {
2375
- flexDirection: "row",
2376
- alignItems: "center",
2377
- justifyContent: "space-between",
2378
- paddingLeft: 4,
2379
- paddingRight: 4,
2380
- },
2381
- metricMatrixRowCompact: {
2382
- paddingLeft: 2,
2383
- paddingRight: 2,
2384
- },
2385
- metricMatrixLabel: {
2386
- flex: 1,
2387
- minWidth: 0,
2388
- paddingVertical: 2,
2389
- },
2390
- metricMatrixTitle: {
2391
- fontSize: 11,
2392
- fontWeight: "600",
2393
- },
2394
- metricMatrixNote: {
2395
- fontSize: 9,
2396
- lineHeight: 12,
2397
- marginTop: 1,
2398
- },
2399
- metricMatrixTargets: {
2400
- flexDirection: "row",
2401
- alignItems: "center",
2402
- gap: 16,
2403
- paddingLeft: 8,
2404
- },
2405
- metricMatrixTargetsCompact: {
2406
- gap: 8,
2407
- paddingLeft: 4,
2408
- },
2409
- matrixToggle: {
2410
- width: 38,
2411
- minHeight: 44,
2412
- },
2413
2439
  allInOneContainer: {
2414
2440
  flexDirection: "row",
2415
2441
  alignItems: "center",
@@ -2417,6 +2443,8 @@ const styles = {
2417
2443
  overflow: "hidden",
2418
2444
  flexShrink: 1,
2419
2445
  minWidth: 0,
2446
+ // See pillContainer: the combined pill must keep a line-height floor too.
2447
+ minHeight: 20,
2420
2448
  },
2421
2449
  dividerText: {
2422
2450
  fontSize: 10,
@@ -2460,4 +2488,31 @@ const styles = {
2460
2488
  fontStyle: "italic",
2461
2489
  paddingVertical: 4,
2462
2490
  },
2491
+ modalColumn: {
2492
+ width: "100%",
2493
+ },
2494
+ modalContent: {
2495
+ width: "100%",
2496
+ paddingTop: 6,
2497
+ },
2498
+ customPillModal: {
2499
+ width: "100%",
2500
+ padding: 12,
2501
+ },
2502
+ codeBlock: {
2503
+ flexDirection: "row",
2504
+ alignItems: "flex-start",
2505
+ gap: 8,
2506
+ marginTop: 8,
2507
+ },
2508
+ codeText: {
2509
+ flex: 1,
2510
+ minWidth: 0,
2511
+ fontSize: 11,
2512
+ fontFamily: "monospace",
2513
+ },
2514
+ timestampText: {
2515
+ fontSize: 11,
2516
+ marginTop: 8,
2517
+ },
2463
2518
  } as const;