@xpufx/paseo-top 0.4.2 → 0.4.4
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.
- package/README.md +11 -12
- package/client/multi-host-view.tsx +177 -0
- package/client/multi-host.ts +98 -0
- package/client/pill-labels.ts +30 -6
- package/client/pill.tsx +42 -61
- package/client/settings-events.ts +33 -0
- package/client/surface.tsx +10 -9
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +12 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +29 -3
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +32 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +17 -1
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +17 -2
- package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +142 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +16 -2
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +7 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +131 -63
- package/client/vendor/paseo-plugin-helper/components/index.ts +1 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +4 -3
- package/client/vendor/paseo-plugin-helper/host.ts +12 -58
- package/client/vendor/paseo-plugin-helper/index.ts +2 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +19 -29
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +24 -22
- package/client/vendor/paseo-plugin-helper/panel.tsx +11 -3
- package/client/vendor/paseo-plugin-helper/pill.tsx +135 -339
- package/client/vendor/paseo-plugin-helper/styles/index.ts +1 -0
- package/client/vendor/paseo-plugin-helper/styles/recipes.ts +550 -0
- package/client/vendor/paseo-plugin-helper/surface.tsx +62 -2
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +3 -39
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +40 -1
- package/client/vendor/paseo-plugin-helper/tickets.tsx +766 -0
- package/index.server.ts +7 -0
- package/package.json +12 -3
- package/paseo-plugin.json +1 -1
- package/server/vendor/paseo-plugin-helper/index.ts +2 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
- package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
- package/server/vendor/paseo-plugin-helper/tickets.ts +1281 -0
- package/shared/helper-version.ts +20 -0
- package/shared/multi-host.ts +348 -0
- package/shared/resources.ts +1 -1
- package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
- package/shared/vendor/paseo-plugin-helper/tickets.ts +1010 -0
- package/shared/version.ts +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# paseo-top
|
|
2
2
|
|
|
3
|
-
Live host system resource monitor and telemetry provider for [Paseo](https://github.com/getpaseo/paseo)
|
|
3
|
+
Live host system resource monitor and telemetry provider for [Paseo](https://github.com/getpaseo/paseo).
|
|
4
4
|
|
|
5
5
|
Displays real-time host metrics, session metadata, turn telemetry, and custom pill widgets directly in the composer track bar without cluttering the interface. Automatically stamps performance summaries into agent conversation timelines and provides an interactive modal dashboard with live gauges.
|
|
6
6
|
|
|
@@ -12,11 +12,11 @@ Built on [paseo-plugin-helper](https://github.com/xpufx/paseo/tree/main/packages
|
|
|
12
12
|
|
|
13
13
|
| General Composer View | Dashboard View |
|
|
14
14
|
| :---: | :---: |
|
|
15
|
-
| [](screenshots/paseo-top-general-view.jpg) | [](screenshots/paseo-top-dashboard.jpg) |
|
|
15
|
+
| [](https://raw.githubusercontent.com/xpufx/paseo/main/plugins/top/screenshots/paseo-top-general-view.jpg) | [](https://raw.githubusercontent.com/xpufx/paseo/main/plugins/top/screenshots/paseo-top-dashboard.jpg) |
|
|
16
16
|
|
|
17
17
|
| System Resources Modal | Workspace Context Modal | Settings Modal | About Modal |
|
|
18
18
|
| :---: | :---: | :---: | :---: |
|
|
19
|
-
| <img src="screenshots/1-paseo-top-modal-system.jpg" alt="System Resources Modal" width="100%" /> | <img src="screenshots/2-paseo-top-modal-workspace.jpg" alt="Workspace Context Modal" width="100%" /> | <img src="screenshots/3-paseo-top-modal-settings.jpg" alt="Settings Modal" width="100%" /> | <img src="screenshots/4-paseo-top-modal-about.jpg" alt="About Modal" width="100%" /> |
|
|
19
|
+
| <img src="https://raw.githubusercontent.com/xpufx/paseo/main/plugins/top/screenshots/1-paseo-top-modal-system.jpg" alt="System Resources Modal" width="100%" /> | <img src="https://raw.githubusercontent.com/xpufx/paseo/main/plugins/top/screenshots/2-paseo-top-modal-workspace.jpg" alt="Workspace Context Modal" width="100%" /> | <img src="https://raw.githubusercontent.com/xpufx/paseo/main/plugins/top/screenshots/3-paseo-top-modal-settings.jpg" alt="Settings Modal" width="100%" /> | <img src="https://raw.githubusercontent.com/xpufx/paseo/main/plugins/top/screenshots/4-paseo-top-modal-about.jpg" alt="About Modal" width="100%" /> |
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
## Core Capabilities
|
|
@@ -45,6 +45,7 @@ Built on [paseo-plugin-helper](https://github.com/xpufx/paseo/tree/main/packages
|
|
|
45
45
|
|
|
46
46
|
### 4. Fleet & MCP Integration
|
|
47
47
|
- **MCP Server Health**: Bridges with `mcp-tools` to monitor connected MCP servers, distinguishing healthy, degraded, or offline servers alongside latency figures.
|
|
48
|
+
- **Multi-Host Fleet View**: The dashboard **Fleet** tab enumerates hosts from `useHosts()` and shows one card per host with a status badge (`online` / `stale` / `offline` / `error`), probe latency, and per-host counts. Counts are summed across responsive hosts; CPU/RAM/load stay per-host and are never aggregated. Polling is bounded to one in-flight probe per host on a 15s cadence with a 4s per-host timeout that marks the host `stale` until the next tick. No remote shell or custom-command execution is performed.
|
|
48
49
|
|
|
49
50
|
### 5. Flexible Display Modes
|
|
50
51
|
- **Cycle Mode**: Automatically cycles through enabled metric segments on each refresh interval.
|
|
@@ -75,24 +76,22 @@ Add user-defined metrics by dropping declarative `.jsonc` definitions into `~/.p
|
|
|
75
76
|
|
|
76
77
|
### 8. Native Modal Dashboard
|
|
77
78
|
Clicking the pill opens a native dashboard with:
|
|
78
|
-
- **Pinned Header Navbar**: Fixed tabs for System Resources, Activity Timeline, Custom Pills, Settings, and About. Content smoothly scrolls beneath the navbar.
|
|
79
|
+
- **Pinned Header Navbar**: Fixed tabs for System Resources, Fleet, Activity Timeline, Custom Pills, Settings, and About. Content smoothly scrolls beneath the navbar.
|
|
79
80
|
- **Visual Gauges**: Responsive bars showing CPU and RAM distribution.
|
|
80
81
|
- **Full Settings Suite**: Live toggles for every metric and pill, with persistent state managed by `PluginStorage`.
|
|
81
82
|
|
|
82
83
|
## Installation
|
|
83
84
|
|
|
84
|
-
Install
|
|
85
|
+
Install from npm:
|
|
85
86
|
|
|
86
|
-
```
|
|
87
|
-
paseo plugin add xpufx/paseo
|
|
87
|
+
```sh
|
|
88
|
+
paseo plugin add npm:@xpufx/paseo-top
|
|
88
89
|
```
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
Or install directly from the Git repository:
|
|
91
92
|
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
cd paseo
|
|
95
|
-
paseo plugin add ./plugins/top
|
|
93
|
+
```sh
|
|
94
|
+
paseo plugin add xpufx/paseo --path plugins/top
|
|
96
95
|
```
|
|
97
96
|
|
|
98
97
|
## Development
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
Badge,
|
|
5
|
+
Card,
|
|
6
|
+
CardHeader,
|
|
7
|
+
EmptyState,
|
|
8
|
+
KeyValue,
|
|
9
|
+
KeyValueGroup,
|
|
10
|
+
Row,
|
|
11
|
+
SectionHeader,
|
|
12
|
+
Stack,
|
|
13
|
+
StatusDot,
|
|
14
|
+
usePluginTheme,
|
|
15
|
+
} from "./vendor/paseo-plugin-helper/index";
|
|
16
|
+
import {
|
|
17
|
+
aggregateFleet,
|
|
18
|
+
type FleetHostSnapshot,
|
|
19
|
+
type FleetHostStatus,
|
|
20
|
+
} from "../shared/multi-host";
|
|
21
|
+
import { useFleetPolling } from "./multi-host";
|
|
22
|
+
|
|
23
|
+
const STATUS_VARIANT: Record<
|
|
24
|
+
FleetHostStatus,
|
|
25
|
+
"success" | "warning" | "neutral" | "danger"
|
|
26
|
+
> = {
|
|
27
|
+
online: "success",
|
|
28
|
+
stale: "warning",
|
|
29
|
+
offline: "neutral",
|
|
30
|
+
error: "danger",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const STATUS_LABEL: Record<FleetHostStatus, string> = {
|
|
34
|
+
online: "online",
|
|
35
|
+
stale: "stale",
|
|
36
|
+
offline: "offline",
|
|
37
|
+
error: "error",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function formatLatency(latencyMs: number | null): string {
|
|
41
|
+
if (latencyMs === null) return "--";
|
|
42
|
+
if (latencyMs < 1000) return `${Math.round(latencyMs)} ms`;
|
|
43
|
+
return `${(latencyMs / 1000).toFixed(2)} s`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatStaleAge(
|
|
47
|
+
staleAt: number | null,
|
|
48
|
+
now: number,
|
|
49
|
+
): string | null {
|
|
50
|
+
if (staleAt === null) return null;
|
|
51
|
+
const seconds = Math.max(0, Math.round((now - staleAt) / 1000));
|
|
52
|
+
return `stale ${seconds}s`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function FleetHostCard({
|
|
56
|
+
snapshot,
|
|
57
|
+
now,
|
|
58
|
+
}: {
|
|
59
|
+
snapshot: FleetHostSnapshot;
|
|
60
|
+
now: number;
|
|
61
|
+
}) {
|
|
62
|
+
const { colors } = usePluginTheme();
|
|
63
|
+
const variant = STATUS_VARIANT[snapshot.status];
|
|
64
|
+
const staleAge = formatStaleAge(snapshot.staleAt, now);
|
|
65
|
+
const subtitle =
|
|
66
|
+
snapshot.status === "online"
|
|
67
|
+
? "Connected"
|
|
68
|
+
: (staleAge ?? snapshot.error ?? "Not connected");
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Card variant="elevated">
|
|
72
|
+
<CardHeader
|
|
73
|
+
icon="Server"
|
|
74
|
+
title={snapshot.label}
|
|
75
|
+
subtitle={subtitle}
|
|
76
|
+
badge={
|
|
77
|
+
<Row gap={6} align="center">
|
|
78
|
+
<StatusDot
|
|
79
|
+
variant={snapshot.status === "online" ? "success" : variant}
|
|
80
|
+
pulse={snapshot.status === "online"}
|
|
81
|
+
/>
|
|
82
|
+
<Badge
|
|
83
|
+
label={STATUS_LABEL[snapshot.status]}
|
|
84
|
+
variant={variant}
|
|
85
|
+
size="sm"
|
|
86
|
+
/>
|
|
87
|
+
</Row>
|
|
88
|
+
}
|
|
89
|
+
/>
|
|
90
|
+
<KeyValueGroup>
|
|
91
|
+
<KeyValue label="Host" value={snapshot.serverId} truncate="middle" copyable />
|
|
92
|
+
<KeyValue label="Latency" value={formatLatency(snapshot.latencyMs)} />
|
|
93
|
+
<KeyValue
|
|
94
|
+
label="Agents"
|
|
95
|
+
value={snapshot.counts ? String(snapshot.counts.agents) : "--"}
|
|
96
|
+
/>
|
|
97
|
+
<KeyValue
|
|
98
|
+
label="Active agents"
|
|
99
|
+
value={snapshot.counts ? String(snapshot.counts.activeAgents) : "--"}
|
|
100
|
+
/>
|
|
101
|
+
<KeyValue
|
|
102
|
+
label="Workspaces"
|
|
103
|
+
value={snapshot.counts ? String(snapshot.counts.workspaces) : "--"}
|
|
104
|
+
/>
|
|
105
|
+
</KeyValueGroup>
|
|
106
|
+
{snapshot.status === "error" && snapshot.error ? (
|
|
107
|
+
<Text style={{ marginTop: 8, fontSize: 11, color: colors.statusDanger }}>
|
|
108
|
+
{snapshot.error}
|
|
109
|
+
</Text>
|
|
110
|
+
) : null}
|
|
111
|
+
</Card>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Multi-host Fleet view. Renders one card per configured host with a status
|
|
117
|
+
* badge and per-host counts, plus a counts-only aggregate. Local Top gauges
|
|
118
|
+
* (CPU/RAM) stay in the Activity tab and are never summed or averaged here.
|
|
119
|
+
*
|
|
120
|
+
* Mounting is the lifecycle: this component is rendered only while the Fleet
|
|
121
|
+
* tab is active, so it starts polling on mount and disposes on unmount. Local
|
|
122
|
+
* Top behavior is unaffected whenever the tab is not selected.
|
|
123
|
+
*/
|
|
124
|
+
export function FleetView() {
|
|
125
|
+
const { colors } = usePluginTheme();
|
|
126
|
+
const snapshots = useFleetPolling();
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
const aggregate = useMemo(() => aggregateFleet(snapshots), [snapshots]);
|
|
129
|
+
|
|
130
|
+
if (snapshots.length === 0) {
|
|
131
|
+
return (
|
|
132
|
+
<EmptyState
|
|
133
|
+
icon="Server"
|
|
134
|
+
title="No other hosts"
|
|
135
|
+
description="Configure additional hosts in Paseo to see their fleet status here."
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Stack gap={12}>
|
|
142
|
+
<Text style={{ fontSize: 16, fontWeight: "700", color: colors.foreground }}>
|
|
143
|
+
Multi-Host Fleet
|
|
144
|
+
</Text>
|
|
145
|
+
|
|
146
|
+
<Card variant="elevated">
|
|
147
|
+
<CardHeader
|
|
148
|
+
title="Fleet Totals"
|
|
149
|
+
icon="Activity"
|
|
150
|
+
subtitle="Counts only. CPU/RAM/load stay per-host."
|
|
151
|
+
/>
|
|
152
|
+
<KeyValueGroup>
|
|
153
|
+
<KeyValue label="Hosts" value={`${aggregate.responsive}/${aggregate.hosts} responsive`} />
|
|
154
|
+
<KeyValue
|
|
155
|
+
label="Online / Stale"
|
|
156
|
+
value={`${aggregate.online} / ${aggregate.stale}`}
|
|
157
|
+
/>
|
|
158
|
+
<KeyValue label="Agents" value={String(aggregate.counts.agents)} />
|
|
159
|
+
<KeyValue
|
|
160
|
+
label="Active agents"
|
|
161
|
+
value={String(aggregate.counts.activeAgents)}
|
|
162
|
+
/>
|
|
163
|
+
<KeyValue label="Workspaces" value={String(aggregate.counts.workspaces)} />
|
|
164
|
+
</KeyValueGroup>
|
|
165
|
+
</Card>
|
|
166
|
+
|
|
167
|
+
<SectionHeader title="Hosts" count={snapshots.length} />
|
|
168
|
+
{snapshots.map((snapshot) => (
|
|
169
|
+
<FleetHostCard
|
|
170
|
+
key={snapshot.serverId}
|
|
171
|
+
snapshot={snapshot}
|
|
172
|
+
now={now}
|
|
173
|
+
/>
|
|
174
|
+
))}
|
|
175
|
+
</Stack>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { getPaseoClient, useHosts } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
MultiHostPoller,
|
|
5
|
+
type FleetHostClient,
|
|
6
|
+
type FleetHostCounts,
|
|
7
|
+
type FleetHostSnapshot,
|
|
8
|
+
} from "../shared/multi-host";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wrap a borrowed `getPaseoClient(serverId)` as a fleet client.
|
|
12
|
+
*
|
|
13
|
+
* Only counts are read. The borrowed API exposes agents/workspaces/projects/
|
|
14
|
+
* terminals/providers/config and no host-metrics RPC, so the fleet view cannot
|
|
15
|
+
* show remote CPU/RAM/load/uptime; fabricating them would be mock data.
|
|
16
|
+
*/
|
|
17
|
+
export function createFleetClient(serverId: string): FleetHostClient {
|
|
18
|
+
// getPaseoClient throws for a non-online/unknown host; the poller only
|
|
19
|
+
// acquires for online hosts, and the throw is caught per host so one bad
|
|
20
|
+
// host cannot break the others.
|
|
21
|
+
const client = getPaseoClient(serverId);
|
|
22
|
+
return {
|
|
23
|
+
async readCounts(): Promise<FleetHostCounts> {
|
|
24
|
+
const [agents, workspaces] = await Promise.all([
|
|
25
|
+
client.agents.list(),
|
|
26
|
+
client.workspaces.list(),
|
|
27
|
+
]);
|
|
28
|
+
const entries = agents.entries ?? [];
|
|
29
|
+
return {
|
|
30
|
+
agents: entries.length,
|
|
31
|
+
activeAgents: entries.filter((entry) => entry.agent?.status === "running")
|
|
32
|
+
.length,
|
|
33
|
+
workspaces: workspaces.entries?.length ?? 0,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
dispose() {
|
|
37
|
+
return client.dispose();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UseFleetPollingOptions {
|
|
43
|
+
/** Poll only while the fleet view is visible; keeps local-only paths idle. */
|
|
44
|
+
enabled?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Bounded multi-host fleet polling hook.
|
|
49
|
+
*
|
|
50
|
+
* Enumerates online hosts with `useHosts()`, acquires a borrowed
|
|
51
|
+
* `getPaseoClient(serverId)` per online host, and drives the pure
|
|
52
|
+
* {@link MultiHostPoller} state machine: max one in-flight probe per host,
|
|
53
|
+
* 15s cadence, 4s per-host timeout, stale marking, and dispose/reacquire on
|
|
54
|
+
* host status transitions. Local Top behavior is untouched because nothing
|
|
55
|
+
* starts unless the fleet view is mounted and enabled.
|
|
56
|
+
*/
|
|
57
|
+
export function useFleetPolling({ enabled = true }: UseFleetPollingOptions = {}) {
|
|
58
|
+
const hosts = useHosts();
|
|
59
|
+
const [snapshots, setSnapshots] = useState<FleetHostSnapshot[]>([]);
|
|
60
|
+
const pollerRef = useRef<MultiHostPoller | null>(null);
|
|
61
|
+
|
|
62
|
+
const hostInputs = useMemo(
|
|
63
|
+
() =>
|
|
64
|
+
hosts.map((host) => ({
|
|
65
|
+
serverId: host.serverId,
|
|
66
|
+
label: host.label,
|
|
67
|
+
status: host.status,
|
|
68
|
+
})),
|
|
69
|
+
[hosts],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Keep the latest host list in a ref so the create-once effect can seed from
|
|
73
|
+
// it without re-subscribing on every status change.
|
|
74
|
+
const hostInputsRef = useRef(hostInputs);
|
|
75
|
+
hostInputsRef.current = hostInputs;
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!enabled) return;
|
|
79
|
+
const poller = new MultiHostPoller({
|
|
80
|
+
acquire: createFleetClient,
|
|
81
|
+
onUpdate: setSnapshots,
|
|
82
|
+
});
|
|
83
|
+
pollerRef.current = poller;
|
|
84
|
+
poller.setHosts(hostInputsRef.current);
|
|
85
|
+
poller.start();
|
|
86
|
+
return () => {
|
|
87
|
+
poller.dispose();
|
|
88
|
+
pollerRef.current = null;
|
|
89
|
+
setSnapshots([]);
|
|
90
|
+
};
|
|
91
|
+
}, [enabled]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
pollerRef.current?.setHosts(hostInputs);
|
|
95
|
+
}, [hostInputs]);
|
|
96
|
+
|
|
97
|
+
return snapshots;
|
|
98
|
+
}
|
package/client/pill-labels.ts
CHANGED
|
@@ -56,6 +56,20 @@ export interface SegmentSnapshot {
|
|
|
56
56
|
worktreeLocationText?: string;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* First non-blank string wins, else the fallback. `??` only guards null and
|
|
61
|
+
* undefined, so a present-but-empty host field (blank `title`, `provider`,
|
|
62
|
+
* `branch`, `agentId`) slipped through as an empty label. On a button-shaped
|
|
63
|
+
* host an empty label renders icon-only, which reads as the pill collapsing to
|
|
64
|
+
* a skinny/empty line (xpufx-org/paseo#507).
|
|
65
|
+
*/
|
|
66
|
+
function firstNonBlank(fallback: string, ...values: Array<string | null | undefined>): string {
|
|
67
|
+
for (const value of values) {
|
|
68
|
+
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
69
|
+
}
|
|
70
|
+
return fallback;
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
export interface TokenMetrics {
|
|
60
74
|
inputTokens?: number;
|
|
61
75
|
outputTokens?: number;
|
|
@@ -262,28 +276,38 @@ export function describeSegment(
|
|
|
262
276
|
const prefix = def?.shortLabel ? `${def.shortLabel} ` : "";
|
|
263
277
|
switch (item) {
|
|
264
278
|
case "branch":
|
|
265
|
-
return {
|
|
279
|
+
return {
|
|
280
|
+
icon,
|
|
281
|
+
iconTone: "accent",
|
|
282
|
+
text: firstNonBlank("--", data?.branch),
|
|
283
|
+
tone: "foreground",
|
|
284
|
+
};
|
|
266
285
|
case "worktree":
|
|
267
286
|
return {
|
|
268
287
|
icon,
|
|
269
288
|
iconTone: "accent",
|
|
270
|
-
text:
|
|
289
|
+
text: firstNonBlank("--", worktreeLocationText),
|
|
271
290
|
tone: "foreground",
|
|
272
291
|
};
|
|
273
292
|
case "agent_title":
|
|
274
|
-
return {
|
|
293
|
+
return {
|
|
294
|
+
icon,
|
|
295
|
+
iconTone: "accent",
|
|
296
|
+
text: firstNonBlank("Agent", agent?.title),
|
|
297
|
+
tone: "foreground",
|
|
298
|
+
};
|
|
275
299
|
case "agent":
|
|
276
300
|
return {
|
|
277
301
|
icon,
|
|
278
302
|
iconTone: "accent",
|
|
279
|
-
text: agent?.model
|
|
303
|
+
text: firstNonBlank("Agent", agent?.model, agent?.provider),
|
|
280
304
|
tone: "foreground",
|
|
281
305
|
};
|
|
282
306
|
case "agent_provider":
|
|
283
307
|
return {
|
|
284
308
|
icon,
|
|
285
309
|
iconTone: "accent",
|
|
286
|
-
text:
|
|
310
|
+
text: firstNonBlank("Provider", agent?.provider),
|
|
287
311
|
tone: "foreground",
|
|
288
312
|
};
|
|
289
313
|
case "agent_activity": {
|
|
@@ -299,7 +323,7 @@ export function describeSegment(
|
|
|
299
323
|
return {
|
|
300
324
|
icon,
|
|
301
325
|
iconTone: "accent",
|
|
302
|
-
text: agentId && agentId.length > 7 ? agentId.slice(0, 7) :
|
|
326
|
+
text: agentId && agentId.length > 7 ? agentId.slice(0, 7) : firstNonBlank("--", agentId),
|
|
303
327
|
tone: "foreground",
|
|
304
328
|
};
|
|
305
329
|
case "load":
|
package/client/pill.tsx
CHANGED
|
@@ -28,16 +28,14 @@ import {
|
|
|
28
28
|
Badge,
|
|
29
29
|
Icon,
|
|
30
30
|
AboutSection,
|
|
31
|
-
CustomPillBody,
|
|
32
31
|
CustomPillModalContent,
|
|
33
32
|
useRpcQuery,
|
|
34
33
|
usePluginSettings,
|
|
35
34
|
usePluginTheme,
|
|
36
35
|
getStatusColor,
|
|
37
36
|
triggerHaptic,
|
|
38
|
-
|
|
37
|
+
type HostPillProps,
|
|
39
38
|
type RenderModalProps,
|
|
40
|
-
type RenderPillProps,
|
|
41
39
|
type KeyValueProps,
|
|
42
40
|
type CardHeaderProps,
|
|
43
41
|
type BadgeProps,
|
|
@@ -47,8 +45,8 @@ import {
|
|
|
47
45
|
formatUptime,
|
|
48
46
|
resolveMetricStatus,
|
|
49
47
|
type MetricThresholds,
|
|
50
|
-
type CustomPillState,
|
|
51
48
|
} from "../shared/vendor/paseo-plugin-helper/index";
|
|
49
|
+
import { PermissionAuditView } from "permission-audit/client";
|
|
52
50
|
import {
|
|
53
51
|
getSystemResourcesRpc,
|
|
54
52
|
topSettingsContract,
|
|
@@ -72,6 +70,7 @@ import {
|
|
|
72
70
|
} from "../shared/resources";
|
|
73
71
|
import { PLUGIN_VERSION } from "../shared/version";
|
|
74
72
|
import { TopDashboardSurface } from "./surface";
|
|
73
|
+
import { addSettingsListener, removeSettingsListener, notifySettingsChanged } from "./settings-events";
|
|
75
74
|
import { useTopResourceQuery, useCustomPillsQuery } from "./resources-query";
|
|
76
75
|
import { ChoiceChips } from "./settings-ui";
|
|
77
76
|
import {
|
|
@@ -140,6 +139,7 @@ const MEM_THRESHOLDS: MetricThresholds = { warning: 70, danger: 85 };
|
|
|
140
139
|
const TABS = [
|
|
141
140
|
{ id: "system", label: "System", shortLabel: "System", icon: "Activity" },
|
|
142
141
|
{ id: "context", label: "Workspace", shortLabel: "Workspace", icon: "GitBranch" },
|
|
142
|
+
{ id: "permissions", label: "Permissions", shortLabel: "Permissions", icon: "ShieldCheck" },
|
|
143
143
|
{ id: "settings", label: "Settings", shortLabel: "Settings", icon: "Sliders" },
|
|
144
144
|
{ id: "about", label: "About", shortLabel: "About", icon: "Info" },
|
|
145
145
|
];
|
|
@@ -177,7 +177,7 @@ function getMetricColors(
|
|
|
177
177
|
// so pill call sites keep a single import path.
|
|
178
178
|
export type { PillItemType } from "./pill-labels";
|
|
179
179
|
|
|
180
|
-
export type ModalTab = "system" | "context" | "settings" | "about";
|
|
180
|
+
export type ModalTab = "system" | "context" | "permissions" | "settings" | "about";
|
|
181
181
|
|
|
182
182
|
export function getItemTab(item: PillItemType): "system" | "context" {
|
|
183
183
|
switch (item) {
|
|
@@ -448,26 +448,8 @@ function PillItemContent({
|
|
|
448
448
|
);
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
let lastNotifiedSettings: TopSettings | null = null;
|
|
454
|
-
|
|
455
|
-
export function notifySettingsChanged(settings: TopSettings) {
|
|
456
|
-
if (!shouldEmitSnapshotUpdate(
|
|
457
|
-
lastNotifiedSettings as unknown as Record<string, unknown> | null,
|
|
458
|
-
settings as unknown as Record<string, unknown>,
|
|
459
|
-
)) {
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
lastNotifiedSettings = settings;
|
|
463
|
-
for (const listener of settingsListeners) {
|
|
464
|
-
try {
|
|
465
|
-
listener(settings);
|
|
466
|
-
} catch {
|
|
467
|
-
// Ignore listener errors
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
451
|
+
export { notifySettingsChanged } from "./settings-events";
|
|
452
|
+
type SettingsListener = import("./settings-events").SettingsListener;
|
|
471
453
|
|
|
472
454
|
function PillOffline() {
|
|
473
455
|
const { colors } = usePluginTheme();
|
|
@@ -521,9 +503,8 @@ interface PillSegmentProps {
|
|
|
521
503
|
}
|
|
522
504
|
|
|
523
505
|
/**
|
|
524
|
-
* The
|
|
525
|
-
*
|
|
526
|
-
* segment must not nest a second interaction handler.
|
|
506
|
+
* The segment must not nest a second interaction handler: on the host-rendered
|
|
507
|
+
* pill it sits inside the host's own pressable.
|
|
527
508
|
*/
|
|
528
509
|
function PillSegment({
|
|
529
510
|
item,
|
|
@@ -584,7 +565,21 @@ function AllInOnePill({
|
|
|
584
565
|
);
|
|
585
566
|
}
|
|
586
567
|
|
|
587
|
-
|
|
568
|
+
/**
|
|
569
|
+
* Props the pill body views used to receive from the helper's `renderPill`
|
|
570
|
+
* option, which the Paseo 0.9 host shape removed: the host renders the pill
|
|
571
|
+
* body itself, so nothing wires these views any more. The shape is kept
|
|
572
|
+
* because `pill-render.test.tsx` still regression-tests their mobile layout
|
|
573
|
+
* (#507) and that coverage is worth more than the dead wiring.
|
|
574
|
+
*/
|
|
575
|
+
interface PillBodyProps extends HostPillProps {
|
|
576
|
+
isOpen: boolean;
|
|
577
|
+
open(payload?: ModalTab): void;
|
|
578
|
+
close(): void;
|
|
579
|
+
toggle(payload?: ModalTab): void;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export interface SingleItemPillViewProps extends PillBodyProps {
|
|
588
583
|
item: PillItemType;
|
|
589
584
|
defaultTab?: ModalTab;
|
|
590
585
|
}
|
|
@@ -669,7 +664,7 @@ export function SingleItemPillView({
|
|
|
669
664
|
);
|
|
670
665
|
}
|
|
671
666
|
|
|
672
|
-
function PillView({ isOpen, open, workspaceId, agentId }:
|
|
667
|
+
function PillView({ isOpen, open, workspaceId, agentId }: PillBodyProps) {
|
|
673
668
|
// No background settings poll here: the hook re-verifies on mount and
|
|
674
669
|
// window focus, and mutations invalidate the shared settings cache.
|
|
675
670
|
// Polling from every mounted pill/modal/surface multiplied daemon reads
|
|
@@ -1111,6 +1106,7 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
|
|
|
1111
1106
|
<ModalBody
|
|
1112
1107
|
style={{ backgroundColor: colors.surface0 }}
|
|
1113
1108
|
headerMode="pinned"
|
|
1109
|
+
scrollMode="always"
|
|
1114
1110
|
refreshing={isRefetching}
|
|
1115
1111
|
onRefresh={handleRefresh}
|
|
1116
1112
|
header={navbar}
|
|
@@ -1135,6 +1131,7 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
|
|
|
1135
1131
|
<ModalBody
|
|
1136
1132
|
style={{ backgroundColor: colors.surface0 }}
|
|
1137
1133
|
headerMode="pinned"
|
|
1134
|
+
scrollMode="always"
|
|
1138
1135
|
refreshing={isLoading || isRefetching}
|
|
1139
1136
|
onRefresh={handleRefresh}
|
|
1140
1137
|
header={navbar}
|
|
@@ -1683,6 +1680,10 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
|
|
|
1683
1680
|
</>
|
|
1684
1681
|
)}
|
|
1685
1682
|
|
|
1683
|
+
{activeTab === "permissions" && (
|
|
1684
|
+
<PermissionAuditView variant="compact" />
|
|
1685
|
+
)}
|
|
1686
|
+
|
|
1686
1687
|
{activeTab === "about" && (
|
|
1687
1688
|
<AboutSection
|
|
1688
1689
|
name="paseo-top"
|
|
@@ -1729,17 +1730,6 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
|
|
|
1729
1730
|
);
|
|
1730
1731
|
}
|
|
1731
1732
|
|
|
1732
|
-
interface LiveCustomPillViewProps {
|
|
1733
|
-
pillId: string;
|
|
1734
|
-
initial: CustomPillState;
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
function LiveCustomPillView({ pillId, initial }: LiveCustomPillViewProps) {
|
|
1738
|
-
const { data } = useCustomPillsQuery();
|
|
1739
|
-
const liveState = data?.pills.find((p) => p.id === pillId) ?? initial;
|
|
1740
|
-
return <CustomPillBody state={liveState} />;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
1733
|
interface LiveCustomPillModalProps {
|
|
1744
1734
|
pillId: string;
|
|
1745
1735
|
initial: CustomPillStateOutput;
|
|
@@ -1854,12 +1844,6 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
1854
1844
|
modalTitle: "Host System Resources",
|
|
1855
1845
|
modalIcon: "Activity",
|
|
1856
1846
|
icon: "Cpu",
|
|
1857
|
-
resolveDefaultPayload: ({ agentId }) => {
|
|
1858
|
-
if (latestSettings.pillMode === "all") {
|
|
1859
|
-
return latestSettings.defaultTab;
|
|
1860
|
-
}
|
|
1861
|
-
return currentCycleTabByAgent.get(agentId) ?? latestSettings.defaultTab;
|
|
1862
|
-
},
|
|
1863
1847
|
resolveLabel: async (ctx) => {
|
|
1864
1848
|
const snap = await liveSnapshotFor(ctx);
|
|
1865
1849
|
const items = enabledItemsForSettings(latestSettings);
|
|
@@ -1876,7 +1860,6 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
1876
1860
|
};
|
|
1877
1861
|
},
|
|
1878
1862
|
refreshIntervalMs: 3000,
|
|
1879
|
-
renderPill: (props) => <PillView {...props} />,
|
|
1880
1863
|
renderModal: (props) => <ResourceModal {...props} />,
|
|
1881
1864
|
});
|
|
1882
1865
|
activePills.set("paseo-top", cleanup);
|
|
@@ -2081,16 +2064,8 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
2081
2064
|
modalTitle: pillDef.modalTitle,
|
|
2082
2065
|
icon: pillDef.icon,
|
|
2083
2066
|
modalIcon: pillDef.icon,
|
|
2084
|
-
resolveDefaultPayload: () => pillDef.defaultTab,
|
|
2085
2067
|
resolveLabel: singleItemLabelResolver(pillDef.item),
|
|
2086
2068
|
refreshIntervalMs: 5000,
|
|
2087
|
-
renderPill: (props) => (
|
|
2088
|
-
<SingleItemPillView
|
|
2089
|
-
item={pillDef.item}
|
|
2090
|
-
defaultTab={pillDef.defaultTab}
|
|
2091
|
-
{...props}
|
|
2092
|
-
/>
|
|
2093
|
-
),
|
|
2094
2069
|
renderModal: (props) => (
|
|
2095
2070
|
<ResourceModal initialTab={pillDef.defaultTab} {...props} />
|
|
2096
2071
|
),
|
|
@@ -2102,7 +2077,7 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
2102
2077
|
}
|
|
2103
2078
|
|
|
2104
2079
|
// Register settings listener
|
|
2105
|
-
|
|
2080
|
+
addSettingsListener(syncPills);
|
|
2106
2081
|
|
|
2107
2082
|
// Initial sync synchronously
|
|
2108
2083
|
syncPills(topSettingsContract.defaultSettings);
|
|
@@ -2183,7 +2158,6 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
2183
2158
|
);
|
|
2184
2159
|
},
|
|
2185
2160
|
refreshIntervalMs: 5000,
|
|
2186
|
-
renderPill: () => <LiveCustomPillView pillId={pill.id} initial={pill} />,
|
|
2187
2161
|
renderModal: () => (
|
|
2188
2162
|
<LiveCustomPillModal pillId={pill.id} initial={pill} />
|
|
2189
2163
|
),
|
|
@@ -2204,7 +2178,7 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
2204
2178
|
const onSettingsChanged = () => {
|
|
2205
2179
|
void syncCustomPills();
|
|
2206
2180
|
};
|
|
2207
|
-
|
|
2181
|
+
addSettingsListener(onSettingsChanged);
|
|
2208
2182
|
|
|
2209
2183
|
let disposed = false;
|
|
2210
2184
|
const cleanup = () => {
|
|
@@ -2216,8 +2190,8 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
2216
2190
|
cleanupSidebar = null;
|
|
2217
2191
|
}
|
|
2218
2192
|
clearInterval(customPillInterval);
|
|
2219
|
-
|
|
2220
|
-
|
|
2193
|
+
removeSettingsListener(syncPills);
|
|
2194
|
+
removeSettingsListener(onSettingsChanged);
|
|
2221
2195
|
for (const cleanup of activePills.values()) {
|
|
2222
2196
|
cleanup();
|
|
2223
2197
|
}
|
|
@@ -2250,6 +2224,11 @@ const styles = {
|
|
|
2250
2224
|
overflow: "hidden",
|
|
2251
2225
|
flexShrink: 1,
|
|
2252
2226
|
minWidth: 0,
|
|
2227
|
+
// The pill label is one short line; without a height floor a body that
|
|
2228
|
+
// momentarily renders no text (loading, or a blank host field) collapses to
|
|
2229
|
+
// an unreadable skinny line on mobile (xpufx-org/paseo#507). The host
|
|
2230
|
+
// composer pill is 32pt; keep the inner body at least a line tall.
|
|
2231
|
+
minHeight: 20,
|
|
2253
2232
|
},
|
|
2254
2233
|
pillText: {
|
|
2255
2234
|
fontSize: 11,
|
|
@@ -2415,6 +2394,8 @@ const styles = {
|
|
|
2415
2394
|
overflow: "hidden",
|
|
2416
2395
|
flexShrink: 1,
|
|
2417
2396
|
minWidth: 0,
|
|
2397
|
+
// See pillContainer: the combined pill must keep a line-height floor too.
|
|
2398
|
+
minHeight: 20,
|
|
2418
2399
|
},
|
|
2419
2400
|
dividerText: {
|
|
2420
2401
|
fontSize: 10,
|