paseo-plugin-machine-status 0.1.1 → 0.2.0

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 ADDED
@@ -0,0 +1,26 @@
1
+ Machine Status shows what the daemon's Mac is doing while your agents work: CPU load, memory pressure, swap, disk, battery and thermal state, the ports and background processes your agents started, and the Docker engine. It also clears developer caches and stops runaway processes, with a confirmation step for each.
2
+
3
+ ## What you get
4
+
5
+ - **Machine screen**, opened from the sidebar or ⌘K. Overview has stat tiles that expand into detail, CPU and memory charts, a dev-cache scanner, and a process table. Ports & services lists listening ports and what runs behind them. Docker shows the engine, containers and disk usage.
6
+ - **Load pill** beside every agent's composer with CPU load and memory pressure. Pressing it opens a compact card with the trend, the top CPU processes, and the development ports held by agents, so you can tell whether the machine is the problem without leaving the chat.
7
+ - On Paseo 0.11 and newer, ⌘K can open the screen straight on Ports & services or Docker.
8
+
9
+ ## How it decides
10
+
11
+ - Memory follows Activity Monitor's split, and the pressure figure is macOS's own. Cached files are not counted as used.
12
+ - A background process belongs to an agent when it descends from the Paseo daemon in the process tree, not when its name looks like a provider. A port counts as a development port by its executable's path, not by its number. System ports are hidden until you choose **Show system**.
13
+ - Values the plugin cannot read are shown as missing rather than estimated. Per-core utilisation is not shown at all.
14
+
15
+ ## Setup
16
+
17
+ - A macOS daemon on Paseo 0.8 to 0.11. On other platforms the screen says it cannot collect metrics.
18
+ - Docker Desktop, OrbStack or Rancher Desktop for the Docker tab. Without one, the tab says which is missing.
19
+
20
+ ## What it can change
21
+
22
+ - **Dev caches**: only a fixed list of caches that their tools recreate (npm, pnpm, Yarn, Bun, Turbo, Xcode DerivedData, Cargo, Go, pip, uv). The client sends a cache id, never a path; each path must resolve below your home directory, and only its contents are removed.
23
+ - **Processes**: SIGTERM first, and force kill only as a separate confirmed step. Paseo and its parent processes are protected and checked again on every kill.
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
+
26
+ Everything runs on the daemon through local commands such as `vm_stat`, `ps`, `lsof`, `netstat` and `docker`. Nothing is sent off the machine.
package/README.md CHANGED
@@ -56,7 +56,10 @@ paseo plugin ls # expect: running
56
56
  Plugins must be enabled daemon-wide first, under **Settings → Plugins → Enable
57
57
  plugins** (the root `pluginsEnabled` field in `~/.paseo/config.json`).
58
58
 
59
- Open it from the **Machine** sidebar item, or ⌘K → "Machine status".
59
+ Open it from the **Machine** sidebar item, or ⌘K → "Machine status". On Paseo 0.11
60
+ and newer, ⌘K also offers "Machine: ports & services" and "Machine: Docker", which
61
+ open straight on that tab, and the tab travels in the screen's link (`?tab=services`,
62
+ `?tab=docker`), so reload and back keep it.
60
63
 
61
64
  ## The screen
62
65
 
@@ -328,8 +331,7 @@ confirmed step.
328
331
  ## Development
329
332
 
