@xpufx/paseo-top 0.4.4 → 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 (129) 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 +94 -1
  6. package/client/pill.tsx +690 -614
  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/package.json +7 -4
  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 +2 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  61. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  62. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  63. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  64. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  65. package/server/vendor/permission-audit/index.ts +1 -0
  66. package/server/vendor/permission-audit/server.ts +839 -0
  67. package/shared/helper-version.ts +1 -1
  68. package/shared/multi-host.ts +153 -159
  69. package/shared/resources.ts +145 -0
  70. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  71. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  72. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  73. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  74. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  75. package/shared/vendor/permission-audit/filter.ts +61 -0
  76. package/shared/vendor/permission-audit/index.ts +2 -0
  77. package/shared/vendor/permission-audit/shared.ts +119 -0
  78. package/shared/version.ts +1 -1
  79. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  80. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  81. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  82. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  83. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  84. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  85. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  86. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  87. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  88. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  89. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  90. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  91. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  92. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  93. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  94. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  95. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  96. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
  97. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  98. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  99. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  100. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  101. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  102. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  103. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  104. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
  105. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  106. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  107. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  108. package/client/vendor/paseo-plugin-helper/index.ts +0 -30
  109. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  110. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  111. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  112. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
  113. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  114. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  115. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  117. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  118. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  119. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  120. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  121. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  122. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  123. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  124. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  125. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  126. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  127. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  128. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
@@ -2,57 +2,54 @@ import React, { useState } from "react";
2
2
  import { Text, View } from "react-native";
3
3
  import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
4
4
  import {
5
- AboutSection,
6
- Button,
7
- Card,
8
- KeyValue,
9
- KeyValueGroup,
10
- MetricGauge,
11
- ModalBody,
12
- Row,
13
- Stack,
14
- Tabs,
15
- Toggle,
16
- CardHeader,
17
- usePluginSettings,
18
- usePluginTheme,
19
- } from "./vendor/paseo-plugin-helper/index";
5
+ HostAboutSection,
6
+ HostButton,
7
+ HostCard,
8
+ HostCardHeader,
9
+ HostGrid,
10
+ HostKeyValue,
11
+ HostLayoutProvider,
12
+ HostMetricGauge,
13
+ HostRow,
14
+ HostScroll,
15
+ HostStack,
16
+ HostTabs,
17
+ HostThemeProvider,
18
+ HostToggle,
19
+ } from "./host-ui";
20
+ import { usePluginSettings } from "./vendor/paseo-plugin-helper/core/index";
20
21
  import { formatBytes, formatUptime } from "../shared/vendor/paseo-plugin-helper/index";
21
- import { PermissionAuditView } from "permission-audit/client";
22
+ import { PermissionAuditView } from "./vendor/permission-audit";
22
23
  import {
23
24
  topSettingsContract,
24
25
  checkboxesFromTarget,
25
26
  targetFromCheckboxes,
27
+ isPillEnabled,
28
+ isTimelineEnabled,
26
29
  METRIC_DEFINITIONS,
30
+ CPU_THRESHOLDS,
31
+ MEM_THRESHOLDS,
32
+ SURFACE_TABS,
33
+ aboutExtraItems,
27
34
  type PillMode,
28
35
  type TopSettings,
29
36
  } from "../shared/resources";
30
37
  import { PLUGIN_VERSION } from "../shared/version";
31
38
  import { notifySettingsChanged } from "./settings-events";
32
- import { ChoiceChips } from "./settings-ui";
39
+ import { ChoiceChips, MultiChoiceChips } from "./settings-ui";
33
40
  import { useTopResourceQuery } from "./resources-query";
34
41
  import { FleetView } from "./multi-host-view";
35
42
 
36
43
  type SurfaceTab = "system" | "fleet" | "permissions" | "settings" | "about";
37
44
 
