paseo-plugin-machine-status 0.3.1 → 0.3.3
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 +7 -1
- package/README.md +11 -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
|
|
@@ -24,3 +24,9 @@ Machine Status shows what the daemon's Mac is doing while your agents work: CPU
|
|
|
24
24
|
- **Docker**: start, stop, restart and remove containers, quit or start the engine, and prune stopped containers and dangling images. Volumes are never pruned.
|
|
25
25
|
|
|
26
26
|
Everything runs on the daemon through local commands such as `vm_stat`, `ps`, `lsof`, `netstat` and `docker`. Nothing is sent off the machine.
|
|
27
|
+
|
|
28
|
+
## Other plugins by dutchakdev
|
|
29
|
+
|
|
30
|
+
- [Account Switcher](https://paseo.sh/plugins/dutchakdev/account-switcher): several Claude and ChatGPT/Codex subscription accounts in Paseo, with an account per agent, switching of an existing chat, and every account's limits.
|
|
31
|
+
- [Mermaid](https://paseo.sh/plugins/dutchakdev/mermaid): draws Mermaid flowcharts and sequence diagrams from chat messages and `.mmd` files, on desktop and mobile.
|
|
32
|
+
- [secure-input](https://paseo.sh/plugins/dutchakdev/secure-input): agents ask you for secrets through a form, and each value lands in a short-lived file the agent moves without reading it.
|
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
|
|
|
@@ -403,6 +403,12 @@ for the flow and [CHANGELOG.md](CHANGELOG.md) for what changed.
|
|
|
403
403
|
| A cache shows "not present" | The tool is not installed, or it moved its cache |
|
|
404
404
|
| Edits do not appear | `npm run typecheck`, then `paseo plugin reload machine-status` |
|
|
405
405
|
|
|
406
|
+
## Other plugins by dutchakdev
|
|
407
|
+
|
|
408
|
+
- [Account Switcher](https://paseo.sh/plugins/dutchakdev/account-switcher) ([source](https://github.com/dutchakdev/paseo-plugin-account-switcher)): several Claude and ChatGPT/Codex subscription accounts in Paseo, with an account per agent, switching of an existing chat, and every account's limits.
|
|
409
|
+
- [Mermaid](https://paseo.sh/plugins/dutchakdev/mermaid) ([source](https://github.com/dutchakdev/paseo-plugin-mermaid)): draws Mermaid flowcharts and sequence diagrams from chat messages and `.mmd` files, on desktop and mobile.
|
|
410
|
+
- [secure-input](https://paseo.sh/plugins/dutchakdev/secure-input) ([source](https://github.com/dutchakdev/paseo-plugin-secure-input)): agents ask you for secrets through a form, and each value lands in a short-lived file the agent moves without reading it.
|
|
411
|
+
|
|
406
412
|
## License
|
|
407
413
|
|
|
408
414
|
MIT
|
|
@@ -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.3",
|
|
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": [
|