330
333
  ```bash
331
- npm run typecheck
332
- npm test # 237 tests
334
+ npm run check # typecheck, then the tests
333
335
  paseo plugin reload machine-status # source edits need an explicit reload
334
336
  paseo plugin logs machine-status
335
337
  npm run preview # regenerates docs/preview.svg from the real palette
@@ -344,15 +346,35 @@ would otherwise surface as an RPC rejection at runtime. The live tests never
344
346
  mutate anything: cleanup is only planned, and the kill tests assert refusals,
345
347
  including a refusal to terminate the test runner itself.
346
348
 
347
- The SDK remains pinned to 0.8.0 as the development baseline for typechecking and
348
- tests. Paseo supplies the runtime instances on every supported release line, so
349
- none of it ships with the plugin.
349
+ ### Paseo versions
350
+
351
+ The SDK stays pinned to 0.8.0 for typechecking and tests, so the compiler proves
352
+ the plugin uses only the oldest supported API. Paseo supplies the runtime instances
353
+ on every supported release line, so none of it ships with the plugin.
354
+
355
+ Paseo 0.11 replaced surfaces with screens and kept the old calls as deprecated
356
+ aliases. The manifest still admits 0.8, and each connected app evaluates the client
357
+ entry against its own version, so `client/compat.tsx` describes the 0.11 additions
358
+ locally and calls them only after checking that the app has them:
359
+
360
+ | App version | Screen | Sidebar | Opens with | Tab shortcuts in ⌘K |
361
+ | ----------- | ------ | ------- | ---------- | ------------------- |
362
+ | 0.11 and newer | `addScreen` | `addSidebarHeaderItem` + `SidebarRow` | `openScreen`, with `?tab=` | yes |
363
+ | 0.8 to 0.10 | `addSurface` | `addSidebarItem` | `openSurface`, overview tab | not offered |
364
+
365
+ Both paths register under the id `main`, so saved `/plugin/machine-status/main`
366
+ links open the same screen after an upgrade. `tests/compat.test.ts` pins both
367
+ paths, including a half-present API, which falls back rather than mixing the two.
368
+ The daemon half uses only `server.handle`, which has not changed since 0.8.
369
+
370
+ Releases are cut from tags by GitHub Actions; see [CONTRIBUTING.md](CONTRIBUTING.md)
371
+ for the flow and [CHANGELOG.md](CHANGELOG.md) for what changed.
350
372
 
351
373
  ## Troubleshooting
352
374
 
353
375
  | Symptom | Check |
354
376
  | ------- | ----- |
355
- | Sidebar item missing | `paseo plugin ls` reports `running`, and the client is on this host |
377
+ | Sidebar item missing | `paseo plugin ls` reports `running`, the client is on this host, and on 0.11+ the item is not hidden in **Settings → Sidebar** |
356
378
  | "Metrics unavailable" | The daemon is not on macOS, or a `/usr/bin` tool is missing |
357
379
  | Charts look empty | History starts empty; it fills as the tab stays open |
358
380
  | A port shows "another user" | It belongs to root or another account; `lsof` cannot inspect it unprivileged |
@@ -0,0 +1,123 @@
1
+ import type {
2
+ PluginClientContext,
3
+ PluginCommandCapabilities,
4
+ PluginHostProps,
5
+ PluginSurfaceProps,
6
+ } from "@getpaseo/plugin/client";
7
+ import * as ui from "@getpaseo/plugin/client/ui";
8
+ import type { ComponentType, FunctionComponent } from "react";
9
+
10
+ /**
11
+ * Paseo 0.11 replaced surfaces with screens: `addScreen`, `addSidebarHeaderItem`
12
+ * and `openScreen`. The manifest still admits 0.8, and every connected app runs
13
+ * this entry against its own version, so each call checks for the new method and
14
+ * falls back to the old one.
15
+ *
16
+ * The SDK stays pinned to 0.8.0 so typechecking proves the rest of the plugin
17
+ * uses only the oldest supported API. The 0.11 additions are described here,
18
+ * from the 0.11 SDK declarations, and are reachable only behind a runtime check.
19
+ */
20
+
21
+ const SCREEN_ID = "main";
22
+ const TITLE = "Machine";
23
+ const ICON = "Activity";
24
+
25
+ /** String keys and values: 0.11 carries them in the screen's URL. */
26
+ export type ScreenParams = Record<string, string>;
27
+
28
+ interface OpenScreenInput {
29
+ screenId: string;
30
+ params?: ScreenParams;
31
+ }
32
+
33
+ interface SidebarItemProps extends PluginHostProps {
34
+ currentScreen: { screenId: string; params: ScreenParams } | null;
35
+ openScreen(input: OpenScreenInput): void;
36
+ }
37
+
38
+ /** What 0.11 adds to the client context. */
39
+ interface ScreenApi {
40
+ addScreen(contribution: {
41
+ id: string;
42
+ title: string;
43
+ Component: ComponentType<MachineScreenProps>;
44
+ }): () => void;
45
+ addSidebarHeaderItem(contribution: {
46
+ id: string;
47
+ title: string;
48
+ Component: ComponentType<SidebarItemProps>;
49
+ }): () => void;
50
+ }
51
+
52
+ /** What 0.11 adds to command and slash-command callbacks. */
53
+ interface OpenScreenApi {
54
+ openScreen(input: OpenScreenInput): void;
55
+ }
56
+
57
+ /** `@getpaseo/plugin/client/ui` gained this in 0.11; the older app runtime has no such export. */
58
+ type SidebarRowComponent = ComponentType<{ icon?: string; active?: boolean; onPress(): void }>;
59
+
60
+ /** What the screen receives on either API: 0.11 adds `params`, older hosts do not. */
61
+ export type MachineScreenProps = PluginSurfaceProps & { params?: ScreenParams };
62
+
63
+ function screenApi(client: PluginClientContext): ScreenApi | null {
64
+ const candidate = client as PluginClientContext & Partial<ScreenApi>;
65
+ return typeof candidate.addScreen === "function" && typeof candidate.addSidebarHeaderItem === "function"
66
+ ? (candidate as PluginClientContext & ScreenApi)
67
+ : null;
68
+ }
69
+
70
+ function openScreenApi(capabilities: object): OpenScreenApi | null {
71
+ const candidate = capabilities as Partial<OpenScreenApi>;
72
+ return typeof candidate.openScreen === "function" ? (candidate as OpenScreenApi) : null;
73
+ }
74
+
75
+ export function registerMachineScreen(
76
+ client: PluginClientContext,
77
+ Component: FunctionComponent<MachineScreenProps>,
78
+ ): void {
79
+ const screens = screenApi(client);
80
+ if (screens) {
81
+ screens.addScreen({ id: SCREEN_ID, title: TITLE, Component });
82
+ // Same id as the old sidebar item, so saved `/plugin/machine-status/main` links keep working.
83
+ screens.addSidebarHeaderItem({ id: SCREEN_ID, title: TITLE, Component: MachineSidebarItem });
84
+ return;
85
+ }
86
+ client.addSurface(SCREEN_ID, Component);
87
+ client.addSidebarItem({ id: SCREEN_ID, title: TITLE, icon: ICON, surface: SCREEN_ID });
88
+ }
89
+
90
+ function MachineSidebarItem({ currentScreen, openScreen }: SidebarItemProps) {
91
+ // Read off the namespace: the 0.8–0.10 ui runtime has no SidebarRow, and the
92
+ // item renders nothing rather than crash.
93
+ const SidebarRow = (ui as { SidebarRow?: SidebarRowComponent }).SidebarRow;
94
+ if (!SidebarRow) return null;
95
+ return (
96
+ <SidebarRow
97
+ icon={ICON}
98
+ active={currentScreen?.screenId === SCREEN_ID}
99
+ onPress={() => openScreen({ screenId: SCREEN_ID })}
100
+ />
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Opens the Machine screen, on a tab when the host can carry params. Pre-0.11
106
+ * hosts open the surface on its default tab.
107
+ */
108
+ export function openMachineScreen(
109
+ capabilities: Pick<PluginCommandCapabilities, "openSurface">,
110
+ params?: ScreenParams,
111
+ ): void {
112
+ const screens = openScreenApi(capabilities);
113
+ if (screens) {
114
+ screens.openScreen(params ? { screenId: SCREEN_ID, params } : { screenId: SCREEN_ID });
115
+ return;
116
+ }
117
+ capabilities.openSurface(SCREEN_ID);
118
+ }
119
+
120
+ /** True on 0.11+ apps, where a command can land on a specific tab. */
121
+ export function supportsScreenParams(client: PluginClientContext): boolean {
122
+ return openScreenApi(client) !== null;
123
+ }
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 { openMachineScreen } from "./compat";
10
11
  import { createMachineCard } from "./pill-card";
11
12
  import { pillReading, type PillReading } from "../shared/pill";
12
13
  import { STATUS } from "../shared/viz";
@@ -72,7 +73,7 @@ interface TrackedAgent {
72
73
  export function contributePills(client: PluginClientContext) {
73
74
  const store = createReadingStore();
74
75
  const icon = createIcon(store);
75
- const card = createMachineCard(() => client.openSurface("main"));
76
+ const card = createMachineCard(() => openMachineScreen(client));
76
77
  const pills = new Map<string, { workspaceId: string; registration: PluginButtonRegistration }>();
77
78
  let disposed = false;
78
79
  let inFlight = false;
package/client/status.tsx CHANGED
@@ -1,11 +1,12 @@
1
1
  import { useRpc, type PluginSurfaceProps } from "@getpaseo/plugin/client";
2
2
  import { useQuery } from "@tanstack/react-query";
3
- import React, { useMemo, useState } from "react";
3
+ import React, { useEffect, useMemo, useState } from "react";
4
4
  import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
5
5
  import { ColumnChart, Legend, Meter, Sparkbars, StackedColumnChart, StatTile } from "./charts";
6
6
  import { ServicesTab } from "./services";
7
7
  import { CleanupSection } from "./cleanup";
8
8
  import { DockerTab } from "./docker";
9
+ import type { MachineScreenProps } from "./compat";
9
10
  import { ContextMenuProvider } from "./menu";
10
11
  import { readMetrics, type Metrics } from "../shared/contracts";
11
12
  import { formatBytes, formatDuration, formatMinutes, formatPercent } from "../shared/format";
@@ -444,7 +445,7 @@ function DetailCard({
444
445
  }
445
446
 
446
447
 
447
- type TabId = "overview" | "services" | "docker";
448
+ export type TabId = "overview" | "services" | "docker";
448
449
 
449
450
  const TABS: readonly { id: TabId; title: string }[] = [
450
451
  { id: "overview", title: "Overview" },
@@ -452,15 +453,23 @@ const TABS: readonly { id: TabId; title: string }[] = [
452
453
  { id: "docker", title: "Docker" },
453
454
  ];
454
455
 
456
+ /** `?tab=` from a 0.11 screen link; anything unknown, or no params at all, is the overview. */
457
+ function tabFromParams(params: MachineScreenProps["params"]): TabId {
458
+ return TABS.find((entry) => entry.id === params?.tab)?.id ?? "overview";
459
+ }
460
+
455
461
  /**
456
462
  * Only the selected tab is mounted. The services tab shells out to netstat, lsof
457
463
  * and docker on a timer, and mounting it in the background would keep that
458
464
  * running for a panel nobody is looking at.
459
465
  */
460
- export function MachineStatusSurface(props: PluginSurfaceProps) {
466
+ export function MachineStatusSurface(props: MachineScreenProps) {
461
467
  const chrome: Chrome = { theme: props.theme, compact: props.layout.compact };
462
468
  const styles = useChromeStyles(chrome);
463
- const [tab, setTab] = useState<TabId>("overview");
469
+ const linkedTab = tabFromParams(props.params);
470
+ const [tab, setTab] = useState<TabId>(linkedTab);
471
+ // Opening the already-open screen with another `tab` swaps params without a remount.
472
+ useEffect(() => setTab(linkedTab), [linkedTab]);
464
473
 
465
474
  return (
466
475
  <ContextMenuProvider chrome={chrome}>
package/index.client.tsx CHANGED
@@ -1,20 +1,14 @@
1
1
  import type { PluginClientContext } from "@getpaseo/plugin/client";
2
+ import { openMachineScreen, registerMachineScreen, supportsScreenParams } from "./client/compat";
2
3
  import { contributePills } from "./client/pill";
3
4
  import { MachineStatusSurface } from "./client/status";
4
5
 
5
6
  /**
6
- * The app half of the plugin: the Machine surface, the ways into it from the
7
+ * The app half of the plugin: the Machine screen, the ways into it from the
7
8
  * sidebar and the Command Center, and the load pill beside each agent's composer.
8
9
  */
9
10
  export default function contribute(client: PluginClientContext) {
10
- client.addSurface("main", MachineStatusSurface);
11
-
12
- client.addSidebarItem({
13
- id: "main",
14
- title: "Machine",
15
- icon: "Activity",
16
- surface: "main",
17
- });
11
+ registerMachineScreen(client, MachineStatusSurface);
18
12
 
19
13
  client.addCommandCenterItem({
20
14
  id: "open-machine-status",
@@ -22,10 +16,35 @@ export default function contribute(client: PluginClientContext) {
22
16
  icon: "Activity",
23
17
  keywords: ["cpu", "memory", "disk", "swap", "battery", "thermal", "ports", "services"],
24
18
  context: "global",
25
- onSelect({ openSurface }) {
26
- openSurface("main");
19
+ onSelect(capabilities) {
20
+ openMachineScreen(capabilities);
27
21
  },
28
22
  });
29
23
 
24
+ // Straight to a tab needs screen params. On older apps these would open the
25
+ // overview under a misleading title, so they are not offered there.
26
+ if (supportsScreenParams(client)) {
27
+ client.addCommandCenterItem({
28
+ id: "open-machine-services",
29
+ title: "Machine: ports & services",
30
+ icon: "Network",
31
+ keywords: ["ports", "listening", "lsof", "dev server"],
32
+ context: "global",
33
+ onSelect(capabilities) {
34
+ openMachineScreen(capabilities, { tab: "services" });
35
+ },
36
+ });
37
+ client.addCommandCenterItem({
38
+ id: "open-machine-docker",
39
+ title: "Machine: Docker",
40
+ icon: "Container",
41
+ keywords: ["docker", "containers", "orbstack", "rancher"],
42
+ context: "global",
43
+ onSelect(capabilities) {
44
+ openMachineScreen(capabilities, { tab: "docker" });
45
+ },
46
+ });
47
+ }
48
+
30
49
  return contributePills(client);
31
50
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-plugin-machine-status",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Machine dashboard, open ports and Docker control inside Paseo",
5
5
  "license": "MIT",
6
6
  "author": "dutchakdev",
@@ -15,20 +15,26 @@
15
15
  "macos",
16
16
  "docker"
17
17
  ],
18
+ "engines": {
19
+ "node": ">=22.12.0"
20
+ },
18
21
  "scripts": {
19
22
  "typecheck": "tsc --noEmit",
20
23
  "test": "vitest run",
21
24
  "test:watch": "vitest",
22
- "preview": "tsx scripts/preview.ts"
25
+ "preview": "tsx scripts/preview.ts",
26
+ "check": "npm run typecheck && npm test"
23
27
  },
24
28
  "devDependencies": {
25
29
  "@getpaseo/client": "0.8.0",
26
30
  "@getpaseo/plugin": "0.8.0",
27
31
  "@getpaseo/protocol": "0.8.0",
28
32
  "@tanstack/react-query": "^5.90.11",
33
+ "@types/node": "22.20.5",
29
34
  "@types/react": "~19.2.0",
30
35
  "react": "19.1.0",
31
36
  "react-native": "0.81.5",
37
+ "semver": "7.8.5",
32
38
  "tsx": "^4.23.13",
33
39
  "typescript": "^5.9.3",
34
40
  "vitest": "^4.1.11",
@@ -36,6 +42,7 @@
36
42
  },
37
43
  "files": [
38
44
  "paseo-plugin.json",
45
+ "OVERVIEW.md",
39
46
  "index.client.tsx",
40
47
  "index.server.ts",
41
48
  "client/",