pi-extension-utils 0.3.0 → 0.3.2
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/README.md +90 -27
- package/dist/index.js +9 -140
- package/dist/src/client/index.d.ts +7 -0
- package/dist/src/client/index.js +25 -0
- package/dist/src/client/types.d.ts +17 -0
- package/dist/src/client/types.js +1 -0
- package/dist/src/config/index.d.ts +28 -0
- package/dist/src/config/index.js +222 -0
- package/dist/src/index.d.ts +10 -6
- package/dist/src/index.js +10 -6
- package/dist/src/logger/config.d.ts +11 -0
- package/dist/src/logger/config.js +27 -0
- package/dist/src/logger/index.d.ts +20 -0
- package/dist/src/logger/index.js +76 -0
- package/dist/src/{tui-chrome.js → pane/chrome.js} +22 -13
- package/dist/src/pane/overlay.d.ts +75 -0
- package/dist/src/pane/overlay.js +492 -0
- package/dist/src/reminders/client.d.ts +10 -0
- package/dist/src/reminders/client.js +49 -0
- package/dist/src/reminders/config.d.ts +5 -6
- package/dist/src/reminders/config.js +9 -23
- package/dist/src/reminders/debug.js +2 -2
- package/dist/src/reminders/host.d.ts +5 -0
- package/dist/src/reminders/host.js +298 -0
- package/dist/src/reminders/index.d.ts +3 -5
- package/dist/src/reminders/index.js +3 -308
- package/dist/src/reminders/types.d.ts +3 -0
- package/dist/src/reminders/types.js +3 -0
- package/dist/src/ui/client.d.ts +22 -0
- package/dist/src/ui/client.js +17 -0
- package/dist/src/utils-config.d.ts +24 -0
- package/dist/src/utils-config.js +38 -0
- package/dist/src/widgets/client.d.ts +27 -0
- package/dist/src/widgets/client.js +130 -0
- package/dist/src/widgets/host.d.ts +2 -0
- package/dist/src/widgets/host.js +143 -0
- package/docs/README.md +11 -0
- package/docs/client.md +91 -0
- package/docs/config.md +86 -0
- package/docs/pane-overlay.md +88 -0
- package/docs/reference/reminders-spec.md +351 -0
- package/docs/reminders.md +68 -0
- package/docs/widgets.md +57 -0
- package/examples/README.md +23 -0
- package/examples/config.ts +31 -0
- package/examples/logger.ts +20 -0
- package/examples/pane-overlay.ts +47 -0
- package/examples/reminders.ts +25 -0
- package/examples/widget-coordinator.ts +18 -0
- package/package.json +12 -1
- package/dist/src/client.d.ts +0 -47
- package/dist/src/client.js +0 -194
- package/dist/src/logger.d.ts +0 -12
- package/dist/src/logger.js +0 -45
- /package/dist/src/{tui-chrome.d.ts → pane/chrome.d.ts} +0 -0
- /package/dist/src/{key-dispatch.d.ts → pane/key-dispatch.d.ts} +0 -0
- /package/dist/src/{key-dispatch.js → pane/key-dispatch.js} +0 -0
- /package/dist/src/{pane-state.d.ts → pane/state.d.ts} +0 -0
- /package/dist/src/{pane-state.js → pane/state.js} +0 -0
- /package/dist/src/{protocol.d.ts → widgets/protocol.d.ts} +0 -0
- /package/dist/src/{protocol.js → widgets/protocol.js} +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# SPEC: Pi Reminders
|
|
2
|
+
|
|
3
|
+
## 1. Purpose
|
|
4
|
+
|
|
5
|
+
`pi-extension-utils` now contains the shared reminder host for Pi.
|
|
6
|
+
|
|
7
|
+
It centralizes dynamic guidance that producer extensions need to show the model. Packages such as `pi-dag-tasks`, `pi-dynamic-context-pruning`, and `pi-subagents` publish reminder intents over Pi's event bus. the reminders host aggregates those intents and writes compact `<system-reminder>` messages into conversation history when reminders are created, changed, forced, or due for a configured repeat interval.
|
|
8
|
+
|
|
9
|
+
The core goals are:
|
|
10
|
+
|
|
11
|
+
- producers publish intent instead of mutating transcript/tool-result content directly
|
|
12
|
+
- reminders are visible and durable like normal conversation context
|
|
13
|
+
- reminders are not repeated every provider request
|
|
14
|
+
- prompt-cache stability is preserved because old checkpointed messages are not rewritten
|
|
15
|
+
|
|
16
|
+
## 2. Problem
|
|
17
|
+
|
|
18
|
+
Several Pi extensions need to remind the agent about transient state:
|
|
19
|
+
|
|
20
|
+
- task status and ready work from `pi-dag-tasks`
|
|
21
|
+
- compression nudges, protected-tail hints, and safe ranges from `pi-dynamic-context-pruning`
|
|
22
|
+
- async subagent attention/status hints from `pi-subagents`
|
|
23
|
+
|
|
24
|
+
The unsafe old pattern was to append reminder text directly into existing message content, tool results, or rendered compression blocks. That mutates prompt text that may already be inside a cached checkpoint. With Anthropic prompt caching, a later `cache_control: { type: "ephemeral" }` marker is a cache breakpoint/write marker, not an instruction to exclude volatile text from the cache key.
|
|
25
|
+
|
|
26
|
+
A provider-only volatile trailer avoids rewriting history, but repeating the same side-channel every request can confuse the model and is brittle for custom providers that bypass provider-payload hooks. The v2 model therefore writes reminders as durable history messages only when they are due.
|
|
27
|
+
|
|
28
|
+
## 3. Product direction
|
|
29
|
+
|
|
30
|
+
Desired flow:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
pi-dag-tasks / DCP / subagents / future extensions
|
|
34
|
+
-> pi.events.emit("reminder:upsert", reminderIntent)
|
|
35
|
+
-> src/reminders/reminders manager stores, groups, sorts, and tracks announcement state
|
|
36
|
+
-> before_agent_start returns or context queues one durable <system-reminder> custom message only when due
|
|
37
|
+
-> provider receives normal conversation history
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Only the reminders host should write model-facing reminder text. Producer packages should publish intent, not mutate prompt history.
|
|
41
|
+
|
|
42
|
+
## 4. Goals
|
|
43
|
+
|
|
44
|
+
- Provide a neutral event-bus contract for extensions to publish reminder intents.
|
|
45
|
+
- Write reminders as durable conversation-history messages, not provider-only tails.
|
|
46
|
+
- Announce reminder changes, not reminder state every turn.
|
|
47
|
+
- Support one-shot, session, and persistent reminders with optional repeat intervals.
|
|
48
|
+
- Ensure equivalent reminder sets render byte-stably through deterministic ordering and formatting.
|
|
49
|
+
- Keep `source`, `id`, `priority`, `ttl`, `repeatEveryTurns`, and `metadata` internal to normal model-facing output.
|
|
50
|
+
- Avoid adding `cache_control` to reminders.
|
|
51
|
+
- Provide a migration path for existing task reminders, DCP nudges, and subagent attention messages.
|
|
52
|
+
|
|
53
|
+
## 5. Non-goals
|
|
54
|
+
|
|
55
|
+
the reminders host must not become:
|
|
56
|
+
|
|
57
|
+
- a task manager or replacement for `pi-dag-tasks`
|
|
58
|
+
- a DCP compression policy engine
|
|
59
|
+
- a subagent scheduler
|
|
60
|
+
- a generic notification/UI system
|
|
61
|
+
- a prompt-cache router for all provider content
|
|
62
|
+
- a package that adds `cache_control` to reminders
|
|
63
|
+
|
|
64
|
+
## 6. Cache and history invariants
|
|
65
|
+
|
|
66
|
+
These invariants are mandatory:
|
|
67
|
+
|
|
68
|
+
1. **Do not mutate existing transcript messages.**
|
|
69
|
+
Reminder handling must not append text to prior user, assistant, tool result, bash execution, or compressed-block messages.
|
|
70
|
+
|
|
71
|
+
2. **Do not mutate tool results.**
|
|
72
|
+
Tool outputs are often checkpointed. Reminder text must not be appended to `tool_result` content.
|
|
73
|
+
|
|
74
|
+
3. **Do not mutate DCP compression blocks.**
|
|
75
|
+
Compression blocks represent stable summaries/checkpoints. Reminders and nudges must be rendered separately.
|
|
76
|
+
|
|
77
|
+
4. **Write new history messages only when due.**
|
|
78
|
+
Do not repeat the entire active reminder set on every request. Write when created, changed, forced, or when a configured repeat interval elapses.
|
|
79
|
+
|
|
80
|
+
5. **Do not put `cache_control` on reminders.**
|
|
81
|
+
Anthropic `cache_control: { type: "ephemeral" }` is a cache breakpoint/write marker. Reminder messages are normal history and should not create cache markers.
|
|
82
|
+
|
|
83
|
+
6. **Render deterministically.**
|
|
84
|
+
For the same due reminder set, output order and formatting must be byte-stable.
|
|
85
|
+
|
|
86
|
+
## 7. Event namespace
|
|
87
|
+
|
|
88
|
+
Event names:
|
|
89
|
+
|
|
90
|
+
- `reminder:upsert`
|
|
91
|
+
- `reminder:remove`
|
|
92
|
+
- `reminder:clear-source`
|
|
93
|
+
- `reminder:list`
|
|
94
|
+
- `reminder:announce-now`
|
|
95
|
+
|
|
96
|
+
Shared constants should be exported by the package so producer extensions do not duplicate string literals.
|
|
97
|
+
|
|
98
|
+
## 8. Reminder intent contract
|
|
99
|
+
|
|
100
|
+
Initial TypeScript shape:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
export type ReminderTtl = "once" | "session" | "persistent";
|
|
104
|
+
|
|
105
|
+
export interface ReminderIntent {
|
|
106
|
+
/** Stable unique ID within the producer source. */
|
|
107
|
+
id: string;
|
|
108
|
+
|
|
109
|
+
/** Producer namespace, e.g. "pi-dag-tasks", "dcp", "pi-subagents". */
|
|
110
|
+
source: string;
|
|
111
|
+
|
|
112
|
+
/** Compact model-facing reminder text. Source/id are not rendered by default. */
|
|
113
|
+
text: string;
|
|
114
|
+
|
|
115
|
+
/** Optional compact display label, e.g. "Tasks", "DCP", "Subagents". */
|
|
116
|
+
label?: string;
|
|
117
|
+
|
|
118
|
+
/** Higher values render earlier. Default: 0. */
|
|
119
|
+
priority?: number;
|
|
120
|
+
|
|
121
|
+
/** Whether this reminder should be shown in chat. Default: true. */
|
|
122
|
+
display?: boolean;
|
|
123
|
+
|
|
124
|
+
/** Lifecycle policy. Default: "once". */
|
|
125
|
+
ttl?: ReminderTtl;
|
|
126
|
+
|
|
127
|
+
/** Optional repeat interval for persistent reminders. */
|
|
128
|
+
repeatEveryTurns?: number;
|
|
129
|
+
|
|
130
|
+
/** Optional structured metadata for debugging; not rendered. */
|
|
131
|
+
metadata?: Record<string, unknown>;
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Upsert semantics:
|
|
136
|
+
|
|
137
|
+
- key: `(source, id)`
|
|
138
|
+
- a later upsert with the same key replaces the previous reminder
|
|
139
|
+
- `source`, `id`, `priority`, `display`, `ttl`, `repeatEveryTurns`, and `metadata` are internal manager fields for replacement, sorting, lifecycle, and debugging; they are not rendered by default
|
|
140
|
+
- `text` must be short, already summarized, and safe to render as one compact line when possible
|
|
141
|
+
- `display: false` keeps the reminder model-visible in history but omits it from the chat renderer; mixed announcements display only the reminders whose `display` is not false
|
|
142
|
+
- empty or whitespace-only text removes the reminder
|
|
143
|
+
- create/change marks the reminder due for the next history announcement
|
|
144
|
+
|
|
145
|
+
TTL semantics:
|
|
146
|
+
|
|
147
|
+
- `once`: default; announce once into history, then remove
|
|
148
|
+
- `session`: announce on create/change; keep until explicit removal, `clear-source`, or session end
|
|
149
|
+
- `persistent`: announce on create/change; optionally repeat every `repeatEveryTurns`; keep until explicit removal, `clear-source`, or session end
|
|
150
|
+
|
|
151
|
+
Force semantics:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
export interface ReminderAnnounceNowRequest {
|
|
155
|
+
source?: string;
|
|
156
|
+
id?: string;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- no payload: force all active reminders
|
|
161
|
+
- `source`: force reminders from one source
|
|
162
|
+
- `source` + `id`: force one reminder
|
|
163
|
+
|
|
164
|
+
Removal/list semantics:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
export interface ReminderRemoveRequest {
|
|
168
|
+
source: string;
|
|
169
|
+
id: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface ReminderClearSourceRequest {
|
|
173
|
+
source: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface ReminderListRequest {
|
|
177
|
+
source?: string;
|
|
178
|
+
resolve: (snapshot: ReminderSnapshot) => void;
|
|
179
|
+
reject?: (error: unknown) => void;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## 9. Rendering model
|
|
184
|
+
|
|
185
|
+
The manager renders due reminders into a single compact `<system-reminder>` message. It must not render one XML block per reminder, and it must not expose internal `source`, `id`, `priority`, `ttl`, `repeatEveryTurns`, or `metadata` unless an explicit debug surface asks for it.
|
|
186
|
+
|
|
187
|
+
Recommended model-facing shape:
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
<system-reminder>
|
|
191
|
+
Tasks: 3 open / 1 active / 2 ready. Active #7 Draft SPEC. Next: review injection boundary.
|
|
192
|
+
DCP: compress older closed ranges; do not end in protected tail >= m0184. Safe: m0041-m0097.
|
|
193
|
+
Subagents: 5de6cc9f needs attention; check status before interrupting.
|
|
194
|
+
</system-reminder>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
For token efficiency, render one line per source/group where possible. Multiple reminders from the same source should be merged into that source's line, ordered internally by priority and id.
|
|
198
|
+
|
|
199
|
+
Rendering order:
|
|
200
|
+
|
|
201
|
+
1. groups by descending max `priority`
|
|
202
|
+
2. ascending compact group label/source
|
|
203
|
+
3. reminders within a group by descending `priority`, then ascending `id`
|
|
204
|
+
|
|
205
|
+
Formatting requirements:
|
|
206
|
+
|
|
207
|
+
- stable newline convention
|
|
208
|
+
- no timestamps unless supplied by the producer as stable text
|
|
209
|
+
- no random IDs
|
|
210
|
+
- no object key order leakage from metadata
|
|
211
|
+
- no `cache_control` on reminder messages
|
|
212
|
+
- no per-reminder XML attributes or wrappers in normal model-facing output
|
|
213
|
+
- escape or normalize wrapper-breaking text: producer text must not be able to close `</system-reminder>` early or create malformed reminder structure
|
|
214
|
+
|
|
215
|
+
Sanitization rules:
|
|
216
|
+
|
|
217
|
+
- trim leading/trailing whitespace from each reminder text
|
|
218
|
+
- collapse internal runs of whitespace/newlines to compact spaces unless a future API supports preformatted text
|
|
219
|
+
- replace literal `</system-reminder>` and other wrapper-breaking sequences in reminder text with safe escaped text
|
|
220
|
+
- derive labels from a small allowlist or sanitize to short alphanumeric labels plus `-`/`_`
|
|
221
|
+
- never render arbitrary metadata values in the reminder message
|
|
222
|
+
|
|
223
|
+
V2 intentionally does not support cross-source semantic deduplication. Replacement is by `(source, id)` only. Cross-source dedupe can hide important guidance and should require a separate design.
|
|
224
|
+
|
|
225
|
+
## 10. Pi integration points
|
|
226
|
+
|
|
227
|
+
the reminders host is a normal Pi extension package.
|
|
228
|
+
|
|
229
|
+
Expected entrypoint:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
export default function (pi: ExtensionAPI): void {
|
|
233
|
+
// register event listeners
|
|
234
|
+
// write due reminder history messages
|
|
235
|
+
// clean up per-session state
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Initial implementation scope:
|
|
240
|
+
|
|
241
|
+
- event constants and shared TypeScript types
|
|
242
|
+
- in-memory reminder manager with announcement state
|
|
243
|
+
- compact deterministic renderer
|
|
244
|
+
- `before_agent_start` custom-message insertion for reminders due before a run
|
|
245
|
+
- `context` detection for reminders that become due during long-running agent loops, queued with `pi.sendMessage(..., { deliverAs: "steer" })`
|
|
246
|
+
- `/remind` debug command
|
|
247
|
+
- `reminder:list` debug event
|
|
248
|
+
- tests for lifecycle, repeat behavior, forced announcements, context insertion, rendering stability, and no provider-payload injection
|
|
249
|
+
|
|
250
|
+
Primary hooks:
|
|
251
|
+
|
|
252
|
+
- `pi.on("before_agent_start", ...)`: if reminders are already due before a run starts, return one custom message with `customType: "src/reminders/reminders"`, `display: true`, and `<system-reminder>` model-visible content.
|
|
253
|
+
- `pi.on("context", ...)`: if reminders become due during an agent loop, queue one persisted custom message with `pi.sendMessage(..., { deliverAs: "steer" })` so long-running agents can receive reminders before the next LLM call without transient context mutation.
|
|
254
|
+
- `pi.registerMessageRenderer("src/reminders/reminders", ...)`: render persisted reminder messages in chat without XML wrapper tags.
|
|
255
|
+
|
|
256
|
+
Lifecycle hooks:
|
|
257
|
+
|
|
258
|
+
- `session_start` / `session_shutdown`: clear in-memory reminders
|
|
259
|
+
- `turn_start`: advance the manager's turn counter for `repeatEveryTurns`
|
|
260
|
+
|
|
261
|
+
Provider payload hooks are not the primary injection path in v2. Provider-specific payload rewriting should not be used for reminders unless a separate design explicitly reintroduces it.
|
|
262
|
+
|
|
263
|
+
Deferred until after v2:
|
|
264
|
+
|
|
265
|
+
- custom renderers
|
|
266
|
+
- cross-source semantic dedupe
|
|
267
|
+
- subagent migration unless a specific volatile model-only path requires it
|
|
268
|
+
|
|
269
|
+
## 11. Producer integration plan
|
|
270
|
+
|
|
271
|
+
### `pi-dag-tasks`
|
|
272
|
+
|
|
273
|
+
Current task reminders should move from direct injection into last user/tool-result content to `reminder:upsert`.
|
|
274
|
+
|
|
275
|
+
Recommended intent:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
{ source: "pi-dag-tasks", id: "state", label: "Tasks", text, ttl: "persistent", repeatEveryTurns: 10 }
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Task state should announce on create/change and, for the current v2 policy, repeat every 10 Pi turns. This can be tuned later.
|
|
282
|
+
|
|
283
|
+
### `pi-dynamic-context-pruning`
|
|
284
|
+
|
|
285
|
+
DCP nudges should become one-shot reminder intents.
|
|
286
|
+
|
|
287
|
+
Recommended intent:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
{ source: "dcp", id: "nudge", label: "DCP", text, ttl: "once", priority: 30 }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
DCP must stop appending nudge text to the latest visible user/assistant message. Compression block materialization remains separate from reminder rendering.
|
|
294
|
+
|
|
295
|
+
### `pi-subagents`
|
|
296
|
+
|
|
297
|
+
Async attention/status hints should become reminder intents when they are guidance rather than durable transcript events.
|
|
298
|
+
|
|
299
|
+
Recommended internal IDs:
|
|
300
|
+
|
|
301
|
+
- `async-attention:<runId>`
|
|
302
|
+
- `async-complete:<runId>`
|
|
303
|
+
|
|
304
|
+
Subagent summaries that are intended as durable conversation content are not reminders and should not use this path.
|
|
305
|
+
|
|
306
|
+
## 12. Failure behavior
|
|
307
|
+
|
|
308
|
+
Reminder handling is best-effort and must not block agent requests.
|
|
309
|
+
|
|
310
|
+
- Invalid reminder payloads should be logged/debuggable without breaking the session.
|
|
311
|
+
- Producer exceptions should not affect other reminders.
|
|
312
|
+
- If rendering fails, skip the reminder message and optionally emit a debug log.
|
|
313
|
+
- If no reminders are due, do not add an empty history message.
|
|
314
|
+
|
|
315
|
+
## 13. Testing requirements
|
|
316
|
+
|
|
317
|
+
Initial tests should cover:
|
|
318
|
+
|
|
319
|
+
- upsert replaces by `(source, id)`
|
|
320
|
+
- remove and clear-source behavior
|
|
321
|
+
- `once`, `session`, and `persistent` lifecycle behavior
|
|
322
|
+
- `persistent` repeat behavior via `repeatEveryTurns`
|
|
323
|
+
- `reminder:announce-now` for one reminder and all reminders
|
|
324
|
+
- deterministic ordering by priority/source/id
|
|
325
|
+
- compact grouped rendering with one line per source/group where possible
|
|
326
|
+
- internal source/id/priority/ttl/repeatEveryTurns metadata is not rendered in normal output
|
|
327
|
+
- wrapper-breaking text such as `</system-reminder>`, `<`, and `&` cannot break the message
|
|
328
|
+
- no reminder output when store is empty or nothing is due
|
|
329
|
+
- rendered reminder contains no `cache_control`
|
|
330
|
+
- equivalent reminder sets render byte-identically
|
|
331
|
+
- provider/context hooks do not duplicate reminder injection
|
|
332
|
+
|
|
333
|
+
## 14. Open questions
|
|
334
|
+
|
|
335
|
+
- Should `persistent` reminders ever survive session reload via persisted extension state, or remain in-memory only with producers responsible for republishing?
|
|
336
|
+
- Should `/remind` grow flags for persistent/debug reminders, or stay a simple one-shot debug command?
|
|
337
|
+
- What repeat interval should task-state reminders use, if any?
|
|
338
|
+
- Should compact labels be producer-supplied, manager-derived, or configurable?
|
|
339
|
+
- How should duplicate semantic reminders from multiple packages be resolved without hiding important guidance?
|
|
340
|
+
|
|
341
|
+
## 15. Success criteria
|
|
342
|
+
|
|
343
|
+
The first useful v2 is complete when:
|
|
344
|
+
|
|
345
|
+
- the reminders host exposes stable event names and shared types.
|
|
346
|
+
- Producer packages can publish, replace, remove, clear, list, and force reminder intents.
|
|
347
|
+
- The manager writes deterministic compact `<system-reminder>` history messages only when reminders are due.
|
|
348
|
+
- No `cache_control` is attached to reminder messages.
|
|
349
|
+
- Provider/context hooks do not inject duplicate reminder tails.
|
|
350
|
+
- `/remind <text>` creates a one-shot debug reminder.
|
|
351
|
+
- `pi-dag-tasks` and DCP can migrate their reminders/nudges away from direct message mutation.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Reminders
|
|
2
|
+
|
|
3
|
+
Reminders let producer extensions publish compact model-visible guidance.
|
|
4
|
+
|
|
5
|
+
Use reminders instead of appending text to prompts, transcript messages, or tool results.
|
|
6
|
+
|
|
7
|
+
## Producer API
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
client.reminders.upsert({
|
|
11
|
+
source: "my-extension",
|
|
12
|
+
id: "state",
|
|
13
|
+
label: "MyExt",
|
|
14
|
+
text: "One short reminder for the model.",
|
|
15
|
+
ttl: "session",
|
|
16
|
+
repeatEveryTurns: 10,
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Operations
|
|
21
|
+
|
|
22
|
+
| Call | Effect |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `upsert(intent)` | Create or replace `(source, id)` |
|
|
25
|
+
| `remove(source, id)` | Remove one reminder |
|
|
26
|
+
| `clearSource(source)` | Remove all reminders for a source |
|
|
27
|
+
| `list(source?)` | Read current snapshot |
|
|
28
|
+
| `announceNow(payload?)` | Force due reminders to announce |
|
|
29
|
+
|
|
30
|
+
## TTL
|
|
31
|
+
|
|
32
|
+
| TTL | Meaning |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `once` | Announce once, then expire |
|
|
35
|
+
| `session` | Survives until session reset/shutdown |
|
|
36
|
+
| `persistent` | Survives across sessions |
|
|
37
|
+
|
|
38
|
+
## Host config
|
|
39
|
+
|
|
40
|
+
The bundled reminder host stores package config in:
|
|
41
|
+
|
|
42
|
+
```txt
|
|
43
|
+
getAgentDir()/config/utils.jsonc
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```jsonc
|
|
47
|
+
{
|
|
48
|
+
"logging": {
|
|
49
|
+
"level": "info",
|
|
50
|
+
"maxFiles": 3,
|
|
51
|
+
"maxBytes": 1048576
|
|
52
|
+
},
|
|
53
|
+
"reminders": {
|
|
54
|
+
// Show reminders with display:false in the transcript UI for debugging.
|
|
55
|
+
"debugShowAllInTui": false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use `/reminders` to toggle the debug display setting from Pi.
|
|
61
|
+
|
|
62
|
+
## Rules
|
|
63
|
+
|
|
64
|
+
- Keep `text` short.
|
|
65
|
+
- Use stable `(source, id)` keys.
|
|
66
|
+
- Prefer replacing one reminder over creating many.
|
|
67
|
+
- Do not put secrets in reminders.
|
|
68
|
+
|
package/docs/widgets.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Widget Coordinator
|
|
2
|
+
|
|
3
|
+
Use the coordinator when more than one extension wants to render above or below the editor.
|
|
4
|
+
|
|
5
|
+
Raw `ctx.ui.setWidget()` calls from separate extensions do not give you a deterministic cross-extension order. Load timing can change which widget appears first. The coordinator gives every extension one shared ordered slot per placement, so widget order is stable.
|
|
6
|
+
|
|
7
|
+
## Register
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
client.widgets.set("belowEditor", "status", () => ({
|
|
11
|
+
render: () => ["my-extension: ready"],
|
|
12
|
+
invalidate: () => {},
|
|
13
|
+
}), { order: 10 });
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Remove
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
client.widgets.remove("belowEditor", "status");
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Placements
|
|
23
|
+
|
|
24
|
+
| Placement | Slot |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `aboveEditor` | Above the editor |
|
|
27
|
+
| `belowEditor` | Below the editor |
|
|
28
|
+
|
|
29
|
+
## Ordering
|
|
30
|
+
|
|
31
|
+
Widgets in the same placement are sorted by:
|
|
32
|
+
|
|
33
|
+
1. `order`
|
|
34
|
+
2. insertion order
|
|
35
|
+
|
|
36
|
+
Use wide gaps so other extensions can fit between yours:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
{ order: 10 } // primary status
|
|
40
|
+
{ order: 50 } // secondary details
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Fullscreen behavior
|
|
44
|
+
|
|
45
|
+
When any client holds a fullscreen lease, coordinated widgets are hidden and restored afterwards.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
await client.ui.fullscreen((tui, theme, keybindings, done) => new MyComponent(tui, theme, done));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Fallback
|
|
52
|
+
|
|
53
|
+
If the host is not ready yet, widget calls use the extension's own `ctx.ui.setWidget`. When the host announces readiness, the client clears the fallback widget and re-registers through the coordinator.
|
|
54
|
+
|
|
55
|
+
## Example
|
|
56
|
+
|
|
57
|
+
See [examples/widget-coordinator.ts](../examples/widget-coordinator.ts).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Copy an example into a Pi extension package and adjust names/state.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
| File | Shows |
|
|
8
|
+
|---|---|
|
|
9
|
+
| [widget-coordinator.ts](widget-coordinator.ts) | Ordered widgets via `client.widgets` |
|
|
10
|
+
| [config.ts](config.ts) | Extension-owned JSON/JSONC config |
|
|
11
|
+
| [pane-overlay.ts](pane-overlay.ts) | Fullscreen master/detail UI |
|
|
12
|
+
| [reminders.ts](reminders.ts) | Reminder producer calls |
|
|
13
|
+
| [logger.ts](logger.ts) | Namespaced logger |
|
|
14
|
+
|
|
15
|
+
## Local package testing
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd ../pi-extension-utils
|
|
19
|
+
npm run build
|
|
20
|
+
|
|
21
|
+
cd ../your-extension
|
|
22
|
+
npm install ../pi-extension-utils
|
|
23
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
import { defineConfig } from "pi-extension-utils";
|
|
3
|
+
|
|
4
|
+
const schema = Type.Object({
|
|
5
|
+
asyncByDefault: Type.Boolean({
|
|
6
|
+
default: false,
|
|
7
|
+
description: "Run jobs asynchronously unless the caller opts out.",
|
|
8
|
+
}),
|
|
9
|
+
maxDepth: Type.Number({
|
|
10
|
+
default: 1,
|
|
11
|
+
description: "Maximum nested job depth.",
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
type ExampleConfig = Static<typeof schema>;
|
|
16
|
+
|
|
17
|
+
const config = defineConfig({
|
|
18
|
+
name: "example-extension",
|
|
19
|
+
// Uses getAgentDir()/config/example-extension.jsonc by default.
|
|
20
|
+
schema,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function loadConfig(): ExampleConfig {
|
|
24
|
+
return config.get();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function enableAsyncByDefault(): ExampleConfig {
|
|
28
|
+
return config.update((cfg) => {
|
|
29
|
+
cfg.asyncByDefault = true;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createLogger } from "pi-extension-utils";
|
|
3
|
+
|
|
4
|
+
const log = createLogger("example-extension", {
|
|
5
|
+
level: "info",
|
|
6
|
+
maxFiles: 5,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default function (pi: ExtensionAPI) {
|
|
10
|
+
pi.on("session_start", (_event, ctx) => {
|
|
11
|
+
log.info(`session started cwd=${ctx.cwd}`);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
pi.registerCommand("example-log", {
|
|
15
|
+
description: "Write one example log line",
|
|
16
|
+
handler: async () => {
|
|
17
|
+
log.info("command ran");
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { connect, paneOverlay } from "pi-extension-utils";
|
|
3
|
+
|
|
4
|
+
interface RunRow {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
lines: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const runs: RunRow[] = [
|
|
11
|
+
{ id: "a", label: "explorer", lines: ["status: running", "task: inspect files"] },
|
|
12
|
+
{ id: "b", label: "qa", lines: ["status: done", "tests: passed"] },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export default function (pi: ExtensionAPI) {
|
|
16
|
+
pi.registerCommand("example-dashboard", {
|
|
17
|
+
description: "Open an example pane overlay",
|
|
18
|
+
handler: async (_args, ctx) => {
|
|
19
|
+
const client = connect(pi, { ctx, clientId: "example-dashboard" });
|
|
20
|
+
|
|
21
|
+
await client.ui.fullscreen(
|
|
22
|
+
paneOverlay<void, RunRow>({
|
|
23
|
+
primary: {
|
|
24
|
+
title: "Runs",
|
|
25
|
+
mode: "cursor",
|
|
26
|
+
rows: runs,
|
|
27
|
+
selectionKey: (run) => run.id,
|
|
28
|
+
renderRow: (run) => run.label,
|
|
29
|
+
},
|
|
30
|
+
detail: {
|
|
31
|
+
title: (overlay) => overlay.selectedRow?.label ?? "Details",
|
|
32
|
+
rows: (overlay) => overlay.selectedRow?.lines ?? ["No selection"],
|
|
33
|
+
},
|
|
34
|
+
split: { initialFraction: 0.35, minPrimaryWidth: 20, minDetailWidth: 30 },
|
|
35
|
+
legendPlacement: "footer",
|
|
36
|
+
customActions: [
|
|
37
|
+
{
|
|
38
|
+
keys: "enter",
|
|
39
|
+
label: "select",
|
|
40
|
+
run: (overlay) => overlay.close(),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { connect } from "pi-extension-utils";
|
|
3
|
+
|
|
4
|
+
export default function (pi: ExtensionAPI) {
|
|
5
|
+
pi.on("session_start", (_event, ctx) => {
|
|
6
|
+
const client = connect(pi, { ctx, clientId: "example-reminders" });
|
|
7
|
+
|
|
8
|
+
client.reminders.upsert({
|
|
9
|
+
source: "example-reminders",
|
|
10
|
+
id: "status",
|
|
11
|
+
label: "Example",
|
|
12
|
+
text: "Use the example project conventions.",
|
|
13
|
+
ttl: "session",
|
|
14
|
+
repeatEveryTurns: 10,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
pi.registerCommand("example-clear-reminder", {
|
|
19
|
+
description: "Clear the example reminder",
|
|
20
|
+
handler: async (_args, ctx) => {
|
|
21
|
+
const client = connect(pi, { ctx, clientId: "example-reminders" });
|
|
22
|
+
client.reminders.remove("example-reminders", "status");
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { connect } from "pi-extension-utils";
|
|
3
|
+
|
|
4
|
+
export default function (pi: ExtensionAPI) {
|
|
5
|
+
pi.on("session_start", (_event, ctx) => {
|
|
6
|
+
const client = connect(pi, { ctx, clientId: "example-widget" });
|
|
7
|
+
|
|
8
|
+
client.widgets.set(
|
|
9
|
+
"belowEditor",
|
|
10
|
+
"example-status",
|
|
11
|
+
() => ({
|
|
12
|
+
render: () => ["example widget: ready"],
|
|
13
|
+
invalidate: () => {},
|
|
14
|
+
}),
|
|
15
|
+
{ order: 10 },
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extension-utils",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Shared Pi extension utilities for coordinated widgets, fullscreen leases, and logging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
+
"docs",
|
|
17
|
+
"examples",
|
|
16
18
|
"README.md",
|
|
17
19
|
"LICENSE"
|
|
18
20
|
],
|
|
@@ -30,6 +32,10 @@
|
|
|
30
32
|
"keywords": [],
|
|
31
33
|
"author": "",
|
|
32
34
|
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/barisgit/pi-extension-utils"
|
|
38
|
+
},
|
|
33
39
|
"peerDependencies": {
|
|
34
40
|
"@earendil-works/pi-agent-core": "*",
|
|
35
41
|
"@earendil-works/pi-ai": "*",
|
|
@@ -40,7 +46,12 @@
|
|
|
40
46
|
"@earendil-works/pi-agent-core": "^0.75.4",
|
|
41
47
|
"@earendil-works/pi-ai": "^0.75.4",
|
|
42
48
|
"@earendil-works/pi-coding-agent": "^0.75.4",
|
|
49
|
+
"@earendil-works/pi-tui": "^0.79.3",
|
|
43
50
|
"@types/node": "^24.0.0",
|
|
44
51
|
"typescript": "^6.0.3"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"jsonc-parser": "^3.3.1",
|
|
55
|
+
"typebox": "^1.2.9"
|
|
45
56
|
}
|
|
46
57
|
}
|