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 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 to 0.14. On other platforms the screen says it cannot collect metrics.
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.x%20%E2%80%93%200.14.x-3987e5" alt="Supports Paseo 0.11.x through 0.14.x, including betas">
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.x through 0.14.x, including betas**, on both the
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 `0.14.0-beta.1` satisfies
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.8.0 for typechecking and tests, so the compiler proves
368
- the plugin uses only the 0.8 API, which every supported release still provides.
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. The directory is seeded from `list`, because a
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: TrackedAgent) {
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 unsubscribe = client.paseo.agents.subscribe((update) => {
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
- unsubscribe();
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.1",
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.8.0",
30
- "@getpaseo/plugin": "0.8.0",
31
- "@getpaseo/protocol": "0.8.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": "^5.9.3",
40
- "vitest": "^4.1.11",
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": [