@xpufx/paseo-top 0.4.4 → 0.4.7

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 +18 -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 +94 -1
  6. package/client/pill.tsx +692 -616
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +1 -1
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +308 -276
  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} +9 -3
  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} +51 -18
  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/{surface.tsx → lifecycle/surface.tsx} +6 -6
  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} +50 -58
  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 -234
  48. package/npm-shrinkwrap.json +44 -0
  49. package/package.json +8 -4
  50. package/paseo-plugin.json +2 -0
  51. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  52. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  53. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  54. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  55. package/screenshots/paseo-top-dashboard.jpg +0 -0
  56. package/screenshots/paseo-top-general-view.jpg +0 -0
  57. package/server/resources.ts +98 -0
  58. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  59. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  60. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  61. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  62. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  63. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  64. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  65. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  66. package/server/vendor/permission-audit/index.ts +1 -0
  67. package/server/vendor/permission-audit/server.ts +839 -0
  68. package/shared/helper-version.ts +1 -1
  69. package/shared/multi-host.ts +153 -159
  70. package/shared/resources.ts +145 -0
  71. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  72. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  73. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  74. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  75. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  76. package/shared/vendor/permission-audit/filter.ts +61 -0
  77. package/shared/vendor/permission-audit/index.ts +2 -0
  78. package/shared/vendor/permission-audit/shared.ts +119 -0
  79. package/shared/version.ts +1 -1
  80. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  81. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  82. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  83. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  84. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  85. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  86. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  87. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  88. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  89. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  90. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  91. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  92. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  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 -132
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  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 -26
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  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 -30
  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 -368
  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/theme/flair.ts +0 -76
  121. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  122. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  123. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  124. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  125. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  126. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  127. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  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,46 +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
- CustomPillModalContent,
18
+ type RenderModalProps,
19
+ } from "./vendor/paseo-plugin-helper/lifecycle/index";
20
+ import {
21
+ getClientHost,
32
22
  useRpcQuery,
33
23
  usePluginSettings,
34
- usePluginTheme,
35
- getStatusColor,
36
- triggerHaptic,
24
+ type ComposerPillRegistrar,
37
25
  type HostPillProps,
38
- type RenderModalProps,
39
- type KeyValueProps,
40
- type CardHeaderProps,
41
- type BadgeProps,
42
- } from "./vendor/paseo-plugin-helper/index";
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,
44
+ getStatusColor,
45
+ useHostLayout,
46
+ useHostTheme,
47
+ } from "./host-ui";
43
48
  import {
44
49
  formatBytes,
45
50
  formatUptime,
46
51
  resolveMetricStatus,
47
52
  type MetricThresholds,
53
+ type ThemeColors,
48
54
  } from "../shared/vendor/paseo-plugin-helper/index";
