paseo-bm 0.2.0-alpha.0 → 0.2.0-alpha.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-bm",
3
- "version": "0.2.0-alpha.0",
3
+ "version": "0.2.0-alpha.1",
4
4
  "description": "Beads Management for Paseo: installs the paseo-bm plugin and its agent roles",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The "Beads" button on every open workspace's header (delta 20260918e §4.6,
3
+ * REQ-060 o; owner decision Q14 a).
4
+ *
5
+ * Paseo 0.8's mobile app has no "+" new-tab button, and its "…" Workspace
6
+ * actions sheet has a fixed list that plugins cannot extend, so the Beads tab
7
+ * (`bm-beads`) had no way in on a phone. The workspace header does draw plugin
8
+ * header buttons on mobile (the first one inline), and a button can open a
9
+ * panel. On desktop the same button is a shortcut next to "+".
10
+ *
11
+ * A header button belongs to ONE workspace, so one is added per open workspace
12
+ * and removed when the workspace goes, following the list the way
13
+ * `waiting-pills` follows its Workers: read now, every `BEADS_HEADER_POLL_MS`,
14
+ * and whenever Paseo reports a workspace update. A failed read keeps the
15
+ * buttons as they are.
16
+ *
17
+ * No JSX and no React Native: the button is data, and the test drives it with a
18
+ * fake client.
19
+ */
20
+ import type { PluginButton, PluginButtonRegistration, PluginClientContext } from "@getpaseo/plugin/client";
21
+ import { BEADS_TAB_PANEL_ID } from "./dashboard-view";
22
+
23
+ /** One id for every workspace's button: Paseo keys a header button by id AND workspace. */
24
+ export const BEADS_HEADER_BUTTON_ID = "bm-beads-open";
25
+
26
+ /** How often the list of open workspaces is read again. */
27
+ export const BEADS_HEADER_POLL_MS = 15_000;
28
+
29
+ /** The button itself: an icon only, so it fits a phone's header. */
30
+ export function beadsHeaderButton(openTab: () => void): PluginButton {
31
+ return {
32
+ title: "Open the Beads tab: beads and metrics of this workspace",
33
+ icon: "ListChecks",
34
+ behavior: { kind: "action", onPress: openTab },
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Which buttons to add and remove: one per open workspace. A workspace being
40
+ * archived counts as gone, as it does in the Workspaces list.
41
+ */
42
+ export function planHeaderButtons(
43
+ shown: ReadonlySet<string>,
44
+ listed: ReadonlyArray<{ id: string; archivingAt?: string | null }>,
45
+ ): { add: string[]; remove: string[] } {
46
+ const open = new Set(listed.filter((workspace) => !workspace.archivingAt).map((workspace) => workspace.id));
47
+ return {
48
+ add: [...open].filter((id) => !shown.has(id)),
49
+ remove: [...shown].filter((id) => !open.has(id)),
50
+ };
51
+ }
52
+
53
+ /** Starts the buttons; returns the cleanup that stops following and removes them all. */
54
+ export function registerBeadsHeaderButtons(client: PluginClientContext): () => void {
55
+ const shown = new Map<string, PluginButtonRegistration>();
56
+ let pending = false;
57
+ let stopped = false;
58
+ let unsubscribe: (() => void) | undefined;
59
+
60
+ const refresh = async () => {
61
+ if (pending || stopped) return;
62
+ pending = true;
63
+ try {
64
+ const { entries } = await client.paseo.workspaces.list({});
65
+ if (stopped) return;
66
+ const plan = planHeaderButtons(new Set(shown.keys()), entries);
67
+ for (const workspaceId of plan.remove) {
68
+ shown.get(workspaceId)?.remove();
69
+ shown.delete(workspaceId);
70
+ }
71
+ for (const workspaceId of plan.add) {
72
+ const registration = client.addHeaderButton({
73
+ id: BEADS_HEADER_BUTTON_ID,
74
+ workspaceId,
75
+ button: beadsHeaderButton(() => client.openPanel(BEADS_TAB_PANEL_ID, { workspaceId })),
76
+ });
77
+ shown.set(workspaceId, registration);
78
+ }
79
+ } catch {
80
+ // A failed read keeps the buttons as they are: better a button one
81
+ // period late than buttons that flicker whenever the daemon is slow.
82
+ } finally {
83
+ pending = false;
84
+ }
85
+ };
86
+
87
+ void refresh();
88
+ try {
89
+ // A newly opened workspace should not wait a whole period for its button.
90
+ unsubscribe = client.paseo.workspaces.subscribe(() => void refresh());
91
+ } catch {
92
+ // No live updates on this host: the timer still follows the list.
93
+ }
94
+ const timer = setInterval(() => void refresh(), BEADS_HEADER_POLL_MS);
95
+ // Node (tests) would otherwise stay alive for the timer; hosts without unref ignore this.
96
+ (timer as { unref?: () => void }).unref?.();
97
+
98
+ return () => {
99
+ stopped = true;
100
+ clearInterval(timer);
101
+ unsubscribe?.();
102
+ for (const registration of shown.values()) registration.remove();
103
+ shown.clear();
104
+ };
105
+ }
@@ -16,6 +16,7 @@ import { CHAT_CARD_KIND, CHAT_CARD_VERSION, chatCardSchema, toChatCard } from ".
16
16
  import { ChatCardView } from "./client/chat-card";
17
17
  import { ChatBeadsPanel } from "./client/bead-chips";
18
18
  import { BeadsTabPanel } from "./client/beads-tab";
19
+ import { registerBeadsHeaderButtons } from "./client/beads-header-button";
19
20
  import { registerWaitingPills } from "./client/waiting-pills";
20
21
 
21
22
  /** A chat item as a paseo-bm card, or nothing (the item stays Paseo's). */
@@ -212,6 +213,9 @@ export default function contribute(client: PluginClientContext): () => void {
212
213
  context: "workspace",
213
214
  Component: BeadsTabPanel,
214
215
  }),
216
+ // The same tab from a button on every workspace header: Paseo 0.8's mobile
217
+ // app has no "+" menu (delta 20260918e §4.6).
218
+ registerBeadsHeaderButtons(client),
215
219
  client.addWorkspacePanel({
216
220
  id: AGENT_TREE_PANEL_ID,
217
221
  title: "Beads agents",
@@ -3,4 +3,4 @@
3
3
  // which the build runs before packing. Design ref: docs/design/paseo-bm.md §2.4.
4
4
 
5
5
  /** Version of the paseo-bm payload, baked in at build time. */
6
- export const PLUGIN_VERSION = "0.2.0-alpha.0";
6
+ export const PLUGIN_VERSION = "0.2.0-alpha.1";