@xpufx/paseo-top 0.4.3 → 0.4.5

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