49
- import { PermissionAuditView } from "permission-audit/client";
55
+ import { PermissionAuditView } from "./vendor/permission-audit";
50
56
  import {
51
57
  getSystemResourcesRpc,
52
58
  topSettingsContract,
@@ -54,14 +60,18 @@ import {
54
60
  listCustomPillsRpc,
55
61
  runCustomPillModalCommandRpc,
56
62
  isPillEnabled,
57
- isProviderDependent,
63
+ isTimelineEnabled,
58
64
  legacyFlagView,
59
65
  isMcpSurfaceEnabled,
60
66
  customPillEffectiveEnabled,
61
67
  METRIC_DEFINITIONS,
62
68
  DEFAULT_METRIC_SURFACES,
63
- checkboxesFromTarget,
64
69
  targetFromCheckboxes,
70
+ CPU_THRESHOLDS,
71
+ MEM_THRESHOLDS,
72
+ MODAL_TABS,
73
+ aboutExtraItems,
74
+ hasAnyLegacyPillFlagEnabled,
65
75
  type SystemResources,
66
76
  type TopSettings,
67
77
  type CustomPillStateOutput,
@@ -72,7 +82,7 @@ import { PLUGIN_VERSION } from "../shared/version";
72
82
  import { TopDashboardSurface } from "./surface";
73
83
  import { addSettingsListener, removeSettingsListener, notifySettingsChanged } from "./settings-events";
74
84
  import { useTopResourceQuery, useCustomPillsQuery } from "./resources-query";
75
- import { ChoiceChips } from "./settings-ui";
85
+ import { ChoiceChips, MultiChoiceChips } from "./settings-ui";
76
86
  import {
77
87
  buildAllLabel,
78
88
  describeSegment,
@@ -90,6 +100,11 @@ import {
90
100
 
91
101
  const EMPTY_PARAMS = {};
92
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
+
93
108
  function formatWorktreeLocation(dir: string | null | undefined): string {
94
109
  if (!dir) return "";
95
110
  // Check for Paseo-managed worktree: ~/.paseo/worktrees/<hash>/<slug>
@@ -133,25 +148,9 @@ function formatIdleDuration(isoString: string | null | undefined): string {
133
148
  return `${days}d ${hours % 24}h`;
134
149
  }
135
150
 
136
- const CPU_THRESHOLDS: MetricThresholds = { warning: 60, danger: 85 };
137
- const MEM_THRESHOLDS: MetricThresholds = { warning: 70, danger: 85 };
138
-
139
- const TABS = [
140
- { id: "system", label: "System", shortLabel: "System", icon: "Activity" },
141
- { id: "context", label: "Workspace", shortLabel: "Workspace", icon: "GitBranch" },
142
- { id: "permissions", label: "Permissions", shortLabel: "Permissions", icon: "ShieldCheck" },
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 ||
@@ -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,7 +461,7 @@ function PillItemContent({
444
461
  </Text>
445
462
  ) : null}
446
463
  </Text>
447
- </Row>
464
+ </HostRow>
448
465
  );
449
466
  }
450
467
 
@@ -452,19 +469,19 @@ export { notifySettingsChanged } from "./settings-events";
452
469
  type SettingsListener = import("./settings-events").SettingsListener;
453
470
 
454
471
  function PillOffline() {
455
- const { colors } = usePluginTheme();
472
+ const { colors } = useHostTheme();
456
473
  return (
457
- <Row gap={4} align="center" style={styles.pillContainer}>
474
+ <HostRow gap={4} align="center" style={styles.pillContainer}>
458
475
  <Icon name="Ghost" size={13} color={colors.statusDanger} />
459
476
  <Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
460
477
  Offline
461
478
  </Text>
462
- </Row>
479
+ </HostRow>
463
480
  );
464
481
  }
465
482
 
466
483
  function PillLoading() {
467
- const { colors } = usePluginTheme();
484
+ const { colors } = useHostTheme();
468
485
  return (
469
486
  <Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
470
487
  top...
@@ -473,19 +490,19 @@ function PillLoading() {
473
490
  }
474
491
 
475
492
  function PillEmpty() {
476
- const { colors } = usePluginTheme();
493
+ const { colors } = useHostTheme();
477
494
  return (
478
- <Row gap={4} align="center" style={styles.pillContainer}>
495
+ <HostRow gap={4} align="center" style={styles.pillContainer}>
479
496
  <Icon name="Activity" size={12} color={colors.accent} />
480
497
  <Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
481
498
  top
482
499
  </Text>
483
- </Row>
500
+ </HostRow>
484
501
  );
485
502
  }
486
503
 
487
504
  function McpLoadingPill() {
488
- const { colors } = usePluginTheme();
505
+ const { colors } = useHostTheme();
489
506
  return (
490
507
  <Text numberOfLines={1} style={[styles.pillText, { color: colors.foregroundMuted }]}>
491
508
  MCP…
@@ -543,9 +560,9 @@ function AllInOnePill({
543
560
  worktreeLocationText,
544
561
  isOpen,
545
562
  }: AllInOnePillProps) {
546
- const { colors } = usePluginTheme();
563
+ const { colors } = useHostTheme();
547
564
  return (
548
- <Row gap={6} align="center" style={styles.allInOneContainer}>
565
+ <HostRow gap={6} align="center" style={styles.allInOneContainer}>
549
566
  {items.map((item, idx) => (
550
567
  <React.Fragment key={item}>
551
568
  {idx > 0 && (
@@ -561,7 +578,7 @@ function AllInOnePill({
561
578
  />
562
579
  </React.Fragment>
563
580
  ))}
564
- </Row>
581
+ </HostRow>
565
582
  );
566
583
  }
567
584
 
@@ -584,7 +601,18 @@ export interface SingleItemPillViewProps extends PillBodyProps {
584
601
  defaultTab?: ModalTab;
585
602
  }
586
603
 
587
- 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({
588
616
  item,
589
617
  workspaceId,
590
618
  agentId,
@@ -664,7 +692,18 @@ export function SingleItemPillView({
664
692
  );
665
693
  }
666
694
 
667
- function PillView({ isOpen, open, workspaceId, agentId }: PillBodyProps) {
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) {
668
707
  // No background settings poll here: the hook re-verifies on mount and
669
708
  // window focus, and mutations invalidate the shared settings cache.
670
709
  // Polling from every mounted pill/modal/surface multiplied daemon reads
@@ -686,18 +725,7 @@ function PillView({ isOpen, open, workspaceId, agentId }: PillBodyProps) {
686
725
  lastUsage: a?.lastUsage,
687
726
  }));
688
727
 
689
- const hasAnyEnabled =
690
- flags.showCpuRam ||
691
- flags.showBranch ||
692
- flags.showWorktree ||
693
- flags.showAgentTitle ||
694
- flags.showAgent ||
695
- flags.showAgentProvider ||
696
- flags.showAgentActivity ||
697
- flags.showAgentId ||
698
- flags.showLoad ||
699
- flags.showUptime ||
700
- (flags.showMcp);
728
+ const hasAnyEnabled = hasAnyLegacyPillFlagEnabled(flags) || flags.showMcp;
701
729
 
702
730
  // If no items are selected, fallback to CPU & RAM without mutating saved settings
703
731
  const effectiveShowCpuRam = flags.showCpuRam || !hasAnyEnabled;
@@ -784,6 +812,9 @@ function PillView({ isOpen, open, workspaceId, agentId }: PillBodyProps) {
784
812
  if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.turns)) {
785
813
  items.push("turns");
786
814
  }
815
+ if (settings.metricSurfaces && isPillEnabled(settings.metricSurfaces.velocity)) {
816
+ items.push("velocity");
817
+ }
787
818
 
788
819
  // Legacy render fallback only. On the supported 0.8 host the pill is
789
820
  // host-rendered from `resolveLabel`, which advances the cycle on the shared
@@ -839,12 +870,11 @@ interface ResourceModalProps extends RenderModalProps<ModalTab> {
839
870
  initialTab?: ModalTab;
840
871
  }
841
872
 
842
- function MetricSurfaceMatrix({
873
+ function MetricPillSelector({
843
874
  settings,
844
875
  updateSettings,
845
876
  notifySettingsChanged,
846
877
  mcpInstalled,
847
- mcpRunning,
848
878
  customPills,
849
879
  customOverrides,
850
880
  onCustomToggle,
@@ -853,141 +883,119 @@ function MetricSurfaceMatrix({
853
883
  updateSettings: (updates: Partial<TopSettings>) => void;
854
884
  notifySettingsChanged: (s: TopSettings) => void;
855
885
  mcpInstalled: boolean;
856
- mcpRunning?: boolean | null;
857
886
  customPills: Array<{ id: string; title: string; sourceFile?: string; enabled: boolean }>;
858
887
  customOverrides: Record<string, boolean> | undefined;
859
888
  onCustomToggle: (id: string, val: boolean) => void;
860
889
  }) {
861
- const { colors, isCompact } = usePluginTheme();
890
+ const { colors } = useHostTheme();
862
891
  const surfaces = settings.metricSurfaces ?? DEFAULT_METRIC_SURFACES;
863
- const provisioned = new Set(settings.provisionedMetrics ?? []);
864
- const matrixRowMinHeight = isCompact ? 44 : 32;
865
- const setTarget = (id: MetricId, target: SurfaceTarget) => {
866
- const next = { ...surfaces, [id]: target };
867
- 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 };
914
+ updateSettings({ metricSurfaces: next });
915
+ notifySettingsChanged(nextSettings);
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 };
868
939
  updateSettings({ metricSurfaces: next });
869
- notifySettingsChanged(s);
940
+ notifySettingsChanged(nextSettings);
870
941
  };
942
+
871
943
  return (
872
- <View style={[styles.metricMatrix, isCompact && styles.metricMatrixCompact]}>
873
- <View style={[styles.metricMatrixHeader, isCompact && styles.metricMatrixHeaderCompact]}>
874
- <Text style={[styles.metricMatrixHeaderText, { color: colors.foregroundMuted }]}>
875
- Metric
876
- </Text>
877
- <View style={[styles.metricMatrixTargetHeader, isCompact && styles.metricMatrixTargetHeaderCompact]}>
878
- <Text style={[styles.metricMatrixHeaderText, { color: colors.foregroundMuted }]}>
879
- Pill
880
- </Text>
881
- <Text style={[styles.metricMatrixHeaderText, { color: colors.foregroundMuted }]}>
882
- 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
883
956
  </Text>
884
- </View>
885
- </View>
886
- {METRIC_DEFINITIONS.map((def, index) => {
887
- const boxes = checkboxesFromTarget(surfaces[def.id]);
888
- const unprovisioned =
889
- isProviderDependent(def.id) && !provisioned.has(def.id);
890
- const timelineDisabled = !!def.pillOnly;
891
- const disabled = def.id === "mcp" && !mcpInstalled;
892
- const note =
893
- def.pillOnly
894
- ? "live only"
895
- : def.id === "mcp" && !mcpInstalled
896
- ? "mcp-tools not installed"
897
- : def.id === "mcp" && mcpRunning === false
898
- ? "mcp-tools disabled"
899
- : unprovisioned
900
- ? "waiting for provider"
901
- : null;
902
- const setBox = (which: "pill" | "timeline", val: boolean) => {
903
- const nextBoxes = { ...boxes, [which]: val };
904
- if (def.pillOnly) nextBoxes.timeline = false;
905
- setTarget(
906
- def.id,
907
- targetFromCheckboxes(nextBoxes.pill, nextBoxes.timeline),
908
- );
909
- };
910
- return (
911
- <View
912
- key={def.id}
913
- style={[
914
- styles.metricMatrixRow,
915
- isCompact && styles.metricMatrixRowCompact,
916
- { minHeight: matrixRowMinHeight },
917
- index < METRIC_DEFINITIONS.length - 1
918
- ? {
919
- borderBottomWidth: 1,
920
- borderBottomColor: colors.border,
921
- }
922
- : undefined,
923
- ]}
924
- >
925
- <View style={styles.metricMatrixLabel}>
926
- <Text style={[styles.metricMatrixTitle, { color: colors.foreground }]}>
927
- {def.title}
928
- </Text>
929
- {note ? (
930
- <Text style={[styles.metricMatrixNote, { color: colors.foregroundMuted }]}>
931
- {note}
932
- </Text>
933
- ) : null}
934
- </View>
935
- <View style={[styles.metricMatrixTargets, isCompact && styles.metricMatrixTargetsCompact]}>
936
- <Toggle
937
- value={boxes.pill}
938
- disabled={disabled}
939
- onValueChange={(val) => setBox("pill", val)}
940
- style={styles.matrixToggle}
941
- />
942
- <Toggle
943
- value={boxes.timeline && !def.pillOnly}
944
- disabled={disabled || timelineDisabled}
945
- onValueChange={(val) => setBox("timeline", val)}
946
- style={styles.matrixToggle}
947
- />
948
- </View>
949
- </View>
950
- );
951
- })}
952
- {customPills.map((pill) => {
953
- const masterOn = settings.showCustomPills ?? true;
954
- const enabled = customPillEffectiveEnabled(masterOn, customOverrides, pill);
955
- return (
956
- <View
957
- key={`custom-${pill.id}`}
958
- style={styles.metricMatrixRow}
959
- >
960
- <View style={styles.metricMatrixLabel}>
961
- <Text style={[styles.metricMatrixTitle, { color: colors.foreground }]}>
962
- {pill.title}
963
- </Text>
964
- <Text style={[styles.metricMatrixNote, { color: colors.foregroundMuted }]}>
965
- custom pill
966
- </Text>
967
- </View>
968
- <View style={[styles.metricMatrixTargets, isCompact && styles.metricMatrixTargetsCompact]}>
969
- <Toggle
970
- value={enabled}
971
- disabled={!masterOn}
972
- onValueChange={(val) => onCustomToggle(pill.id, val)}
973
- style={styles.matrixToggle}
974
- />
975
- <Toggle
976
- value={false}
977
- disabled={true}
978
- onValueChange={() => {}}
979
- style={styles.matrixToggle}
980
- />
981
- </View>
982
- </View>
983
- );
984
- })}
985
- </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>
986
978
  );
987
979
  }
988
980
 
989
- function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: ResourceModalProps) {
990
- 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 };
991
999
  // Settings re-verify on mount/focus via hook defaults; the settings tab
992
1000
  // refetches explicitly below. No background poll: the modal only lives
993
1001
  // while open. Custom-pill definitions are a separate data source and keep
@@ -1063,18 +1071,7 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1063
1071
  });
1064
1072
  }, [agentId, data, agent, workspace?.directory]);
1065
1073
 
1066
- const hasAnyPillEnabled =
1067
- flags.showCpuRam ||
1068
- flags.showBranch ||
1069
- flags.showWorktree ||
1070
- flags.showAgentTitle ||
1071
- flags.showAgent ||
1072
- flags.showAgentProvider ||
1073
- flags.showAgentActivity ||
1074
- flags.showAgentId ||
1075
- flags.showLoad ||
1076
- flags.showUptime ||
1077
- isMcpEnabled;
1074
+ const hasAnyPillEnabled = hasAnyLegacyPillFlagEnabled(flags) || isMcpEnabled;
1078
1075
 
1079
1076
  const handleRefresh = () => {
1080
1077
  triggerHaptic("light");
@@ -1088,8 +1085,8 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1088
1085
  };
1089
1086
 
1090
1087
  const navbar = (
1091
- <Tabs
1092
- tabs={TABS}
1088
+ <HostTabs
1089
+ tabs={[...MODAL_TABS]}
1093
1090
  activeTab={activeTab}
1094
1091
  onTabChange={handleTabChange}
1095
1092
  />
@@ -1099,235 +1096,251 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1099
1096
  paddingHorizontal: padding.horizontal,
1100
1097
  paddingTop: padding.vertical,
1101
1098
  paddingBottom: Math.round(padding.gap / 2),
1102
- };
1099
+ position: "sticky" as const,
1100
+ top: 0,
1101
+ zIndex: 10,
1102
+ } as any;
1103
1103
 
1104
1104
  if (isError && !data) {
1105
1105
  return (
1106
- <ModalBody
1107
- style={{ backgroundColor: colors.surface0 }}
1108
- headerMode="pinned"
1109
- scrollMode="always"
1110
- refreshing={isRefetching}
1111
- onRefresh={handleRefresh}
1112
- header={navbar}
1113
- headerStyle={navbarStyle}
1114
- contentContainerStyle={[{ paddingTop: Math.round(padding.gap / 2) }]}
1115
- >
1116
- <Card variant="elevated">
1117
- <View style={styles.errorBox}>
1118
- <Icon name="Ghost" size={24} color={colors.statusDanger} />
1119
- <Text style={[styles.errorText, { color: colors.statusDanger }]}>
1120
- {error instanceof Error ? error.message : "Failed to load metrics"}
1121
- </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>
1122
1118
  </View>
1123
- </Card>
1124
- </ModalBody>
1119
+ </View>
1120
+ </HostModalSection>
1125
1121
  );
1126
1122
  }
1127
1123
 
1128
1124
  const { cpuColor, memColor } = getMetricColors(data, colors);
1129
1125
 
1130
1126
  return (
1131
- <ModalBody
1132
- style={{ backgroundColor: colors.surface0 }}
1133
- headerMode="pinned"
1134
- scrollMode="always"
1135
- refreshing={isLoading || isRefetching}
1136
- onRefresh={handleRefresh}
1137
- header={navbar}
1138
- headerStyle={navbarStyle}
1139
- contentContainerStyle={[
1140
- { paddingTop: Math.round(padding.gap / 2) },
1141
- ]}
1142
- >
1143
- {activeTab === "system" && (
1144
- <>
1145
- {/* Dual Metric Gauges Hero */}
1146
- <Card variant="elevated">
1147
- <View style={styles.gaugeContainer}>
1148
- <MetricGauge
1149
- value={data?.cpuUsagePercent ?? 0}
1150
- thresholds={CPU_THRESHOLDS}
1151
- label="CPU Load"
1152
- size={72}
1153
- />
1154
- <MetricGauge
1155
- value={data?.memoryUsedPercent ?? 0}
1156
- thresholds={MEM_THRESHOLDS}
1157
- label="RAM Used"
1158
- size={72}
1159
- />
1160
- </View>
1161
- </Card>
1162
-
1163
- {/* Host Meta Card */}
1164
- <Card variant="elevated">
1165
- <KeyValueGroup columns={1}>
1166
- <CompactKeyValue label="Host" value={data?.hostname ?? "Unknown"} copyable />
1167
- <CompactKeyValue
1168
- label="Uptime"
1169
- value={data?.uptimeSeconds ? formatUptime(data?.uptimeSeconds) : "--"}
1170
- />
1171
- </KeyValueGroup>
1172
- <CompactKeyValue
1173
- label="Processor"
1174
- value={data?.cpuModel ?? "--"}
1175
- subValue={data?.cpuCores ? `(${data?.cpuCores} cores)` : undefined}
1176
- />
1177
- </Card>
1178
-
1179
- {/* CPU Utilization Card */}
1180
- <Card variant="elevated">
1181
- <CompactCardHeader
1182
- title="CPU Details"
1183
- value={
1184
- <Text style={[styles.metricHighlight, { color: cpuColor }]}>
1185
- {data?.cpuUsagePercent ?? 0}%
1186
- </Text>
1187
- }
1188
- />
1189
-
1190
- <ProgressBar
1191
- value={data?.cpuUsagePercent ?? 0}
1192
- thresholds={CPU_THRESHOLDS}
1193
- height={8}
1194
- />
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
+ />
1195
1185
 
1196
- <CompactKeyValue
1197
- label="Load Average (1m, 5m, 15m)"
1198
- value={data?.loadAvg ? data?.loadAvg.map((n) => n.toFixed(2)).join(" ") : "-- -- --"}
1199
- mono
1200
- />
1201
- </Card>
1186
+ <HostProgressBar
1187
+ value={data?.cpuUsagePercent ?? 0}
1188
+ thresholds={CPU_THRESHOLDS}
1189
+ height={8}
1190
+ />
1202
1191
 
1203
- {/* Memory Card */}
1204
- <Card variant="elevated">
1205
- <CompactCardHeader
1206
- title="Memory Details"
1207
- value={
1208
- <Text style={[styles.metricHighlight, { color: memColor }]}>
1209
- {data?.memoryUsedPercent ?? 0}%
1210
- </Text>
1211
- }
1212
- />
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
+ />
1213
1211
 
1214
- <ProgressBar
1215
- value={data?.memoryUsedPercent ?? 0}
1216
- thresholds={MEM_THRESHOLDS}
1217
- height={8}
1218
- />
1212
+ <HostProgressBar
1213
+ value={data?.memoryUsedPercent ?? 0}
1214
+ thresholds={MEM_THRESHOLDS}
1215
+ height={8}
1216
+ />
1219
1217
 
1220
- <CompactKeyValue
1221
- label="Used / Total"
1222
- value={`${formatBytes(data?.memoryUsedBytes ?? 0)} / ${formatBytes(data?.memoryTotalBytes ?? 0)}`}
1223
- />
1224
- </Card>
1225
-
1226
- {/* MCP Servers Card */}
1227
- {Boolean(data?.mcpInstalled) && (
1228
- <Card variant="elevated">
1229
- <CompactCardHeader
1230
- title="MCP Servers"
1231
- subtitle="Source: paseo-mcp-tools"
1232
- icon="Server"
1233
- value={
1234
- data?.mcp ? (
1235
- <CompactBadge
1236
- label={data?.mcp.isStale ? "Stale Snapshot" : "Live"}
1237
- variant={data?.mcp.isStale ? "warning" : "success"}
1238
- 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
+ }
1239
1249
  />
1240
- ) : (
1241
- <CompactBadge label="No Data" variant="neutral" />
1242
- )
1243
- }
1244
- />
1245
1250
 
1246
- {data?.mcp ? (
1247
- <>
1248
- <KeyValueGroup columns={1}>
1249
- <CompactKeyValue
1250
- label="Health"
1251
- value={`${data?.mcp.healthy} healthy / ${data?.mcp.total} total`}
1252
- />
1253
- <CompactKeyValue
1254
- label="Snapshot Updated"
1255
- value={formatTimeAgo(data?.mcp.updatedAt)}
1256
- />
1257
- <CompactKeyValue
1258
- label="Data Provider"
1259
- value="paseo-mcp-tools"
1260
- />
1261
- </KeyValueGroup>
1262
-
1263
- {data?.mcp.servers && data?.mcp.servers.length > 0 ? (
1264
- <View style={styles.mcpList}>
1265
- {data?.mcp.servers.map((srv) => {
1266
- const badgeVariant: "success" | "warning" | "danger" | "neutral" =
1267
- srv.status === "healthy"
1268
- ? "success"
1269
- : srv.status === "degraded"
1270
- ? "warning"
1271
- : srv.status === "down"
1272
- ? "danger"
1273
- : "neutral";
1274
- return (
1275
- <View key={srv.name} style={styles.mcpRow}>
1276
- <View style={styles.mcpInfo}>
1277
- <Text
1278
- numberOfLines={1}
1279
- style={[styles.mcpName, { color: colors.foreground }]}
1280
- >
1281
- {srv.name}
1282
- </Text>
1283
- <Text style={[styles.mcpLatency, { color: colors.foregroundMuted }]}>
1284
- {srv.latencyMs >= 0 ? `${srv.latencyMs}ms` : "timeout"}
1285
- </Text>
1286
- </View>
1287
- <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
+ })}
1288
1304
  </View>
1289
- );
1290
- })}
1291
- </View>
1292
- ) : (
1293
- <Text style={[styles.mcpEmpty, { color: colors.foregroundMuted }]}>
1294
- No MCP servers configured
1295
- </Text>
1296
- )}
1297
- </>
1298
- ) : (
1299
- <Text style={[styles.mcpEmpty, { color: colors.foregroundMuted }]}>
1300
- Waiting for status snapshot from paseo-mcp-tools...
1301
- </Text>
1302
- )}
1303
- </Card>
1304
- )}
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
+ )}
1305
1318
 
1306
1319
  {/* Custom Metric Pills Card */}
1307
1320
  {Boolean(data?.customPills && data?.customPills.length > 0) && (
1308
- <Card variant="elevated">
1309
- <CompactCardHeader
1321
+ <HostCard variant="elevated">
1322
+ <HostCardHeader {...compactCardTitleProp}
1310
1323
  title="Custom Metric Pills"
1311
1324
  subtitle="Discovered from ~/.paseo/top/pills"
1312
1325
  icon="Sliders"
1313
1326
  value={
1314
- <CompactBadge
1327
+ <HostBadge {...compactBadgeProp}
1315
1328
  label={`${data?.customPills?.length ?? 0} active`}
1316
1329
  variant="accent"
1317
1330
  />
1318
1331
  }
1319
1332
  />
1320
- <KeyValueGroup columns={1}>
1333
+ <HostStack gap={12}>
1321
1334
  {(data?.customPills ?? []).map((cp) => (
1322
- <CompactKeyValue
1335
+ <HostKeyValue {...compactKvProps}
1323
1336
  key={cp.id}
1324
1337
  label={cp.title}
1325
1338
  value={cp.displayValue}
1326
1339
  subValue={cp.status !== "neutral" ? `(${cp.status})` : undefined}
1327
1340
  />
1328
1341
  ))}
1329
- </KeyValueGroup>
1330
- </Card>
1342
+ </HostStack>
1343
+ </HostCard>
1331
1344
  )}
1332
1345
  </>
1333
1346
  )}
@@ -1335,48 +1348,48 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1335
1348
  {activeTab === "context" && (
1336
1349
  <>
1337
1350
  {/* Workspace Information */}
1338
- <Card variant="elevated">
1339
- <CompactCardHeader
1351
+ <HostCard variant="elevated">
1352
+ <HostCardHeader {...compactCardTitleProp}
1340
1353
  title="Workspace & Git"
1341
1354
  icon="GitBranch"
1342
1355
  value={
1343
1356
  workspace?.status ? (
1344
- <CompactBadge label={workspace.status} variant="info" />
1357
+ <HostBadge {...compactBadgeProp} label={workspace.status} variant="info" />
1345
1358
  ) : undefined
1346
1359
  }
1347
1360
  />
1348
- <KeyValueGroup columns={1}>
1349
- <CompactKeyValue label="Git Branch" value={data?.branch || "Unknown"} />
1350
- <CompactKeyValue label="Kind" value={workspace?.kind || "Unknown"} />
1351
- </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>
1352
1365
  {workspace?.directory ? (
1353
- <CompactKeyValue label="Worktree Location" value={workspace.directory} copyable mono />
1366
+ <HostKeyValue {...compactKvProps} label="Worktree Location" value={workspace.directory} copyable mono />
1354
1367
  ) : null}
1355
1368
  {workspace?.name ? (
1356
- <CompactKeyValue label="Workspace Name" value={workspace.name} />
1369
+ <HostKeyValue {...compactKvProps} label="Workspace Name" value={workspace.name} />
1357
1370
  ) : null}
1358
1371
  {workspace?.title && workspace.title !== workspace.name ? (
1359
- <CompactKeyValue label="Workspace Title" value={workspace.title} />
1372
+ <HostKeyValue {...compactKvProps} label="Workspace Title" value={workspace.title} />
1360
1373
  ) : null}
1361
1374
  {workspace?.projectDisplayName ? (
1362
- <CompactKeyValue label="Project" value={workspace.projectDisplayName} />
1375
+ <HostKeyValue {...compactKvProps} label="Project" value={workspace.projectDisplayName} />
1363
1376
  ) : null}
1364
1377
  {workspace?.diffStat ? (
1365
- <CompactKeyValue
1378
+ <HostKeyValue {...compactKvProps}
1366
1379
  label="Git Changes"
1367
1380
  value={`+${workspace.diffStat.additions} -${workspace.diffStat.deletions}`}
1368
1381
  />
1369
1382
  ) : null}
1370
- </Card>
1383
+ </HostCard>
1371
1384
 
1372
1385
  {/* Agent Information */}
1373
- <Card variant="elevated">
1374
- <CompactCardHeader
1386
+ <HostCard variant="elevated">
1387
+ <HostCardHeader {...compactCardTitleProp}
1375
1388
  title={agent?.title ? `Agent: ${agent.title}` : "Agent Session"}
1376
1389
  icon="Bot"
1377
1390
  value={
1378
1391
  agent?.status ? (
1379
- <CompactBadge
1392
+ <HostBadge {...compactBadgeProp}
1380
1393
  label={agent.status}
1381
1394
  variant={agent.status === "running" ? "success" : "info"}
1382
1395
  />
@@ -1384,17 +1397,17 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1384
1397
  }
1385
1398
  />
1386
1399
  {agent?.title ? (
1387
- <CompactKeyValue label="Agent Tab" value={agent.title} />
1400
+ <HostKeyValue {...compactKvProps} label="Agent Tab" value={agent.title} />
1388
1401
  ) : null}
1389
1402
  {agentId ? (
1390
- <CompactKeyValue label="Agent ID" value={agentId} copyable mono />
1403
+ <HostKeyValue {...compactKvProps} label="Agent ID" value={agentId} copyable mono />
1391
1404
  ) : null}
1392
- <KeyValueGroup columns={1}>
1393
- <CompactKeyValue label="Model" value={agent?.model || "Standard"} />
1394
- <CompactKeyValue label="Provider" value={agent?.provider || "Default"} />
1395
- </KeyValueGroup>
1396
- <KeyValueGroup columns={1}>
1397
- <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}
1398
1411
  label="Last Worked"
1399
1412
  value={
1400
1413
  agent?.status === "running"
@@ -1402,7 +1415,7 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1402
1415
  : formatTimeAgo(agent?.lastActivityAt)
1403
1416
  }
1404
1417
  />
1405
- <CompactKeyValue
1418
+ <HostKeyValue {...compactKvProps}
1406
1419
  label="Inactivity"
1407
1420
  value={
1408
1421
  agent?.status === "running"
@@ -1410,25 +1423,25 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1410
1423
  : formatIdleDuration(agent?.lastActivityAt)
1411
1424
  }
1412
1425
  />
1413
- </KeyValueGroup>
1426
+ </HostStack>
1414
1427
  {agent?.cwd ? (
1415
- <CompactKeyValue label="Working Directory" value={agent.cwd} copyable mono />
1428
+ <HostKeyValue {...compactKvProps} label="Working Directory" value={agent.cwd} copyable mono />
1416
1429
  ) : null}
1417
- </Card>
1430
+ </HostCard>
1418
1431
 
1419
1432
  {/* Token Usage & Context Window */}
1420
- <Card variant="elevated">
1421
- <CompactCardHeader
1433
+ <HostCard variant="elevated">
1434
+ <HostCardHeader {...compactCardTitleProp}
1422
1435
  title="Tokens & Context Window"
1423
1436
  icon="Coins"
1424
1437
  value={
1425
1438
  contextPercent != null ? (
1426
- <CompactBadge
1439
+ <HostBadge {...compactBadgeProp}
1427
1440
  label={`${contextPercent}% ctx`}
1428
1441
  variant={contextPercent >= 85 ? "danger" : contextPercent >= 70 ? "warning" : "success"}
1429
1442
  />
1430
1443
  ) : tokenMetrics?.totalTokens != null ? (
1431
- <CompactBadge
1444
+ <HostBadge {...compactBadgeProp}
1432
1445
  label={`${formatCompactTokens(tokenMetrics.totalTokens)} tok`}
1433
1446
  variant="neutral"
1434
1447
  />
@@ -1438,67 +1451,67 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1438
1451
 
1439
1452
  {tokenMetrics?.contextMaxTokens != null && tokenMetrics.contextMaxTokens > 0 ? (
1440
1453
  <View style={styles.contextUsageBlock}>
1441
- <View style={styles.contextUsageRow}>
1454
+ <HostRow justify="between" align="center" style={styles.contextUsageRow}>
1442
1455
  <Text style={[styles.compactKvLabel, { color: colors.foregroundMuted }]}>Context Utilization</Text>
1443
1456
  <Text style={[styles.compactKvValue, { color: colors.foreground, fontWeight: "600" }]}>
1444
1457
  {formatCompactTokens(tokenMetrics.contextUsedTokens ?? 0)} / {formatCompactTokens(tokenMetrics.contextMaxTokens)} ({contextPercent}%)
1445
1458
  </Text>
1446
- </View>
1447
- <ProgressBar
1459
+ </HostRow>
1460
+ <HostProgressBar
1448
1461
  value={contextPercent ?? 0}
1449
1462
  thresholds={{ warning: 70, danger: 85 }}
1450
1463
  height={8}
1451
1464
  />
1452
1465
  </View>
1453
1466
  ) : tokenMetrics?.contextUsedTokens != null ? (
1454
- <CompactKeyValue
1467
+ <HostKeyValue {...compactKvProps}
1455
1468
  label="Context Used"
1456
- value={`${tokenMetrics.contextUsedTokens.toLocaleString()} (${formatCompactTokens(tokenMetrics.contextUsedTokens)})`}
1469
+ value={`${tokenMetrics.contextUsedTokens.toLocaleString('en-US')} (${formatCompactTokens(tokenMetrics.contextUsedTokens)})`}
1457
1470
  />
1458
1471
  ) : null}
1459
1472
 
1460
- <KeyValueGroup columns={1}>
1461
- <CompactKeyValue
1473
+ <HostStack gap={12}>
1474
+ <HostKeyValue {...compactKvProps}
1462
1475
  label="Input Tokens"
1463
- value={tokenMetrics?.inputTokens != null ? tokenMetrics.inputTokens.toLocaleString() : "--"}
1476
+ value={tokenMetrics?.inputTokens != null ? tokenMetrics.inputTokens.toLocaleString('en-US') : "--"}
1464
1477
  subValue={tokenMetrics?.inputTokens != null ? formatCompactTokens(tokenMetrics.inputTokens) : undefined}
1465
1478
  />
1466
- <CompactKeyValue
1479
+ <HostKeyValue {...compactKvProps}
1467
1480
  label="Output Tokens"
1468
- value={tokenMetrics?.outputTokens != null ? tokenMetrics.outputTokens.toLocaleString() : "--"}
1481
+ value={tokenMetrics?.outputTokens != null ? tokenMetrics.outputTokens.toLocaleString('en-US') : "--"}
1469
1482
  subValue={tokenMetrics?.outputTokens != null ? formatCompactTokens(tokenMetrics.outputTokens) : undefined}
1470
1483
  />
1471
1484
  {tokenMetrics?.cachedTokens != null && (
1472
- <CompactKeyValue
1485
+ <HostKeyValue {...compactKvProps}
1473
1486
  label="Cached Tokens"
1474
- value={tokenMetrics.cachedTokens.toLocaleString()}
1487
+ value={tokenMetrics.cachedTokens.toLocaleString('en-US')}
1475
1488
  subValue={formatCompactTokens(tokenMetrics.cachedTokens)}
1476
1489
  />
1477
1490
  )}
1478
1491
  {tokenMetrics?.costUsd != null && (
1479
- <CompactKeyValue
1492
+ <HostKeyValue {...compactKvProps}
1480
1493
  label="Session / Turn Cost"
1481
1494
  value={`$${tokenMetrics.costUsd < 0.01 ? tokenMetrics.costUsd.toFixed(4) : tokenMetrics.costUsd.toFixed(2)}`}
1482
1495
  />
1483
1496
  )}
1484
- </KeyValueGroup>
1497
+ </HostStack>
1485
1498
  {!tokenMetrics && (
1486
1499
  <Text style={[styles.mcpEmpty, { color: colors.foregroundMuted, marginTop: 4 }]}>
1487
1500
  No token usage recorded for this agent session yet
1488
1501
  </Text>
1489
1502
  )}
1490
- </Card>
1503
+ </HostCard>
1491
1504
  </>
1492
1505
  )}
1493
1506
 
1494
1507
  {activeTab === "settings" && (
1495
1508
  <>
1496
1509
  {/* Pill Display Mode */}
1497
- <Card variant="elevated">
1498
- <CompactCardHeader
1510
+ <HostCard variant="elevated">
1511
+ <HostCardHeader {...compactCardTitleProp}
1499
1512
  title="Pill Display Mode"
1500
1513
  icon="LayoutGrid"
1501
- subtitle="How active items appear in the composer trackbar"
1514
+ subtitle="How active items appear in the composer"
1502
1515
  />
1503
1516
  <ChoiceChips
1504
1517
  options={[
@@ -1515,7 +1528,7 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1515
1528
  }}
1516
1529
  />
1517
1530
  <View style={styles.settingsSpacer}>
1518
- <Toggle
1531
+ <HostToggle
1519
1532
  label="Show Composer Pill"
1520
1533
  description="Hide the composer pill entirely; the dashboard stays available from the sidebar"
1521
1534
  style={styles.toggleFullWidth}
@@ -1527,13 +1540,18 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1527
1540
  }}
1528
1541
  />
1529
1542
  </View>
1530
- </Card>
1543
+ </HostCard>
1531
1544
 
1532
1545
  {/* Active Pill Items Selectors */}
1533
- <Card variant="elevated">
1534
- <CompactCardHeader
1546
+ <HostCard variant="elevated">
1547
+ <HostCardHeader {...compactCardTitleProp}
1535
1548
  title="Active Pill Items"
1536
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
+ }
1537
1555
  subtitle={
1538
1556
  !hasAnyPillEnabled
1539
1557
  ? "None selected -- automatically showing CPU & RAM"
@@ -1545,12 +1563,11 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1545
1563
  }
1546
1564
  />
1547
1565
  <View style={styles.settingsToggles}>
1548
- <MetricSurfaceMatrix
1566
+ <MetricPillSelector
1549
1567
  settings={settings}
1550
1568
  updateSettings={updateSettings}
1551
1569
  notifySettingsChanged={notifySettingsChanged}
1552
1570
  mcpInstalled={Boolean(data?.mcpInstalled)}
1553
- mcpRunning={data?.mcpRunning ?? null}
1554
1571
  customPills={(customPillList?.pills ?? []).map((pill) => ({
1555
1572
  id: pill.id,
1556
1573
  title: pill.title,
@@ -1566,17 +1583,17 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1566
1583
  }}
1567
1584
  />
1568
1585
  </View>
1569
- </Card>
1586
+ </HostCard>
1570
1587
 
1571
1588
  {/* Custom Metric Pills */}
1572
- <Card variant="elevated">
1573
- <CompactCardHeader
1589
+ <HostCard variant="elevated">
1590
+ <HostCardHeader {...compactCardTitleProp}
1574
1591
  title="Custom Metric Pills"
1575
1592
  icon="Sliders"
1576
1593
  subtitle="Standalone pills discovered from ~/.paseo/top/pills"
1577
1594
  />
1578
1595
  <View style={styles.settingsToggles}>
1579
- <Toggle
1596
+ <HostToggle
1580
1597
  label="Show Custom Metric Pills"
1581
1598
  description="Display pills defined in ~/.paseo/top/pills as standalone composer pills"
1582
1599
  style={styles.toggleFullWidth}
@@ -1590,12 +1607,12 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1590
1607
  />
1591
1608
 
1592
1609
  </View>
1593
- </Card>
1610
+ </HostCard>
1594
1611
 
1595
1612
  {/* Rotation Speed Setting (Cycle mode only) */}
1596
1613
  {(settings.pillMode ?? "cycle") === "cycle" && (
1597
- <Card variant="elevated">
1598
- <CompactCardHeader
1614
+ <HostCard variant="elevated">
1615
+ <HostCardHeader {...compactCardTitleProp}
1599
1616
  title="Rotation Speed"
1600
1617
  icon="Clock"
1601
1618
  value={
@@ -1612,12 +1629,12 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1612
1629
  updateSettings({ intervalSeconds });
1613
1630
  }}
1614
1631
  />
1615
- </Card>
1632
+ </HostCard>
1616
1633
  )}
1617
1634
 
1618
1635
  {/* Timeline Cadence Setting */}
1619
- <Card variant="elevated">
1620
- <CompactCardHeader
1636
+ <HostCard variant="elevated">
1637
+ <HostCardHeader {...compactCardTitleProp}
1621
1638
  title="Timeline Cadence"
1622
1639
  icon="Clock"
1623
1640
  value={
@@ -1643,30 +1660,92 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1643
1660
  <Text style={[styles.modeDesc, styles.modeDescSpaced, { color: colors.foregroundMuted }]}>
1644
1661
  0 behaves as never; N above 1 stamps every Nth turn
1645
1662
  </Text>
1646
- </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
+ )}
1647
1726
 
1648
1727
  {/* Default Modal Tab Setting */}
1649
- <Card variant="elevated">
1650
- <CompactCardHeader
1728
+ <HostCard variant="elevated">
1729
+ <HostCardHeader {...compactCardTitleProp}
1651
1730
  title="Default Modal Tab"
1652
1731
  icon="Sliders"
1653
1732
  value={
1654
1733
  <Text style={[styles.accentValueText, { color: colors.accent }]}>
1655
- {TABS.find((t) => t.id === settings.defaultTab)?.label || "System"}
1734
+ {MODAL_TABS.find((t) => t.id === settings.defaultTab)?.label || "System"}
1656
1735
  </Text>
1657
1736
  }
1658
1737
  />
1659
1738
  <ChoiceChips
1660
- 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 }))}
1661
1740
  value={settings.defaultTab ?? "system"}
