@gpambrozio/paseo-skills 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Every version
7
7
  as `@gpambrozio/paseo-skills` and tagged here, so a version is something to install and a line to
8
8
  read before you move.
9
9
 
10
+ ## [0.3.0] — 2026-09-26
11
+
12
+ ### Changed
13
+
14
+ - **The Skills pill opens a popover above the composer instead of a new tab.** It lists the same
15
+ skills the Skills tab does, with the same search. Pick one to see a short summary of it, type any
16
+ arguments and press **Invoke** — it is sent exactly as it is from the tab, and the popover closes
17
+ so you can watch the agent start. **← All skills** goes back to the list, and **Open tab** at the
18
+ top right opens the full Skills tab as the pill used to, where the whole skill is shown. The pill
19
+ still shows how many skills there are.
20
+
21
+ ## [0.2.1] — 2026-09-23
22
+
23
+ ### Fixed
24
+
25
+ - **The Skills pill reaches agents created after the plugin loaded.** Paseo 0.9 changed
26
+ `agents.subscribe()` to a local listener that no longer asks the daemon for agent data, so the
27
+ pill only covered agents that existed at load; a new session had none until the plugin was
28
+ reloaded. On 0.9 clients the pill now opens its own agent observation
29
+ (`agents.list({ subscribe: {} })`) and follows its snapshots and updates; an 0.8 client keeps the
30
+ previous listen-and-seed behaviour, because sending `subscribe` from there would replace the
31
+ app's own agent subscription.
32
+
10
33
  ## [0.2.0] — 2026-09-22
11
34
 
12
35
  ### Added
package/README.md CHANGED
@@ -49,9 +49,10 @@ plugin.
49
49
 
50
50
  ## Use
51
51
 
52
- Press the **Skills** pill above an agent's composer — its badge counts what the panel will list.
53
- The Command Center reaches the same panel: focus a workspace tab holding an agent, press ⌘K, and
54
- pick **Skills**.
52
+ Press the **Skills** pill above an agent's composer — its badge counts what it will list. It opens
53
+ a popover over the composer with the agent's skills: pick one to read it and invoke it with
54
+ arguments, or press **Open tab** at its top right for the full Skills panel. The Command Center
55
+ reaches the same panel: focus a workspace tab holding an agent, press ⌘K, and pick **Skills**.
55
56
 
56
57
  ## Demo
57
58
 
@@ -87,7 +88,9 @@ A failed reload stays failed; Paseo does not restore the previous code.
87
88
  | `server/resolve/skill-entry.ts` | Entry types, skill id construction, first-wins dedupe. |
88
89
  | `server/resolve/frontmatter.ts` | `SKILL.md` frontmatter parsing. |
89
90
  | `server/resolve/reported.ts` | Splits session-reported entries discovery did not find. |
90
- | `client/panel.tsx` | The panel: list, search, detail, invoke. |
91
+ | `client/browser.tsx` | List, search, detail, invoke — drawn by the panel and the popover. |
92
+ | `client/panel.tsx` | The panel: the browser as a whole tab. |
93
+ | `client/popover.tsx` | The pill's popover: the browser, plus a button to the panel. |
91
94
  | `client/pill.tsx` | The composer pill and the registration loop that owns it. |
92
95
  | `client/skills-query.tsx` | The `skills.list` query the panel and the pill share. |
93
96
 
