paseo-plugin-machine-status 0.3.0 → 0.3.2
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/OVERVIEW.md +1 -1
- package/README.md +5 -5
- package/client/agent-directory.ts +139 -0
- package/client/pill.tsx +5 -24
- package/package.json +7 -6
package/OVERVIEW.md
CHANGED
|
@@ -14,7 +14,7 @@ Machine Status shows what the daemon's Mac is doing while your agents work: CPU
|
|
|
14
14
|
|
|
15
15
|
## Setup
|
|
16
16
|
|
|
17
|
-
- A macOS daemon on Paseo 0.11
|
|
17
|
+
- A macOS daemon on Paseo 0.11 or newer. On other platforms the screen says it cannot collect metrics.
|
|
18
18
|
- Docker Desktop, OrbStack or Rancher Desktop for the Docker tab. Without one, the tab says which is missing.
|
|
19
19
|
|
|
20
20
|
## What it can change
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://github.com/dutchakdev/paseo-plugin-machine-status/actions/workflows/ci.yml"><img src="https://github.com/dutchakdev/paseo-plugin-machine-status/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
|
|
13
13
|
<a href="https://www.npmjs.com/package/paseo-plugin-machine-status"><img src="https://img.shields.io/npm/v/paseo-plugin-machine-status" alt="npm"></a>
|
|
14
|
-
<img src="https://img.shields.io/badge/Paseo-0.11
|
|
14
|
+
<img src="https://img.shields.io/badge/Paseo-0.11%2B-3987e5" alt="Supports Paseo 0.11 and newer, including betas">
|
|
15
15
|
<img src="https://img.shields.io/badge/macOS-only-9aa1a6" alt="macOS only">
|
|
16
16
|
<img src="https://img.shields.io/badge/dependencies-none-c98500" alt="No runtime dependencies">
|
|
17
17
|
<img src="https://img.shields.io/badge/license-MIT-9aa1a6" alt="MIT licensed">
|
|
@@ -44,12 +44,12 @@ source that matches your Paseo:
|
|
|
44
44
|
Paseo 0.11 a bare `owner/name` names a directory entry, so a GitHub source there needs
|
|
45
45
|
the `git:` prefix: `git:dutchakdev/paseo-plugin-machine-status`.
|
|
46
46
|
|
|
47
|
-
The plugin supports **Paseo 0.11
|
|
47
|
+
The plugin supports **Paseo 0.11 and newer, including betas**, on both the
|
|
48
48
|
daemon, which runs its collectors, and the app, which runs its screens.
|
|
49
49
|
|
|
50
50
|
The manifest asks for `>=0.11.0` because it declares the icon shown in the plugin
|
|
51
51
|
directory, and Paseo 0.8 to 0.10 refuse a manifest with a field they do not know.
|
|
52
|
-
Paseo also checks a prerelease's stable version core, so
|
|
52
|
+
Paseo also checks a prerelease's stable version core, so the betas of later releases satisfy
|
|
53
53
|
this range. The 0.11.0 betas pass that check but predate the `icon` field, so they
|
|
54
54
|
refuse the manifest as well and need the release named in the install table; see
|
|
55
55
|
[version requirements](https://paseo.sh/docs/plugins/reference#requirements).
|
|
@@ -364,8 +364,8 @@ including a refusal to terminate the test runner itself.
|
|
|
364
364
|
|
|
365
365
|
### Paseo versions
|
|
366
366
|
|
|
367
|
-
The SDK stays pinned to 0.
|
|
368
|
-
the plugin uses only the 0.
|
|
367
|
+
The SDK stays pinned to 0.11.4 for typechecking and tests, so the compiler proves
|
|
368
|
+
the plugin uses only the 0.11 API, which every supported release still provides.
|
|
369
369
|
Paseo supplies the runtime instances on every supported release line, so none of
|
|
370
370
|
it ships with the plugin.
|
|
371
371
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
+
|
|
3
|
+
export interface DirectoryAgent {
|
|
4
|
+
id: string;
|
|
5
|
+
workspaceId?: string | null;
|
|
6
|
+
archivedAt?: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DirectoryListener {
|
|
10
|
+
/** An agent exists, or changed. */
|
|
11
|
+
track(agent: DirectoryAgent): void;
|
|
12
|
+
/** An agent is gone. */
|
|
13
|
+
hide(agentId: string): void;
|
|
14
|
+
/** The agents that are known at the moment. */
|
|
15
|
+
known(): Iterable<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The most Paseo returns in one page. */
|
|
19
|
+
const PAGE_LIMIT = 200;
|
|
20
|
+
/** How long after a failure the observation is asked for again; each failure in a row doubles it, up to the cap. */
|
|
21
|
+
export const RETRY_MS = 5_000;
|
|
22
|
+
export const RETRY_CAP_MS = 60_000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Follows the agent directory through one owned observation.
|
|
26
|
+
*
|
|
27
|
+
* Paseo 0.15 keeps a plugin installed across a dropped connection and does not run
|
|
28
|
+
* its setup again, so a list read once at setup would miss everything that happened
|
|
29
|
+
* meanwhile. The observation hands over the existing agents and, after each
|
|
30
|
+
* reconnect, a fresh snapshot of the first page. The rest are read page by page, and
|
|
31
|
+
* only a directory read to its end removes the agents that went while the app was
|
|
32
|
+
* away; a page that fails starts the read again later. Paseo gives up an observation
|
|
33
|
+
* whose request fails after a reconnect, so a failure asks for a new one. Returns
|
|
34
|
+
* the cleanup.
|
|
35
|
+
*/
|
|
36
|
+
export function followAgents(paseo: PluginClientContext["paseo"], listener: DirectoryListener): () => void {
|
|
37
|
+
const lifetime = new AbortController();
|
|
38
|
+
let retry: ReturnType<typeof setTimeout> | undefined;
|
|
39
|
+
let failures = 0;
|
|
40
|
+
// Each snapshot starts a new read, and a read still under way gives way to it.
|
|
41
|
+
let reading = 0;
|
|
42
|
+
// Agents an update spoke of during a read: a page read after that may be older than the update.
|
|
43
|
+
const updated = new Set<string>();
|
|
44
|
+
|
|
45
|
+
async function catchUp(first: Awaited<ReturnType<typeof paseo.agents.list>>) {
|
|
46
|
+
const read = ++reading;
|
|
47
|
+
updated.clear();
|
|
48
|
+
const listed = new Set<string>();
|
|
49
|
+
const take = (page: typeof first) => {
|
|
50
|
+
for (const { agent } of page.entries) {
|
|
51
|
+
listed.add(agent.id);
|
|
52
|
+
if (!updated.has(agent.id)) listener.track(agent);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
take(first);
|
|
56
|
+
let { hasMore, nextCursor } = first.pageInfo;
|
|
57
|
+
while (hasMore) {
|
|
58
|
+
// A directory that cannot be read to its end says nothing about the agents it did not reach, so none is removed.
|
|
59
|
+
if (!nextCursor) return;
|
|
60
|
+
let page: typeof first;
|
|
61
|
+
try {
|
|
62
|
+
page = await paseo.agents.list({ page: { limit: PAGE_LIMIT, cursor: nextCursor } });
|
|
63
|
+
} catch {
|
|
64
|
+
if (read === reading) readAgain();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (read !== reading || lifetime.signal.aborted) return;
|
|
68
|
+
if (page.pageInfo.nextCursor === nextCursor) return;
|
|
69
|
+
take(page);
|
|
70
|
+
({ hasMore, nextCursor } = page.pageInfo);
|
|
71
|
+
}
|
|
72
|
+
failures = 0;
|
|
73
|
+
for (const id of [...listener.known()]) {
|
|
74
|
+
if (!listed.has(id) && !updated.has(id)) listener.hide(id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function later(action: () => void) {
|
|
79
|
+
if (lifetime.signal.aborted) return;
|
|
80
|
+
clearTimeout(retry);
|
|
81
|
+
retry = setTimeout(action, Math.min(RETRY_CAP_MS, RETRY_MS * 2 ** failures));
|
|
82
|
+
failures += 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readAgain() {
|
|
86
|
+
later(() => {
|
|
87
|
+
const read = reading;
|
|
88
|
+
void paseo.agents
|
|
89
|
+
.list({ page: { limit: PAGE_LIMIT } })
|
|
90
|
+
.then((first) => {
|
|
91
|
+
if (read === reading && !lifetime.signal.aborted) void catchUp(first);
|
|
92
|
+
return undefined;
|
|
93
|
+
})
|
|
94
|
+
.catch(() => {
|
|
95
|
+
if (read === reading) readAgain();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function observeAgain() {
|
|
101
|
+
later(observe);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function observe() {
|
|
105
|
+
if (lifetime.signal.aborted) return;
|
|
106
|
+
void paseo.agents
|
|
107
|
+
.list({ subscribe: {}, page: { limit: PAGE_LIMIT }, signal: lifetime.signal })
|
|
108
|
+
.then(({ subscription }) => {
|
|
109
|
+
subscription.subscribe({
|
|
110
|
+
snapshot(result) {
|
|
111
|
+
if (lifetime.signal.aborted) return;
|
|
112
|
+
clearTimeout(retry);
|
|
113
|
+
failures = 0;
|
|
114
|
+
void catchUp(result);
|
|
115
|
+
},
|
|
116
|
+
update(message) {
|
|
117
|
+
if (lifetime.signal.aborted || message.type !== "agent_update") return;
|
|
118
|
+
const update = message.payload;
|
|
119
|
+
if (update.kind === "upsert") {
|
|
120
|
+
updated.add(update.agent.id);
|
|
121
|
+
listener.track(update.agent);
|
|
122
|
+
} else {
|
|
123
|
+
updated.add(update.agentId);
|
|
124
|
+
listener.hide(update.agentId);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
error: observeAgain,
|
|
128
|
+
});
|
|
129
|
+
return undefined;
|
|
130
|
+
})
|
|
131
|
+
.catch(observeAgain);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
observe();
|
|
135
|
+
return () => {
|
|
136
|
+
lifetime.abort();
|
|
137
|
+
clearTimeout(retry);
|
|
138
|
+
};
|
|
139
|
+
}
|
package/client/pill.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
8
8
|
import React, { useSyncExternalStore } from "react";
|
|
9
9
|
import { readMetrics } from "../shared/contracts";
|
|
10
|
+
import { followAgents, type DirectoryAgent } from "./agent-directory";
|
|
10
11
|
import { openMachineScreen } from "./compat";
|
|
11
12
|
import { createMachineCard } from "./pill-card";
|
|
12
13
|
import { pillReading, type PillReading } from "../shared/pill";
|
|
@@ -56,19 +57,12 @@ function createIcon(store: ReadingStore) {
|
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
interface TrackedAgent {
|
|
60
|
-
id: string;
|
|
61
|
-
workspaceId?: string | null;
|
|
62
|
-
archivedAt?: string | null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
60
|
/**
|
|
66
61
|
* CPU load and memory pressure beside every agent's composer, fed by one poll.
|
|
67
62
|
*
|
|
68
63
|
* Paseo scopes a pill to a single agent, so this follows the agent directory: a
|
|
69
64
|
* pill appears for each agent that has a workspace and goes when the agent is
|
|
70
|
-
* archived or removed.
|
|
71
|
-
* subscription reports changes rather than the agents that already exist.
|
|
65
|
+
* archived or removed.
|
|
72
66
|
*/
|
|
73
67
|
export function contributePills(client: PluginClientContext) {
|
|
74
68
|
const store = createReadingStore();
|
|
@@ -124,32 +118,19 @@ export function contributePills(client: PluginClientContext) {
|
|
|
124
118
|
pills.delete(agentId);
|
|
125
119
|
}
|
|
126
120
|
|
|
127
|
-
function track(agent:
|
|
121
|
+
function track(agent: DirectoryAgent) {
|
|
128
122
|
if (agent.workspaceId && !agent.archivedAt) show(agent.id, agent.workspaceId);
|
|
129
123
|
else hide(agent.id);
|
|
130
124
|
}
|
|
131
125
|
|
|
132
|
-
const
|
|
133
|
-
if (disposed) return;
|
|
134
|
-
if (update.kind === "upsert") track(update.agent);
|
|
135
|
-
else hide(update.agentId);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
client.paseo.agents
|
|
139
|
-
.list()
|
|
140
|
-
.then(({ entries }) => {
|
|
141
|
-
if (!disposed) for (const entry of entries) track(entry.agent);
|
|
142
|
-
})
|
|
143
|
-
.catch(() => {
|
|
144
|
-
// Without the seed, pills appear as each agent next reports a change.
|
|
145
|
-
});
|
|
126
|
+
const stopFollowing = followAgents(client.paseo, { track, hide, known: () => pills.keys() });
|
|
146
127
|
|
|
147
128
|
const timer = setInterval(() => void refresh(), REFRESH_MS);
|
|
148
129
|
|
|
149
130
|
return () => {
|
|
150
131
|
disposed = true;
|
|
151
132
|
clearInterval(timer);
|
|
152
|
-
|
|
133
|
+
stopFollowing();
|
|
153
134
|
for (const { registration } of pills.values()) registration.remove();
|
|
154
135
|
pills.clear();
|
|
155
136
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paseo-plugin-machine-status",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Machine dashboard, open ports and Docker control inside Paseo",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dutchakdev",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"check": "npm run typecheck && npm test"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@getpaseo/client": "0.
|
|
30
|
-
"@getpaseo/plugin": "0.
|
|
31
|
-
"@getpaseo/protocol": "0.
|
|
29
|
+
"@getpaseo/client": "0.11.4",
|
|
30
|
+
"@getpaseo/plugin": "0.11.4",
|
|
31
|
+
"@getpaseo/protocol": "0.11.4",
|
|
32
32
|
"@tanstack/react-query": "^5.90.11",
|
|
33
33
|
"@types/node": "22.20.5",
|
|
34
34
|
"@types/react": "~19.2.0",
|
|
@@ -36,8 +36,9 @@
|
|
|
36
36
|
"react-native": "0.81.5",
|
|
37
37
|
"semver": "7.8.5",
|
|
38
38
|
"tsx": "^4.23.13",
|
|
39
|
-
"typescript": "^
|
|
40
|
-
"
|
|
39
|
+
"typescript": "^7.0.2",
|
|
40
|
+
"vite": "^8.3.5",
|
|
41
|
+
"vitest": "^5.0.5",
|
|
41
42
|
"zod": "^4.4.3"
|
|
42
43
|
},
|
|
43
44
|
"files": [
|