1662
1741
  onChange={(defaultTab) => {
1663
1742
  triggerHaptic("light");
1664
1743
  updateSettings({ defaultTab });
1665
1744
  }}
1666
1745
  />
1667
- </Card>
1746
+ </HostCard>
1668
1747
 
1669
- <Button
1748
+ <HostButton
1670
1749
  label="Reset to Defaults"
1671
1750
  variant="secondary"
1672
1751
  size="sm"
@@ -1681,40 +1760,19 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1681
1760
  )}
1682
1761
 
1683
1762
  {activeTab === "permissions" && (
1684
- <PermissionAuditView variant="compact" />
1763
+ <PermissionAuditView variant="compact" theme={theme} layout={layout} />
1685
1764
  )}
1686
1765
 
1687
1766
  {activeTab === "about" && (
1688
- <AboutSection
1767
+ <HostAboutSection
1689
1768
  name="paseo-top"
1690
- description="Live host system and workspace monitor for Paseo composer trackbar."
1769
+ description="Live host and session monitor across composer pills, timeline turns, and an interactive dashboard."
1691
1770
  version={data?.version ?? PLUGIN_VERSION}
1692
1771
  author="xpufx"
1693
1772
  repository="https://github.com/xpufx/paseo-top"
1694
1773
  issues="https://github.com/xpufx/paseo-top/issues"
1695
1774
  license="MIT"
1696
- density="tiny"
1697
- extraItems={[
1698
- {
1699
- label: "Host Platform",
1700
- value: data?.platform ? `${data?.platform} (${data?.arch ?? "unknown"})` : "Linux",
1701
- copyable: true,
1702
- },
1703
- { label: "Host Name", value: data?.hostname ?? "localhost", copyable: true },
1704
- { label: "CPU Model", value: data?.cpuModel ?? "unknown", copyable: true },
1705
- {
1706
- label: "CPU Cores",
1707
- value: `${data?.cpuCores ?? 0} cores`,
1708
- },
1709
- {
1710
- label: "Total Memory",
1711
- value: data?.memoryTotalBytes ? formatBytes(data?.memoryTotalBytes) : "unknown",
1712
- },
1713
- {
1714
- label: "Host Uptime",
1715
- value: data?.uptimeSeconds ? formatUptime(data?.uptimeSeconds) : "unknown",
1716
- },
1717
- ]}
1775
+ extraItems={aboutExtraItems(data)}
1718
1776
  />
1719
1777
  )}