@@ -0,0 +1,104 @@
1
+ import type { PluginClientContext } from "@getpaseo/plugin/client";
2
+
3
+ /**
4
+ * Following the host's agents across Paseo 0.8 and 0.9 clients.
5
+ *
6
+ * Since 0.9, `agents.subscribe()` only adds a local listener to observations
7
+ * that the same API instance opened with `agents.list({ subscribe: {} })`; on
8
+ * its own it never hears anything. The observation delivers a snapshot first
9
+ * and again after every reconnect, then the updates in between.
10
+ *
11
+ * A 0.8 client has no observations, and must not send `subscribe` either: the
12
+ * daemon keeps one agents subscription slot per legacy connection, last query
13
+ * wins, so a plugin asking for one would replace the app's own. The choice is
14
+ * therefore made before any request, from the API's shape.
15
+ */
16
+
17
+ type Paseo = PluginClientContext["paseo"];
18
+ type AgentListOptions = NonNullable<Parameters<Paseo["agents"]["list"]>[0]>;
19
+ export type AgentList = Awaited<ReturnType<Paseo["agents"]["list"]>>;
20
+ export type AgentUpdate = Parameters<Parameters<Paseo["agents"]["subscribe"]>[0]>[0];
21
+
22
+ /** The 0.9 observation handle; the 0.8 typings this plugin builds against predate it. */
23
+ type AgentObservation = {
24
+ subscribe(observer: {
25
+ snapshot(snapshot: AgentList): void;
26
+ update(message: { type: string; payload?: unknown }): void;
27
+ error?(error: unknown): void;
28
+ }): () => void;
29
+ release(): Promise<void>;
30
+ };
31
+ type ObservingAgents = {
32
+ list(
33
+ options: AgentListOptions & { subscribe: {}; signal: AbortSignal },
34
+ ): Promise<AgentList & { subscription?: AgentObservation }>;
35
+ };
36
+
37
+ const RETRY_MIN_MS = 2_000;
38
+ const RETRY_MAX_MS = 60_000;
39
+
40
+ /** `observeEvents` shipped together with observations (0.9.0-beta.1). */
41
+ export function canObserveAgents(paseo: Paseo): boolean {
42
+ return typeof (paseo as { observeEvents?: unknown }).observeEvents === "function";
43
+ }
44
+
45
+ /**
46
+ * On a 0.9 client, keeps an agent observation open for the caller's lifetime:
47
+ * `snapshot` replaces the caller's view, `update` applies one change. Paseo
48
+ * releases an observation that fails (a re-request after reconnect, say), so it
49
+ * is reopened with backoff rather than left silent. On a 0.8 client it runs
50
+ * `legacy` instead, the pre-0.9 listener and read. Returns the cleanup.
51
+ */
52
+ export function followAgents(
53
+ paseo: Paseo,
54
+ handlers: { snapshot(list: AgentList): void; update(update: AgentUpdate): void },
55
+ legacy: () => () => void,
56
+ ): () => void {
57
+ if (!canObserveAgents(paseo)) return legacy();
58
+
59
+ const lifetime = new AbortController();
60
+ let observation: AgentObservation | null = null;
61
+ let retry: ReturnType<typeof setTimeout> | null = null;
62
+ let retryDelay = RETRY_MIN_MS;
63
+
64
+ function reopen(error: unknown): void {
65
+ observation = null;
66
+ if (lifetime.signal.aborted || retry !== null) return;
67
+ console.warn("skills: agent observation failed; reopening", error);
68
+ retry = setTimeout(() => {
69
+ retry = null;
70
+ open();
71
+ }, retryDelay);
72
+ retryDelay = Math.min(retryDelay * 2, RETRY_MAX_MS);
73
+ }
74
+
75
+ function open(): void {
76
+ (paseo.agents as unknown as ObservingAgents)
77
+ .list({ subscribe: {}, signal: lifetime.signal })
78
+ .then(({ subscription }) => {
79
+ if (lifetime.signal.aborted) return;
80
+ if (subscription === undefined) throw new Error("the host returned no agent observation");
81
+ observation = subscription;
82
+ subscription.subscribe({
83
+ snapshot(list) {
84
+ retryDelay = RETRY_MIN_MS;
85
+ handlers.snapshot(list);
86
+ },
87
+ update(message) {
88
+ if (message.type === "agent_update") handlers.update(message.payload as AgentUpdate);
89
+ },
90
+ error: reopen,
91
+ });
92
+ })
93
+ .catch(reopen);
94
+ }
95
+
96
+ open();
97
+ return () => {
98
+ lifetime.abort();
99
+ if (retry !== null) clearTimeout(retry);
100
+ retry = null;
101
+ void observation?.release().catch(() => undefined);
102
+ observation = null;
103
+ };
104
+ }