38
- const TABS = [
39
- { id: "system", label: "Activity", shortLabel: "Activity", icon: "Activity" },
40
- { id: "fleet", label: "Fleet", shortLabel: "Fleet", icon: "Server" },
41
- { id: "permissions", label: "Permissions", shortLabel: "Permissions", icon: "ShieldCheck" },
42
- { id: "settings", label: "Settings", shortLabel: "Settings", icon: "Sliders" },
43
- { id: "about", label: "About", shortLabel: "About", icon: "Info" },
44
- ];
45
-
46
45
  const TIMELINE_CADENCE_OPTIONS = Array.from({ length: 11 }, (_, n) => ({
47
46
  id: n,
48
47
  label: n === 0 ? "Never" : n === 1 ? "Every turn" : `${n}`,
49
48
  }));
50
49
 
51
- const CPU_THRESHOLDS = { warning: 60, danger: 85 };
52
- const MEM_THRESHOLDS = { warning: 70, danger: 85 };
53
-
54
- export function TopDashboardSurface(_props: PluginSurfaceProps) {
55
- const { colors } = usePluginTheme();
50
+ export function TopDashboardSurface(props: PluginSurfaceProps) {
51
+ const { theme, layout } = props;
52
+ const { colors } = theme;
56
53
  const [activeTab, setActiveTab] = useState<SurfaceTab>("system");
57
54
  // No background settings poll: mount/focus verification plus mutation
58
55
  // invalidation keep the dashboard in sync without daemon churn.
@@ -78,266 +75,297 @@ export function TopDashboardSurface(_props: PluginSurfaceProps) {
78
75
  };
79
76
 
80
77
  return (
81
- <View style={[styles.root, { backgroundColor: colors.surface0 }]}>
82
- <ModalBody
83
- headerMode="pinned"
84
- header={
85
- <Tabs
86
- tabs={TABS}
78
+ <HostThemeProvider theme={theme}>
79
+ <HostLayoutProvider layout={layout}>
80
+ <View style={[styles.root, { backgroundColor: colors.surface0 }]}>
81
+ {/*
82
+ * The sidebar host supplies no scroller for plugin surfaces, so the
83
+ * dashboard owns exactly one: HostScroll below the pinned tab
84
+ * strip (the header stays fixed because it sits outside the scroller).
85
+ */}
86
+ <HostTabs
87
+ tabs={[...SURFACE_TABS]}
87
88
  activeTab={activeTab}
88
89
  onTabChange={(id) => setActiveTab(id as SurfaceTab)}
89
90
  />
90
- }
91
- headerStyle={{
92
- backgroundColor: colors.surface0,
93
- paddingHorizontal: 12,
94
- paddingTop: 12,
95
- paddingBottom: 6,
96
- }}
97
- contentContainerStyle={{
98
- gap: 12,
99
- paddingHorizontal: 12,
100
- paddingBottom: 12,
101
- paddingTop: 6,
102
- }}
103
- refreshing={isLoading}
104
- >
105
- {activeTab === "system" && (
106
- <Stack gap={12}>
107
- <Text style={{ fontSize: 16, fontWeight: "700", color: colors.foreground }}>
108
- Host System Resources
109
- </Text>
91
+ <HostScroll
92
+ style={styles.scroll}
93
+ contentContainerStyle={{
94
+ gap: 12,
95
+ paddingHorizontal: 12,
96
+ paddingBottom: 12,
97
+ paddingTop: 6,
98
+ }}
99
+ >
100
+ {activeTab === "system" && (
101
+ <HostStack gap={12}>
102
+ <Text style={{ fontSize: 16, fontWeight: "700", color: colors.foreground }}>
103
+ Host System Resources
104
+ </Text>
110
105
 
111
- {pillHidden && (
112
- <Card variant="elevated">
113
- <Text style={{ fontSize: 12, fontWeight: "600", color: colors.foregroundMuted }}>
114
- Composer pill is hidden in trackbar. You can re-enable it from the Settings tab above.
115
- </Text>
116
- </Card>
117
- )}
106
+ {pillHidden && (
107
+ <HostCard variant="elevated">
108
+ <Text
109
+ style={{ fontSize: 12, fontWeight: "600", color: colors.foregroundMuted }}
110
+ >
111
+ Composer pill is hidden in trackbar. You can re-enable it from the Settings
112
+ tab above.
113
+ </Text>
114
+ </HostCard>
115
+ )}
118
116
 
119
- <Card variant="elevated">
120
- <Row justify="space-around" align="center" style={styles.gaugeContainer}>
121
- <MetricGauge
122
- value={data?.cpuUsagePercent ?? 0}
123
- thresholds={CPU_THRESHOLDS}
124
- label="CPU Load"
125
- size={72}
126
- />
127
- <MetricGauge
128
- value={data?.memoryUsedPercent ?? 0}
129
- thresholds={MEM_THRESHOLDS}
130
- label="RAM Used"
131
- size={72}
132
- />
133
- </Row>
134
- </Card>
117
+ <HostCard variant="elevated">
118
+ <HostRow justify="between" align="center" style={styles.gaugeContainer}>
119
+ <HostMetricGauge
120
+ value={data?.cpuUsagePercent ?? 0}
121
+ thresholds={CPU_THRESHOLDS}
122
+ label="CPU Load"
123
+ size={72}
124
+ />
125
+ <HostMetricGauge
126
+ value={data?.memoryUsedPercent ?? 0}
127
+ thresholds={MEM_THRESHOLDS}
128
+ label="RAM Used"
129
+ size={72}
130
+ />
131
+ </HostRow>
132
+ </HostCard>
135
133
 
136
- <Card variant="elevated">
137
- <KeyValueGroup>
138
- <KeyValue
139
- label="CPU"
140
- value={data?.cpuUsagePercent !== undefined ? `${data.cpuUsagePercent}%` : "--"}
141
- />
142
- <KeyValue
143
- label="Memory"
144
- value={
145
- data?.memoryUsedBytes !== undefined
146
- ? `${formatBytes(data.memoryUsedBytes, { compact: true, decimals: 1 })} (${data.memoryUsedPercent ?? "--"}%)`
147
- : "--"
148
- }
149
- />
150
- <KeyValue
151
- label="Load"
152
- value={data?.loadAvg?.[0] !== undefined ? data.loadAvg[0].toFixed(2) : "--"}
153
- />
154
- <KeyValue
155
- label="Uptime"
156
- value={data?.uptimeSeconds ? formatUptime(data.uptimeSeconds) : "--"}
157
- />
158
- <KeyValue label="Branch" value={data?.branch ?? "--"} copyable />
159
- <KeyValue
160
- label="MCP"
161
- value={
162
- data?.mcp && typeof data.mcp.total === "number"
163
- ? `${data.mcp.healthy}/${data.mcp.total} healthy`
164
- : "MCP --"
165
- }
166
- />
167
- </KeyValueGroup>
168
- </Card>
169
- {isLoading && !data ? (
170
- <Text style={{ fontSize: 12, color: colors.foregroundMuted }}>
171
- Loading live snapshot...
172
- </Text>
173
- ) : null}
174
- </Stack>
175
- )}
134
+ <HostCard variant="elevated">
135
+ <HostGrid columns={2} gap={12}>
136
+ <HostKeyValue
137
+ label="CPU"
138
+ value={data?.cpuUsagePercent !== undefined ? `${data.cpuUsagePercent}%` : "--"}
139
+ />
140
+ <HostKeyValue
141
+ label="Memory"
142
+ value={
143
+ data?.memoryUsedBytes !== undefined
144
+ ? `${formatBytes(data.memoryUsedBytes, { compact: true, decimals: 1 })} (${data.memoryUsedPercent ?? "--"}%)`
145
+ : "--"
146
+ }
147
+ />
148
+ <HostKeyValue
149
+ label="Load"
150
+ value={data?.loadAvg?.[0] !== undefined ? data.loadAvg[0].toFixed(2) : "--"}
151
+ />
152
+ <HostKeyValue
153
+ label="Uptime"
154
+ value={data?.uptimeSeconds ? formatUptime(data.uptimeSeconds) : "--"}
155
+ />
156
+ <HostKeyValue label="Branch" value={data?.branch ?? "--"} copyable />
157
+ <HostKeyValue
158
+ label="MCP"
159
+ value={
160
+ data?.mcp && typeof data.mcp.total === "number"
161
+ ? `${data.mcp.healthy}/${data.mcp.total} healthy`
162
+ : "MCP --"
163
+ }
164
+ />
165
+ </HostGrid>
166
+ </HostCard>
167
+ {isLoading && !data ? (
168
+ <Text style={{ fontSize: 12, color: colors.foregroundMuted }}>
169
+ Loading live snapshot...
170
+ </Text>
171
+ ) : null}
172
+ </HostStack>
173
+ )}
176
174
 
177
- {activeTab === "fleet" && <FleetView />}
175
+ {activeTab === "fleet" && <FleetView />}
178
176
 
179
- {activeTab === "permissions" && <PermissionAuditView variant="compact" />}
177
+ {activeTab === "permissions" && <PermissionAuditView variant="compact" theme={theme} layout={layout} />}
180
178
 
181
- {activeTab === "settings" && (
182
- <Stack gap={12}>
183
- <Card variant="elevated">
184
- <CardHeader
185
- title="Pill Display Mode"
186
- icon="LayoutGrid"
187
- subtitle="How active items appear in the composer trackbar"
188
- />
189
- <ChoiceChips
190
- options={[
191
- { id: "cycle", label: "Cycle", description: "Rotate one at a time" },
192
- { id: "all", label: "All in One", description: "Combined into one pill" },
193
- { id: "multiple", label: "Multiple", description: "Dedicated pills" },
194
- ]}
195
- value={settings.pillMode ?? "cycle"}
196
- showActiveDescription
197
- onChange={(pillMode: PillMode) => applySettings({ pillMode })}
198
- />
199
- <View style={styles.settingsSpacer}>
200
- <Toggle
201
- label="Show Composer Pill"
202
- description="Hide the composer pill entirely; the dashboard stays available from the sidebar"
203
- value={settings.showComposerPill ?? true}
204
- onValueChange={(val) => applySettings({ showComposerPill: val })}
205
- />
206
- </View>
207
- </Card>
208
-
209
- <Card variant="elevated">
210
- <CardHeader
211
- title="Metric Surfaces"
212
- icon="Sliders"
213
- subtitle="Choose where each metric appears (pill vs timeline)"
214
- />
215
- <Stack gap={12}>
216
- {METRIC_DEFINITIONS.map((def) => {
217
- const boxes = checkboxesFromTarget(settings.metricSurfaces?.[def.id]);
218
- const setBox = (which: "pill" | "timeline", val: boolean) => {
219
- const nextBoxes = { ...boxes, [which]: val };
220
- if (def.pillOnly) nextBoxes.timeline = false;
221
- applySettings({
222
- metricSurfaces: {
223
- ...settings.metricSurfaces,
224
- [def.id]: targetFromCheckboxes(nextBoxes.pill, nextBoxes.timeline),
225
- } as TopSettings["metricSurfaces"],
226
- });
227
- };
228
- return (
229
- <View key={def.id}>
230
- <Text style={{ fontSize: 11, fontWeight: "700", color: colors.foreground }}>
231
- {def.title}
232
- </Text>
233
- <Row gap={24} align="center" style={styles.metricTargets}>
234
- <Row gap={6} align="center">
235
- <Text style={[styles.metricTargetLabel, { color: colors.foreground }]}>
236
- Pill
237
- </Text>
238
- <Toggle value={boxes.pill} onValueChange={(val) => setBox("pill", val)} />
239
- </Row>
240
- <Row gap={6} align="center">
241
- <Text style={[styles.metricTargetLabel, { color: colors.foreground }]}>
242
- Timeline
243
- </Text>
244
- <Toggle
245
- value={boxes.timeline && !def.pillOnly}
246
- disabled={!!def.pillOnly}
247
- onValueChange={(val) => setBox("timeline", val)}
248
- />
249
- </Row>
250
- </Row>
179
+ {activeTab === "settings" && (
180
+ <HostStack gap={12}>
181
+ <HostCard variant="elevated">
182
+ <HostCardHeader
183
+ title="Pill Display Mode"
184
+ icon="LayoutGrid"
185
+ subtitle="How active items appear in the composer trackbar"
186
+ />
187
+ <ChoiceChips
188
+ options={[
189
+ { id: "cycle", label: "Cycle", description: "Rotate one at a time" },
190
+ { id: "all", label: "All in One", description: "Combined into one pill" },
191
+ { id: "multiple", label: "Multiple", description: "Dedicated pills" },
192
+ ]}
193
+ value={settings.pillMode ?? "cycle"}
194
+ showActiveDescription
195
+ onChange={(pillMode: PillMode) => applySettings({ pillMode })}
196
+ />
197
+ <View style={styles.settingsSpacer}>
198
+ <HostToggle
199
+ label="Show Composer Pill"
200
+ description="Hide the composer pill entirely; the dashboard stays available from the sidebar"
201
+ value={settings.showComposerPill ?? true}
202
+ onValueChange={(val) => applySettings({ showComposerPill: val })}
203
+ />
251
204
  </View>
252
- );
253
- })}
254
- </Stack>
255
- </Card>
205
+ </HostCard>
256
206
 
257
- <Card variant="elevated">
258
- <CardHeader
259
- title="Rotation Speed"
260
- icon="Clock"
261
- value={
262
- <Text style={{ color: colors.accent, fontWeight: "600" }}>
263
- {`${settings.intervalSeconds}s`}
264
- </Text>
265
- }
266
- />
267
- <ChoiceChips
268
- options={[2, 3, 4, 6].map((sec) => ({ id: sec, label: `${sec}s` }))}
269
- value={settings.intervalSeconds}
270
- onChange={(intervalSeconds) => applySettings({ intervalSeconds })}
271
- />
272
- </Card>
207
+ <HostCard variant="elevated">
208
+ <HostCardHeader
209
+ title="Active Pill Items"
210
+ icon="Sliders"
211
+ value={
212
+ <Text style={{ fontWeight: "600", color: colors.accent }}>
213
+ {`${METRIC_DEFINITIONS.filter((d) => isPillEnabled(settings.metricSurfaces?.[d.id])).length} active`}
214
+ </Text>
215
+ }
216
+ subtitle="Choose which items appear in the composer pill"
217
+ />
218
+ <MultiChoiceChips
219
+ options={METRIC_DEFINITIONS.map((def) => ({
220
+ id: def.id,
221
+ label: def.title,
222
+ icon: def.icon,
223
+ }))}
224
+ selected={METRIC_DEFINITIONS.filter((d) => isPillEnabled(settings.metricSurfaces?.[d.id])).map((d) => d.id)}
225
+ onToggle={(metricId) => {
226
+ const boxes = checkboxesFromTarget(settings.metricSurfaces?.[metricId]);
227
+ applySettings({
228
+ metricSurfaces: {
229
+ ...settings.metricSurfaces,
230
+ [metricId]: targetFromCheckboxes(!boxes.pill, boxes.timeline),
231
+ } as TopSettings["metricSurfaces"],
232
+ });
233
+ }}
234
+ onSelectAll={() => {
235
+ const next = { ...settings.metricSurfaces };
236
+ for (const def of METRIC_DEFINITIONS) {
237
+ const boxes = checkboxesFromTarget(settings.metricSurfaces?.[def.id]);
238
+ next[def.id] = targetFromCheckboxes(true, boxes.timeline);
239
+ }
240
+ applySettings({ metricSurfaces: next as TopSettings["metricSurfaces"] });
241
+ }}
242
+ onClearAll={() => {
243
+ const next = { ...settings.metricSurfaces };
244
+ for (const def of METRIC_DEFINITIONS) {
245
+ const boxes = checkboxesFromTarget(settings.metricSurfaces?.[def.id]);
246
+ next[def.id] = targetFromCheckboxes(false, boxes.timeline);
247
+ }
248
+ applySettings({ metricSurfaces: next as TopSettings["metricSurfaces"] });
249
+ }}
250
+ />
251
+ </HostCard>
273
252
 
274
- <Card variant="elevated">
275
- <CardHeader
276
- title="Timeline Cadence"
277
- icon="Clock"
278
- value={
279
- <Text style={{ color: colors.accent, fontWeight: "600" }}>
280
- {(settings.timelineCadence ?? 1) === 0
281
- ? "Never"
282
- : (settings.timelineCadence ?? 1) === 1
283
- ? "Every turn"
284
- : `Every ${(settings.timelineCadence ?? 1)} turns`}
285
- </Text>
286
- }
287
- subtitle="How often a card is stamped into the timeline view (0 = never)"
288
- />
289
- <ChoiceChips
290
- options={TIMELINE_CADENCE_OPTIONS}
291
- value={settings.timelineCadence ?? 1}
292
- onChange={(timelineCadence) => applySettings({ timelineCadence })}
293
- />
294
- </Card>
253
+ <HostCard variant="elevated">
254
+ <HostCardHeader
255
+ title="Timeline Card Metrics"
256
+ icon="GitCommitHorizontal"
257
+ value={
258
+ <Text style={{ fontWeight: "600", color: colors.accent }}>
259
+ {`${METRIC_DEFINITIONS.filter((d) => !d.pillOnly && isTimelineEnabled(settings.metricSurfaces?.[d.id])).length} active`}
260
+ </Text>
261
+ }
262
+ subtitle="Choose which metrics appear on turn timeline cards"
263
+ />
264
+ <MultiChoiceChips
265
+ options={METRIC_DEFINITIONS.filter((d) => !d.pillOnly).map((def) => ({
266
+ id: def.id,
267
+ label: def.title,
268
+ icon: def.icon,
269
+ }))}
270
+ selected={METRIC_DEFINITIONS.filter((d) => !d.pillOnly && isTimelineEnabled(settings.metricSurfaces?.[d.id])).map((d) => d.id)}
271
+ onToggle={(metricId) => {
272
+ const boxes = checkboxesFromTarget(settings.metricSurfaces?.[metricId]);
273
+ applySettings({
274
+ metricSurfaces: {
275
+ ...settings.metricSurfaces,
276
+ [metricId]: targetFromCheckboxes(boxes.pill, !boxes.timeline),
277
+ } as TopSettings["metricSurfaces"],
278
+ });
279
+ }}
280
+ onSelectAll={() => {
281
+ const next = { ...settings.metricSurfaces };
282
+ for (const def of METRIC_DEFINITIONS) {
283
+ if (def.pillOnly) continue;
284
+ const boxes = checkboxesFromTarget(settings.metricSurfaces?.[def.id]);
285
+ next[def.id] = targetFromCheckboxes(boxes.pill, true);
286
+ }
287
+ applySettings({ metricSurfaces: next as TopSettings["metricSurfaces"] });
288
+ }}
289
+ onClearAll={() => {
290
+ const next = { ...settings.metricSurfaces };
291
+ for (const def of METRIC_DEFINITIONS) {
292
+ if (def.pillOnly) continue;
293
+ const boxes = checkboxesFromTarget(settings.metricSurfaces?.[def.id]);
294
+ next[def.id] = targetFromCheckboxes(boxes.pill, false);
295
+ }
296
+ applySettings({ metricSurfaces: next as TopSettings["metricSurfaces"] });
297
+ }}
298
+ />
299
+ </HostCard>
295
300
 
296
- <Button
297
- label="Reset to Defaults"
298
- variant="secondary"
299
- size="sm"
300
- onPress={() => {
301
- void resetSettings().then((defaults) => {
302
- notifySettingsChanged(defaults ?? topSettingsContract.defaultSettings);
303
- });
304
- }}
305
- />
306
- </Stack>
307
- )}
301
+ <HostCard variant="elevated">
302
+ <HostCardHeader
303
+ title="Rotation Speed"
304
+ icon="Clock"
305
+ value={
306
+ <Text style={{ color: colors.accent, fontWeight: "600" }}>
307
+ {`${settings.intervalSeconds}s`}
308
+ </Text>
309
+ }
310
+ />
311
+ <ChoiceChips
312
+ options={[2, 3, 4, 6].map((sec) => ({ id: sec, label: `${sec}s` }))}
313
+ value={settings.intervalSeconds}
314
+ onChange={(intervalSeconds) => applySettings({ intervalSeconds })}
315
+ />
316
+ </HostCard>
308
317
 
309
- {activeTab === "about" && (
310
- <AboutSection
311
- name="paseo-top"
312
- description="Live host system and workspace monitor for Paseo composer trackbar."
313
- version={data?.version ?? PLUGIN_VERSION}
314
- author="xpufx"
315
- repository="https://github.com/xpufx/paseo-top"
316
- issues="https://github.com/xpufx/paseo-top/issues"
317
- license="MIT"
318
- density="tiny"
319
- extraItems={[
320
- {
321
- label: "Host Platform",
322
- value: data?.platform ? `${data.platform} (${data.arch ?? "unknown"})` : "Linux",
323
- copyable: true,
324
- },
325
- { label: "Host Name", value: data?.hostname ?? "localhost", copyable: true },
326
- { label: "CPU Model", value: data?.cpuModel ?? "unknown", copyable: true },
327
- { label: "CPU Cores", value: `${data?.cpuCores ?? 0} cores` },
328
- {
329
- label: "Total Memory",
330
- value: data?.memoryTotalBytes ? formatBytes(data.memoryTotalBytes) : "unknown",
331
- },
332
- {
333
- label: "Host Uptime",
334
- value: data?.uptimeSeconds ? formatUptime(data.uptimeSeconds) : "unknown",
335
- },
336
- ]}
337
- />
338
- )}
339
- </ModalBody>
340
- </View>
318
+ <HostCard variant="elevated">
319
+ <HostCardHeader
320
+ title="Timeline Cadence"
321
+ icon="Clock"
322
+ value={
323
+ <Text style={{ color: colors.accent, fontWeight: "600" }}>
324
+ {(settings.timelineCadence ?? 1) === 0
325
+ ? "Never"
326
+ : (settings.timelineCadence ?? 1) === 1
327
+ ? "Every turn"
328
+ : `Every ${(settings.timelineCadence ?? 1)} turns`}
329
+ </Text>
330
+ }
331
+ subtitle="How often a card is stamped into the timeline view (0 = never)"
332
+ />
333
+ <ChoiceChips
334
+ options={TIMELINE_CADENCE_OPTIONS}
335
+ value={settings.timelineCadence ?? 1}
336
+ onChange={(timelineCadence) => applySettings({ timelineCadence })}
337
+ />
338
+ </HostCard>
339
+
340
+ <HostButton
341
+ label="Reset to Defaults"
342
+ variant="secondary"
343
+ size="sm"
344
+ onPress={() => {
345
+ void resetSettings().then((defaults) => {
346
+ notifySettingsChanged(defaults ?? topSettingsContract.defaultSettings);
347
+ });
348
+ }}
349
+ />
350
+ </HostStack>
351
+ )}
352
+
353
+ {activeTab === "about" && (
354
+ <HostAboutSection
355
+ name="paseo-top"
356
+ description="Live host system and workspace monitor for Paseo composer trackbar."
357
+ version={data?.version ?? PLUGIN_VERSION}
358
+ author="xpufx"
359
+ repository="https://github.com/xpufx/paseo-top"
360
+ issues="https://github.com/xpufx/paseo-top/issues"
361
+ license="MIT"
362
+ extraItems={aboutExtraItems(data)}
363
+ />
364
+ )}
365
+ </HostScroll>
366
+ </View>
367
+ </HostLayoutProvider>
368
+ </HostThemeProvider>
341
369
  );
342
370
  }
343
371
 
@@ -347,6 +375,10 @@ const styles = {
347
375
  minHeight: 0,
348
376
  width: "100%",
349
377
  },
378
+ scroll: {
379
+ flex: 1,
380
+ minHeight: 0,
381
+ },
350
382
  gaugeContainer: {
351
383
  paddingVertical: 12,
352
384
  width: "100%",