1720
1778
 
@@ -1726,7 +1784,9 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
1726
1784
  </Text>
1727
1785
  </View>
1728
1786
  )}
1729
- </ModalBody>
1787
+ </View>
1788
+ </View>
1789
+ </HostModalSection>
1730
1790
  );
1731
1791
  }
1732
1792
 
@@ -1735,8 +1795,69 @@ interface LiveCustomPillModalProps {
1735
1795
  initial: CustomPillStateOutput;
1736
1796
  }
1737
1797
 
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");
1815
+
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
+ );
1857
+ }
1858
+
1738
1859
  function LiveCustomPillModal({ pillId, initial }: LiveCustomPillModalProps) {
1739
- const { colors } = usePluginTheme();
1860
+ const { colors } = useHostTheme();
1740
1861
  const [refreshing, setRefreshing] = useState(false);
1741
1862
  const [outputOverride, setOutputOverride] = useState<string | undefined>(undefined);
1742
1863
  const { data, refetch } = useCustomPillsQuery();
@@ -1871,17 +1992,7 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
1871
1992
  activePills.delete("paseo-top");
1872
1993
  }
1873
1994
 
1874
- const hasAny =
1875
- flags.showCpuRam ||
1876
- flags.showBranch ||
1877
- flags.showWorktree ||
1878
- flags.showAgentTitle ||
1879
- flags.showAgent ||
1880
- flags.showAgentProvider ||
1881
- flags.showAgentActivity ||
1882
- flags.showAgentId ||
1883
- flags.showLoad ||
1884
- flags.showUptime;
1995
+ const hasAny = hasAnyLegacyPillFlagEnabled(flags);
1885
1996
 
