@vibearound/plugin-channel-sdk 0.1.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/dist/connection.d.ts +62 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +89 -0
- package/dist/connection.js.map +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/renderer.d.ts +154 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +317 -0
- package/dist/renderer.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +31 -0
- package/src/connection.ts +147 -0
- package/src/index.ts +78 -0
- package/src/renderer.ts +395 -0
- package/src/types.ts +87 -0
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlockRenderer — abstract base class for block-based message rendering.
|
|
3
|
+
*
|
|
4
|
+
* ## How it works
|
|
5
|
+
*
|
|
6
|
+
* ACP streams agent responses as a sequence of typed events:
|
|
7
|
+
* text chunk, text chunk, tool call, tool update, text chunk, …
|
|
8
|
+
*
|
|
9
|
+
* Each contiguous run of the **same kind** (text / thinking / tool) is grouped
|
|
10
|
+
* into one "block". When the kind changes, the current block is **sealed**
|
|
11
|
+
* (no more edits) and a new block starts.
|
|
12
|
+
*
|
|
13
|
+
* Blocks are rendered to the platform by subclass-implemented `sendBlock` and
|
|
14
|
+
* `editBlock`. The renderer handles:
|
|
15
|
+
*
|
|
16
|
+
* - **Debounced flushing** — batches rapid deltas before sending (avoids
|
|
17
|
+
* excessive API calls during fast streaming).
|
|
18
|
+
* - **Edit throttling** — enforces a minimum interval between edits to
|
|
19
|
+
* respect platform rate limits.
|
|
20
|
+
* - **Ordered delivery** — a `sendChain` Promise serializes all send/edit
|
|
21
|
+
* calls so messages always arrive in the correct order.
|
|
22
|
+
* - **Sentinel guard** — prevents concurrent creates for the same block.
|
|
23
|
+
* - **Verbose filtering** — thinking / tool blocks can be suppressed without
|
|
24
|
+
* creating phantom block boundaries.
|
|
25
|
+
*
|
|
26
|
+
* ## Usage
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* class MyRenderer extends BlockRenderer<string> {
|
|
30
|
+
* protected async sendBlock(channelId, kind, content) {
|
|
31
|
+
* const msg = await myApi.sendMessage(channelId, content);
|
|
32
|
+
* return msg.id;
|
|
33
|
+
* }
|
|
34
|
+
* protected async editBlock(channelId, ref, kind, content, sealed) {
|
|
35
|
+
* await myApi.editMessage(ref, content);
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* // In main.ts:
|
|
40
|
+
* const renderer = new MyRenderer({ verbose: { showThinking: false } });
|
|
41
|
+
*
|
|
42
|
+
* // When user sends a message:
|
|
43
|
+
* renderer.onPromptSent(channelId);
|
|
44
|
+
* try {
|
|
45
|
+
* await agent.prompt({ sessionId, content });
|
|
46
|
+
* await renderer.onTurnEnd(channelId);
|
|
47
|
+
* } catch (e) {
|
|
48
|
+
* await renderer.onTurnError(channelId, String(e));
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* // In the ACP client's sessionUpdate handler:
|
|
52
|
+
* renderer.onSessionUpdate(notification);
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import type { SessionNotification } from "@agentclientprotocol/sdk";
|
|
57
|
+
import type { BlockKind, BlockRendererOptions, VerboseConfig } from "./types.js";
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Internal state types
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
interface ManagedBlock<TRef> {
|
|
64
|
+
/** Channel this block belongs to. Captured at creation time. */
|
|
65
|
+
channelId: string;
|
|
66
|
+
kind: BlockKind;
|
|
67
|
+
content: string;
|
|
68
|
+
/** Platform message reference set after the first successful send. */
|
|
69
|
+
ref: TRef | null;
|
|
70
|
+
/** True while a create request is in-flight (prevents concurrent creates). */
|
|
71
|
+
creating: boolean;
|
|
72
|
+
/** True once the block will receive no more content. */
|
|
73
|
+
sealed: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface ChannelState<TRef> {
|
|
77
|
+
blocks: ManagedBlock<TRef>[];
|
|
78
|
+
flushTimer: ReturnType<typeof setTimeout> | null;
|
|
79
|
+
/** Timestamp of the last successful send or edit (for throttle calculation). */
|
|
80
|
+
lastEditMs: number;
|
|
81
|
+
/** Serializes all send/edit calls — guarantees message order. */
|
|
82
|
+
sendChain: Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Constants
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 500;
|
|
90
|
+
const DEFAULT_MIN_EDIT_INTERVAL_MS = 1000;
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// BlockRenderer
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Abstract base class for block-based rendering of ACP session streams.
|
|
98
|
+
*
|
|
99
|
+
* @typeParam TRef - Platform-specific message reference type (e.g. `number`
|
|
100
|
+
* for Telegram message IDs, `string` for Feishu message IDs). Used as the
|
|
101
|
+
* return type of `sendBlock` and the first argument of `editBlock`.
|
|
102
|
+
*/
|
|
103
|
+
export abstract class BlockRenderer<TRef = string> {
|
|
104
|
+
protected readonly flushIntervalMs: number;
|
|
105
|
+
protected readonly minEditIntervalMs: number;
|
|
106
|
+
protected readonly verbose: VerboseConfig;
|
|
107
|
+
|
|
108
|
+
private states = new Map<string, ChannelState<TRef>>();
|
|
109
|
+
|
|
110
|
+
constructor(options: BlockRendererOptions = {}) {
|
|
111
|
+
this.flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
112
|
+
this.minEditIntervalMs = options.minEditIntervalMs ?? DEFAULT_MIN_EDIT_INTERVAL_MS;
|
|
113
|
+
this.verbose = {
|
|
114
|
+
showThinking: options.verbose?.showThinking ?? false,
|
|
115
|
+
showToolUse: options.verbose?.showToolUse ?? false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Abstract / overridable — subclass implements these
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Send a new block message to the platform.
|
|
125
|
+
*
|
|
126
|
+
* Return the platform message reference that will be passed to future
|
|
127
|
+
* `editBlock` calls. Return `null` if editing is not supported.
|
|
128
|
+
*/
|
|
129
|
+
protected abstract sendBlock(
|
|
130
|
+
channelId: string,
|
|
131
|
+
kind: BlockKind,
|
|
132
|
+
content: string,
|
|
133
|
+
): Promise<TRef | null>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Edit an existing block message in-place.
|
|
137
|
+
*
|
|
138
|
+
* Optional — if not implemented, blocks are never edited (send-only mode,
|
|
139
|
+
* suitable for platforms like WeChat that don't support message editing).
|
|
140
|
+
*
|
|
141
|
+
* @param sealed - `true` when this is the final edit (block done streaming).
|
|
142
|
+
* Use to switch from a "streaming" card format to a finalized one.
|
|
143
|
+
*/
|
|
144
|
+
protected editBlock?(
|
|
145
|
+
channelId: string,
|
|
146
|
+
ref: TRef,
|
|
147
|
+
kind: BlockKind,
|
|
148
|
+
content: string,
|
|
149
|
+
sealed: boolean,
|
|
150
|
+
): Promise<void>;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Format block content before sending or editing.
|
|
154
|
+
*
|
|
155
|
+
* Default applies standard emoji prefixes:
|
|
156
|
+
* - `thinking` → `💭 <content>`
|
|
157
|
+
* - `tool` → trimmed content
|
|
158
|
+
* - `text` → content as-is
|
|
159
|
+
*
|
|
160
|
+
* Override to apply platform-specific formatting (e.g. markdown escaping).
|
|
161
|
+
*/
|
|
162
|
+
protected formatContent(kind: BlockKind, content: string, _sealed: boolean): string {
|
|
163
|
+
switch (kind) {
|
|
164
|
+
case "thinking": return `💭 ${content}`;
|
|
165
|
+
case "tool": return content.trim();
|
|
166
|
+
case "text": return content;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Called after the last block has been flushed and the turn is complete.
|
|
172
|
+
* Override to perform cleanup (e.g. remove a "typing" indicator, a
|
|
173
|
+
* processing reaction, etc.).
|
|
174
|
+
*/
|
|
175
|
+
protected onAfterTurnEnd(_channelId: string): Promise<void> {
|
|
176
|
+
return Promise.resolve();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Called after a turn error, once state has been cleaned up.
|
|
181
|
+
* Override to send an error message to the user.
|
|
182
|
+
*/
|
|
183
|
+
protected onAfterTurnError(_channelId: string, _error: string): Promise<void> {
|
|
184
|
+
return Promise.resolve();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Map an ACP `sessionId` to the channel ID used internally.
|
|
189
|
+
*
|
|
190
|
+
* Default: identity (sessionId === channelId).
|
|
191
|
+
*
|
|
192
|
+
* Override if your plugin namespaces channel IDs (e.g. Feishu uses
|
|
193
|
+
* `"feishu:<sessionId>"`, WeChat uses `"weixin-openclaw-bridge:<sessionId>"`).
|
|
194
|
+
*/
|
|
195
|
+
protected sessionIdToChannelId(sessionId: string): string {
|
|
196
|
+
return sessionId;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Public API
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Process an ACP `sessionUpdate` notification from the host.
|
|
205
|
+
*
|
|
206
|
+
* Routes the event to the correct block based on its variant, appending
|
|
207
|
+
* deltas to the current block or starting a new one when the kind changes.
|
|
208
|
+
*
|
|
209
|
+
* Call this from the ACP `Client.sessionUpdate` handler.
|
|
210
|
+
*/
|
|
211
|
+
onSessionUpdate(notification: SessionNotification): void {
|
|
212
|
+
const sessionId = notification.sessionId;
|
|
213
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
214
|
+
const update = notification.update as any;
|
|
215
|
+
const variant = update.sessionUpdate as string;
|
|
216
|
+
const channelId = this.sessionIdToChannelId(sessionId);
|
|
217
|
+
|
|
218
|
+
switch (variant) {
|
|
219
|
+
case "agent_message_chunk": {
|
|
220
|
+
const delta = (update.content?.text ?? "") as string;
|
|
221
|
+
if (delta) this.appendToBlock(channelId, "text", delta);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case "agent_thought_chunk": {
|
|
225
|
+
if (!this.verbose.showThinking) return; // skip — no block, no boundary
|
|
226
|
+
const delta = (update.content?.text ?? "") as string;
|
|
227
|
+
if (delta) this.appendToBlock(channelId, "thinking", delta);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case "tool_call": {
|
|
231
|
+
if (!this.verbose.showToolUse) return; // skip
|
|
232
|
+
const title = update.title as string | undefined;
|
|
233
|
+
if (title) this.appendToBlock(channelId, "tool", `🔧 ${title}\n`);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case "tool_call_update": {
|
|
237
|
+
if (!this.verbose.showToolUse) return; // skip
|
|
238
|
+
const title = (update.title ?? "tool") as string;
|
|
239
|
+
const status = update.status as string | undefined;
|
|
240
|
+
if (status === "completed" || status === "error") {
|
|
241
|
+
this.appendToBlock(channelId, "tool", `✅ ${title}\n`);
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Call this before sending a prompt to the agent.
|
|
250
|
+
*
|
|
251
|
+
* Clears any leftover state from a previous turn so the new turn starts
|
|
252
|
+
* with a clean slate.
|
|
253
|
+
*/
|
|
254
|
+
onPromptSent(channelId: string): void {
|
|
255
|
+
const old = this.states.get(channelId);
|
|
256
|
+
if (old?.flushTimer) clearTimeout(old.flushTimer);
|
|
257
|
+
this.states.set(channelId, {
|
|
258
|
+
blocks: [],
|
|
259
|
+
flushTimer: null,
|
|
260
|
+
lastEditMs: 0,
|
|
261
|
+
sendChain: Promise.resolve(),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Call this after `agent.prompt()` resolves (turn complete).
|
|
267
|
+
*
|
|
268
|
+
* Seals and flushes the last block, then waits for all pending sends/edits
|
|
269
|
+
* to complete before calling `onAfterTurnEnd`.
|
|
270
|
+
*/
|
|
271
|
+
async onTurnEnd(channelId: string): Promise<void> {
|
|
272
|
+
const state = this.states.get(channelId);
|
|
273
|
+
if (!state) return;
|
|
274
|
+
|
|
275
|
+
if (state.flushTimer) {
|
|
276
|
+
clearTimeout(state.flushTimer);
|
|
277
|
+
state.flushTimer = null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const last = state.blocks.at(-1);
|
|
281
|
+
if (last && !last.sealed) {
|
|
282
|
+
last.sealed = true;
|
|
283
|
+
this.enqueueFlush(state, last);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Wait for the entire chain to drain before cleanup
|
|
287
|
+
await state.sendChain;
|
|
288
|
+
this.states.delete(channelId);
|
|
289
|
+
await this.onAfterTurnEnd(channelId);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Call this when `agent.prompt()` throws (turn error).
|
|
294
|
+
*
|
|
295
|
+
* Discards pending state and calls `onAfterTurnError` so the subclass can
|
|
296
|
+
* send an error message to the user.
|
|
297
|
+
*/
|
|
298
|
+
async onTurnError(channelId: string, error: string): Promise<void> {
|
|
299
|
+
const state = this.states.get(channelId);
|
|
300
|
+
if (state?.flushTimer) clearTimeout(state.flushTimer);
|
|
301
|
+
this.states.delete(channelId);
|
|
302
|
+
await this.onAfterTurnError(channelId, error);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Internal — block management
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
private appendToBlock(channelId: string, kind: BlockKind, delta: string): void {
|
|
310
|
+
let state = this.states.get(channelId);
|
|
311
|
+
if (!state) {
|
|
312
|
+
// Auto-create state if onPromptSent wasn't called (e.g. host-initiated turns)
|
|
313
|
+
state = { blocks: [], flushTimer: null, lastEditMs: 0, sendChain: Promise.resolve() };
|
|
314
|
+
this.states.set(channelId, state);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const last = state.blocks.at(-1);
|
|
318
|
+
|
|
319
|
+
if (last && !last.sealed && last.kind === kind) {
|
|
320
|
+
// Same kind — accumulate
|
|
321
|
+
last.content += delta;
|
|
322
|
+
} else {
|
|
323
|
+
// Kind changed — seal current block and start a new one
|
|
324
|
+
if (last && !last.sealed) {
|
|
325
|
+
last.sealed = true;
|
|
326
|
+
// Clear the debounce timer: we're doing an immediate flush of the sealed block
|
|
327
|
+
if (state.flushTimer) {
|
|
328
|
+
clearTimeout(state.flushTimer);
|
|
329
|
+
state.flushTimer = null;
|
|
330
|
+
}
|
|
331
|
+
this.enqueueFlush(state, last);
|
|
332
|
+
}
|
|
333
|
+
state.blocks.push({ channelId, kind, content: delta, ref: null, creating: false, sealed: false });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this.scheduleFlush(channelId, state);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private scheduleFlush(channelId: string, state: ChannelState<TRef>): void {
|
|
340
|
+
if (state.flushTimer) return; // already scheduled
|
|
341
|
+
|
|
342
|
+
state.flushTimer = setTimeout(() => {
|
|
343
|
+
state.flushTimer = null;
|
|
344
|
+
this.flush(channelId, state);
|
|
345
|
+
}, this.flushIntervalMs);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private flush(channelId: string, state: ChannelState<TRef>): void {
|
|
349
|
+
const block = state.blocks.at(-1);
|
|
350
|
+
if (!block || block.sealed || !block.content) return;
|
|
351
|
+
|
|
352
|
+
const now = Date.now();
|
|
353
|
+
if (now - state.lastEditMs < this.minEditIntervalMs) {
|
|
354
|
+
// Throttled — reschedule for the remaining window
|
|
355
|
+
const delay = this.minEditIntervalMs - (now - state.lastEditMs);
|
|
356
|
+
if (!state.flushTimer) {
|
|
357
|
+
state.flushTimer = setTimeout(() => {
|
|
358
|
+
state.flushTimer = null;
|
|
359
|
+
this.flush(channelId, state);
|
|
360
|
+
}, delay);
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
this.enqueueFlush(state, block);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private enqueueFlush(state: ChannelState<TRef>, block: ManagedBlock<TRef>): void {
|
|
369
|
+
state.sendChain = state.sendChain
|
|
370
|
+
.then(() => this.flushBlock(state, block))
|
|
371
|
+
.catch(() => {}); // errors are handled inside flushBlock
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private async flushBlock(state: ChannelState<TRef>, block: ManagedBlock<TRef>): Promise<void> {
|
|
375
|
+
const content = this.formatContent(block.kind, block.content, block.sealed);
|
|
376
|
+
if (!content) return;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
if (block.ref === null && !block.creating) {
|
|
380
|
+
// First send — use sentinel to prevent concurrent creates
|
|
381
|
+
block.creating = true;
|
|
382
|
+
block.ref = await this.sendBlock(block.channelId, block.kind, content);
|
|
383
|
+
block.creating = false;
|
|
384
|
+
state.lastEditMs = Date.now();
|
|
385
|
+
} else if (block.ref !== null && !block.creating && this.editBlock) {
|
|
386
|
+
// Subsequent update — edit in-place
|
|
387
|
+
await this.editBlock(block.channelId, block.ref, block.kind, content, block.sealed);
|
|
388
|
+
state.lastEditMs = Date.now();
|
|
389
|
+
}
|
|
390
|
+
// else: create is in-flight (creating === true) — skip
|
|
391
|
+
} catch {
|
|
392
|
+
block.creating = false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for VibeAround channel plugins.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the ACP SDK types plugins commonly need, plus SDK-specific types
|
|
5
|
+
* for block rendering, plugin manifests, and verbose configuration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export ACP SDK types so plugin authors only need one import
|
|
9
|
+
export type {
|
|
10
|
+
Agent,
|
|
11
|
+
Client,
|
|
12
|
+
SessionNotification,
|
|
13
|
+
RequestPermissionRequest,
|
|
14
|
+
RequestPermissionResponse,
|
|
15
|
+
} from "@agentclientprotocol/sdk";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Plugin manifest
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export interface PluginCapabilities {
|
|
22
|
+
/** Plugin supports real-time streaming updates. */
|
|
23
|
+
streaming?: boolean;
|
|
24
|
+
/** Platform supports rich interactive cards (e.g. Feishu). */
|
|
25
|
+
interactiveCards?: boolean;
|
|
26
|
+
/** Platform supports editing already-sent messages. */
|
|
27
|
+
editMessage?: boolean;
|
|
28
|
+
/** Platform supports file upload/download. */
|
|
29
|
+
media?: boolean;
|
|
30
|
+
auth?: { methods?: string[] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Shape of plugin.json — the plugin manifest file. */
|
|
34
|
+
export interface PluginManifest {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
kind: "channel";
|
|
38
|
+
runtime: "node";
|
|
39
|
+
entry: string;
|
|
40
|
+
build?: string;
|
|
41
|
+
configSchema?: Record<string, unknown>;
|
|
42
|
+
capabilities?: PluginCapabilities;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Block rendering
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/** The three kinds of content blocks a plugin renders. */
|
|
50
|
+
export type BlockKind = "text" | "thinking" | "tool";
|
|
51
|
+
|
|
52
|
+
export interface VerboseConfig {
|
|
53
|
+
/** Show agent thinking/reasoning blocks. Default: false. */
|
|
54
|
+
showThinking: boolean;
|
|
55
|
+
/** Show tool call / tool result blocks. Default: false. */
|
|
56
|
+
showToolUse: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface BlockRendererOptions {
|
|
60
|
+
/**
|
|
61
|
+
* Debounce interval before flushing an unsealed block (ms).
|
|
62
|
+
* Controls how often in-progress blocks are sent to the platform.
|
|
63
|
+
* Default: 500.
|
|
64
|
+
*/
|
|
65
|
+
flushIntervalMs?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Minimum interval between consecutive edits to the same message (ms).
|
|
68
|
+
* Prevents hitting platform API rate limits.
|
|
69
|
+
* Default: 1000.
|
|
70
|
+
*/
|
|
71
|
+
minEditIntervalMs?: number;
|
|
72
|
+
verbose?: Partial<VerboseConfig>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Init / config
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Plugin config and metadata passed by the host in `_meta` during initialize.
|
|
81
|
+
*/
|
|
82
|
+
export interface PluginInitMeta {
|
|
83
|
+
/** Plugin-specific config object from settings.json. */
|
|
84
|
+
config: Record<string, unknown>;
|
|
85
|
+
/** Host-provided cache directory path for temporary files. */
|
|
86
|
+
cacheDir?: string;
|
|
87
|
+
}
|