1886
1997
  const effectiveCpu = flags.showCpuRam || !hasAny;
1887
1998
 
@@ -2044,6 +2155,16 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
2044
2155
  defaultTab: "system",
2045
2156
  });
2046
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
+ }
2047
2168
 
2048
2169
  const desiredIds = new Set(desiredPills.map((p) => p.id));
2049
2170
 
@@ -2158,8 +2279,12 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
2158
2279
  );
2159
2280
  },
2160
2281
  refreshIntervalMs: 5000,
2161
- renderModal: () => (
2162
- <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>
2163
2288
  ),
2164
2289
  });
2165
2290
  activeCustomPills.set(pill.id, cleanup);
@@ -2213,8 +2338,6 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
2213
2338
  // primitive covers.
2214
2339
  const styles = {
2215
2340
  modalRoot: {
2216
- flex: 1,
2217
- minHeight: 0,
2218
2341
  width: "100%",
2219
2342
  },
2220
2343
  pillContainer: {
@@ -2313,80 +2436,6 @@ const styles = {
2313
2436
  accentValueText: {
2314
2437
  fontWeight: "600",
2315
2438
  },
2316
- metricMatrix: {
2317
- gap: 0,
2318
- width: "100%",
2319
- },
2320
- metricMatrixCompact: {
2321
- minWidth: 0,
2322
- },
2323
- metricMatrixHeader: {
2324
- flexDirection: "row",
2325
- alignItems: "center",
2326
- justifyContent: "space-between",
2327
- paddingLeft: 4,
2328
- paddingRight: 4,
2329
- paddingBottom: 2,
2330
- },
2331
- metricMatrixHeaderCompact: {
2332
- paddingLeft: 2,
2333
- paddingRight: 2,
2334
- },
2335
- metricMatrixHeaderText: {
2336
- fontSize: 9,
2337
- fontWeight: "700",
2338
- textTransform: "uppercase",
2339
- letterSpacing: 0.5,
2340
- },
2341
- metricMatrixTargetHeader: {
2342
- flexDirection: "row",
2343
- alignItems: "center",
2344
- gap: 28,
2345
- paddingRight: 10,
2346
- },
2347
- metricMatrixTargetHeaderCompact: {
2348
- gap: 16,
2349
- paddingRight: 8,
2350
- },
2351
- metricMatrixRow: {
2352
- flexDirection: "row",
2353
- alignItems: "center",
2354
- justifyContent: "space-between",
2355
- paddingLeft: 4,
2356
- paddingRight: 4,
2357
- },
2358
- metricMatrixRowCompact: {
2359
- paddingLeft: 2,
2360
- paddingRight: 2,
2361
- },
2362
- metricMatrixLabel: {
2363
- flex: 1,
2364
- minWidth: 0,
2365
- paddingVertical: 2,
2366
- },
2367
- metricMatrixTitle: {
2368
- fontSize: 11,
2369
- fontWeight: "600",
2370
- },
2371
- metricMatrixNote: {
2372
- fontSize: 9,
2373
- lineHeight: 12,
2374
- marginTop: 1,
2375
- },
2376
- metricMatrixTargets: {
2377
- flexDirection: "row",
2378
- alignItems: "center",
2379
- gap: 16,
2380
- paddingLeft: 8,
2381
- },
2382
- metricMatrixTargetsCompact: {
2383
- gap: 8,
2384
- paddingLeft: 4,
2385
- },
2386
- matrixToggle: {
2387
- width: 38,
2388
- minHeight: 44,
2389
- },
2390
2439
  allInOneContainer: {
2391
2440
  flexDirection: "row",
2392
2441
  alignItems: "center",
@@ -2439,4 +2488,31 @@ const styles = {
2439
2488
  fontStyle: "italic",
2440
2489
  paddingVertical: 4,
2441
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
+ },
2442
2518
  } as const;