@selesai/code 0.13.10 → 0.13.11
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 +10 -0
- package/dist/extensions/handoff-new.ts +5 -2
- package/dist/extensions/herdr-agent-state.ts +310 -0
- package/dist/extensions/package.json +1 -0
- package/dist/modes/rpc/rpc-client.d.ts +9 -1
- package/dist/modes/rpc/rpc-client.js +8 -0
- package/dist/modes/rpc/rpc-mode.js +81 -9
- package/dist/modes/rpc/rpc-types.d.ts +37 -0
- package/docs/rpc.md +56 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.11] - 2026-09-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **RPC rev-4 `get_skills` command.** New RPC command returning the resolved skill catalog (one entry per discovered skill with its effective enablement) plus the raw merged `global + project` `settings.skills` patterns, so hosts can mirror the TUI `/skills` toggle surface without reimplementing `isEnabledByOverrides` semantics. Exposed on the typed client as `RpcClient.getSkills()`.
|
|
9
|
+
- **RPC `working` loader parity.** `ctx.ui.setWorkingMessage()`, `setWorkingVisible()`, and `setWorkingIndicator()` now emit a fire-and-forget `extension_ui_request {method: "working"}` (message/visible/frames/intervalMs patch fields) instead of no-oping in RPC mode, so the host can render the streaming loader row.
|
|
10
|
+
- **Herdr agent-state extension.** New bundled extension that reports session/lifecycle state (`working`/`blocked`/`idle`) plus session references to the Herdr pane over `HERDR_SOCKET_PATH`. Instance state lives inside the factory so successor sessions after `/new`, fork, resume, or reload keep reporting; only the monotonic `reportSeq` stays module-level to satisfy Herdr's per-source sequence guard.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **handoff-new no longer fails session creation.** The `setup` callback read the session name from the (already replaced) extension ctx, throwing the stale-ctx error and aborting `newSession` with "Failed to create session". The name is now captured before `newSession()` and passed into `setup` as a plain value; the session name still carries over as before.
|
|
14
|
+
|
|
5
15
|
## [0.13.10] - 2026-09-02
|
|
6
16
|
|
|
7
17
|
### Added
|
|
@@ -108,12 +108,15 @@ async function handoffNew(args: string, ctx: ExtensionCommandContext) {
|
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// Read the name before newSession: the original ctx is invalidated once
|
|
112
|
+
// the session is replaced, so it cannot be read inside setup().
|
|
113
|
+
const sessionName = ctx.sessionManager.getSessionName();
|
|
114
|
+
|
|
111
115
|
const newSessionResult = await ctx.newSession({
|
|
112
116
|
parentSession: currentSessionFile,
|
|
113
117
|
// Carry the current session's name over to the new session as the starting point.
|
|
114
118
|
setup: (replacementManager) => {
|
|
115
|
-
|
|
116
|
-
if (name) replacementManager.appendSessionInfo(name);
|
|
119
|
+
if (sessionName) replacementManager.appendSessionInfo(sessionName);
|
|
117
120
|
},
|
|
118
121
|
withSession: async (replacementCtx) => {
|
|
119
122
|
await replacementCtx.sendUserMessage(result);
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// adapted from herdr's pi integration (herdr/src/integration/assets/pi/herdr-agent-state.ts)
|
|
2
|
+
// reports lifecycle state for selesai instead of pi: agent=selesai, source=herdr:selesai
|
|
3
|
+
// HERDR_INTEGRATION_ID=selesai
|
|
4
|
+
// HERDR_INTEGRATION_VERSION=2
|
|
5
|
+
// @ts-nocheck
|
|
6
|
+
//
|
|
7
|
+
// Instance state lives inside the factory, not at module scope. Selesai caches
|
|
8
|
+
// the extension module per cwd and re-invokes the factory for every session, so
|
|
9
|
+
// a module-scoped `released`/`queuedState`/`sendInFlight` from a previous
|
|
10
|
+
// session (handoff-new, auto-handoff, /new, resume, fork, reload) would make
|
|
11
|
+
// every successor instance silently drop its state reports and leave the pane
|
|
12
|
+
// stuck on the last reported state. Only the monotonic reportSeq stays
|
|
13
|
+
// module-level: Herdr guards pane.report_agent per source by seq and drops
|
|
14
|
+
// lower-seq reports, so the successor must never restart the counter below the
|
|
15
|
+
// previous instance's high-water mark.
|
|
16
|
+
|
|
17
|
+
import net from "node:net";
|
|
18
|
+
|
|
19
|
+
const HERDR_ENV = process.env.HERDR_ENV;
|
|
20
|
+
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
21
|
+
const socketEndpoint =
|
|
22
|
+
process.platform === "win32" && socketPath ? `\\\\.\\pipe\\${socketPath}` : socketPath;
|
|
23
|
+
const paneId = process.env.HERDR_PANE_ID;
|
|
24
|
+
const source = "herdr:selesai";
|
|
25
|
+
|
|
26
|
+
function enabled() {
|
|
27
|
+
return HERDR_ENV === "1" && !!socketPath && !!paneId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sendRequestAttempt(request: unknown, timeoutMs: number): Promise<boolean> {
|
|
31
|
+
if (!enabled()) {
|
|
32
|
+
return Promise.resolve(true);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
let done = false;
|
|
37
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
38
|
+
const finish = (delivered: boolean) => {
|
|
39
|
+
if (done) return;
|
|
40
|
+
done = true;
|
|
41
|
+
if (timeout) {
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
}
|
|
44
|
+
socket.destroy();
|
|
45
|
+
resolve(delivered);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const socket = net.createConnection(socketEndpoint!);
|
|
49
|
+
socket.on("error", () => finish(false));
|
|
50
|
+
socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
|
51
|
+
socket.on("data", () => finish(true));
|
|
52
|
+
socket.on("end", () => finish(false));
|
|
53
|
+
timeout = setTimeout(() => finish(false), timeoutMs);
|
|
54
|
+
timeout.unref?.();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function sendRequest(request: unknown): Promise<void> {
|
|
59
|
+
if (await sendRequestAttempt(request, 500)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await sendRequestAttempt(request, 1500);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type AgentState = "working" | "blocked" | "idle";
|
|
66
|
+
|
|
67
|
+
type QueuedState = {
|
|
68
|
+
state: AgentState;
|
|
69
|
+
message?: string;
|
|
70
|
+
seq: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Monotonic across all extension instances in this process. A per-session
|
|
74
|
+
// restart below the previous instance's high-water mark would be dropped by
|
|
75
|
+
// Herdr's per-source seq guard.
|
|
76
|
+
let reportSeq = Date.now() * 1000;
|
|
77
|
+
|
|
78
|
+
function nextReportSeq(): number {
|
|
79
|
+
reportSeq = Math.max(reportSeq + 1, Date.now() * 1000);
|
|
80
|
+
return reportSeq;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default function (pi) {
|
|
84
|
+
if (!enabled()) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let currentAgentSessionId: string | undefined;
|
|
89
|
+
let currentAgentSessionPath: string | undefined;
|
|
90
|
+
let sendInFlight = false;
|
|
91
|
+
let queuedState: QueuedState | undefined;
|
|
92
|
+
let activeDrain: Promise<void> = Promise.resolve();
|
|
93
|
+
let released = false;
|
|
94
|
+
let agentActive = false;
|
|
95
|
+
let blockedCount = 0;
|
|
96
|
+
let blockedMessage: string | undefined;
|
|
97
|
+
let lastState: AgentState | undefined;
|
|
98
|
+
let lastMessage: string | undefined;
|
|
99
|
+
let rootSession = false;
|
|
100
|
+
|
|
101
|
+
function updateSessionRef(ctx: any): void {
|
|
102
|
+
try {
|
|
103
|
+
const file = ctx?.sessionManager?.getSessionFile?.();
|
|
104
|
+
currentAgentSessionPath =
|
|
105
|
+
typeof file === "string" && file.startsWith("/") ? file : undefined;
|
|
106
|
+
} catch {
|
|
107
|
+
currentAgentSessionPath = undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const id = ctx?.sessionManager?.getSessionId?.();
|
|
112
|
+
currentAgentSessionId = typeof id === "string" && id.length > 0 ? id : undefined;
|
|
113
|
+
} catch {
|
|
114
|
+
currentAgentSessionId = undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function withSessionRef(params: Record<string, unknown>): Record<string, unknown> {
|
|
119
|
+
if (currentAgentSessionPath) {
|
|
120
|
+
return { ...params, agent_session_path: currentAgentSessionPath };
|
|
121
|
+
}
|
|
122
|
+
if (currentAgentSessionId) {
|
|
123
|
+
return { ...params, agent_session_id: currentAgentSessionId };
|
|
124
|
+
}
|
|
125
|
+
return params;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function currentSessionRef(): Record<string, unknown> | undefined {
|
|
129
|
+
if (currentAgentSessionPath) {
|
|
130
|
+
return { agent_session_path: currentAgentSessionPath };
|
|
131
|
+
}
|
|
132
|
+
if (currentAgentSessionId) {
|
|
133
|
+
return { agent_session_id: currentAgentSessionId };
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function reportSession(sessionStartSource?: string): Promise<void> {
|
|
139
|
+
const sessionRef = currentSessionRef();
|
|
140
|
+
if (!sessionRef) {
|
|
141
|
+
return Promise.resolve();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return sendRequest({
|
|
145
|
+
id: `${source}:session:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
146
|
+
method: "pane.report_agent_session",
|
|
147
|
+
params: {
|
|
148
|
+
pane_id: paneId,
|
|
149
|
+
source,
|
|
150
|
+
agent: "selesai",
|
|
151
|
+
seq: nextReportSeq(),
|
|
152
|
+
session_start_source: sessionStartSource,
|
|
153
|
+
...sessionRef,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sendState(state: AgentState, message?: string, seq = nextReportSeq()): Promise<void> {
|
|
159
|
+
return sendRequest({
|
|
160
|
+
id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
161
|
+
method: "pane.report_agent",
|
|
162
|
+
params: withSessionRef({
|
|
163
|
+
pane_id: paneId,
|
|
164
|
+
source,
|
|
165
|
+
agent: "selesai",
|
|
166
|
+
state,
|
|
167
|
+
message,
|
|
168
|
+
seq,
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function queueState(state: AgentState, message?: string): void {
|
|
174
|
+
if (released) {
|
|
175
|
+
// The pane was released on quit; a late report would reclaim it and
|
|
176
|
+
// leave Herdr showing an agent that already exited.
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
queuedState = { state, message, seq: nextReportSeq() };
|
|
180
|
+
if (!sendInFlight) {
|
|
181
|
+
activeDrain = drainStateQueue();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function drainStateQueue(): Promise<void> {
|
|
186
|
+
if (sendInFlight) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
sendInFlight = true;
|
|
191
|
+
try {
|
|
192
|
+
while (queuedState) {
|
|
193
|
+
const next = queuedState;
|
|
194
|
+
queuedState = undefined;
|
|
195
|
+
await sendState(next.state, next.message, next.seq);
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
sendInFlight = false;
|
|
199
|
+
if (queuedState) {
|
|
200
|
+
activeDrain = drainStateQueue();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function releaseAgent(): Promise<void> {
|
|
206
|
+
// Stop new reports, drop anything still queued, and wait for the in-flight
|
|
207
|
+
// send to finish so the release is the last write on the wire; a report
|
|
208
|
+
// landing after the release would reclaim the pane.
|
|
209
|
+
released = true;
|
|
210
|
+
queuedState = undefined;
|
|
211
|
+
await activeDrain.catch(() => undefined);
|
|
212
|
+
return sendRequest({
|
|
213
|
+
id: `${source}:release:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
214
|
+
method: "pane.release_agent",
|
|
215
|
+
params: {
|
|
216
|
+
pane_id: paneId,
|
|
217
|
+
source,
|
|
218
|
+
agent: "selesai",
|
|
219
|
+
seq: nextReportSeq(),
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function desiredState() {
|
|
225
|
+
if (blockedCount > 0) {
|
|
226
|
+
return { state: "blocked" as const, message: blockedMessage };
|
|
227
|
+
}
|
|
228
|
+
if (agentActive) {
|
|
229
|
+
return { state: "working" as const, message: undefined };
|
|
230
|
+
}
|
|
231
|
+
return { state: "idle" as const, message: undefined };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function publishState(force = false) {
|
|
235
|
+
const next = desiredState();
|
|
236
|
+
if (!force && next.state === lastState && next.message === lastMessage) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
lastState = next.state;
|
|
240
|
+
lastMessage = next.message;
|
|
241
|
+
queueState(next.state, next.message);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
pi.events.on("herdr:blocked", (data) => {
|
|
245
|
+
if (!rootSession) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!data?.active) {
|
|
249
|
+
blockedCount = Math.max(0, blockedCount - 1);
|
|
250
|
+
if (blockedCount === 0) {
|
|
251
|
+
blockedMessage = undefined;
|
|
252
|
+
}
|
|
253
|
+
publishState();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
blockedCount += 1;
|
|
258
|
+
blockedMessage = data.label;
|
|
259
|
+
publishState();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
pi.on("session_start", async (event, ctx) => {
|
|
263
|
+
// TUI only: RPC/JSON/print modes are headless (no PTY herdr can display),
|
|
264
|
+
// and RPC still reports hasUI=true, so mode is the reliable gate.
|
|
265
|
+
if (ctx?.mode !== "tui") {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
rootSession = true;
|
|
269
|
+
updateSessionRef(ctx);
|
|
270
|
+
await reportSession(event?.reason);
|
|
271
|
+
// A reload can replace this extension mid-run without emitting another agent_start.
|
|
272
|
+
agentActive = ctx?.isIdle?.() === false;
|
|
273
|
+
publishState(true);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
277
|
+
if (!rootSession) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
updateSessionRef(ctx);
|
|
281
|
+
void reportSession();
|
|
282
|
+
agentActive = true;
|
|
283
|
+
publishState();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
287
|
+
if (!rootSession || ctx?.isIdle?.() !== true) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
agentActive = false;
|
|
292
|
+
publishState();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
pi.on("session_shutdown", async (event) => {
|
|
296
|
+
if (!rootSession) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (event?.reason !== "quit") {
|
|
300
|
+
// new/resume/fork/reload: a successor instance in this same pane
|
|
301
|
+
// re-reports immediately. Releasing here races that report, and a
|
|
302
|
+
// release landing after the successor's report clears the pane.
|
|
303
|
+
// Silence this instance so no stale queued report lands around it.
|
|
304
|
+
released = true;
|
|
305
|
+
queuedState = undefined;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
await releaseAgent();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -12,7 +12,7 @@ import type { BashResult } from "../../core/bash-executor.ts";
|
|
|
12
12
|
import type { CompactionResult } from "../../core/compaction/index.ts";
|
|
13
13
|
import type { SessionEntry, SessionTreeNode } from "../../core/session-manager.ts";
|
|
14
14
|
import type { ChangelogEntry } from "../../utils/changelog.ts";
|
|
15
|
-
import type { RpcSessionInfo, RpcSessionState, RpcSlashCommand } from "./rpc-types.ts";
|
|
15
|
+
import type { RpcSessionInfo, RpcSessionState, RpcSkillInfo, RpcSlashCommand } from "./rpc-types.ts";
|
|
16
16
|
export interface RpcClientOptions {
|
|
17
17
|
/** Path to the CLI entry point (default: searches for dist/cli.js) */
|
|
18
18
|
cliPath?: string;
|
|
@@ -286,6 +286,14 @@ export declare class RpcClient {
|
|
|
286
286
|
* Get available commands (extension commands, prompt templates, builtins).
|
|
287
287
|
*/
|
|
288
288
|
getCommands(): Promise<RpcSlashCommand[]>;
|
|
289
|
+
/**
|
|
290
|
+
* Get the resolved skill catalog (per-skill enabled state) plus the raw
|
|
291
|
+
* global+project merged settings.skills patterns for round-trip toggling.
|
|
292
|
+
*/
|
|
293
|
+
getSkills(): Promise<{
|
|
294
|
+
skills: RpcSkillInfo[];
|
|
295
|
+
patterns: string[];
|
|
296
|
+
}>;
|
|
289
297
|
/**
|
|
290
298
|
* Get settings for a scope ("global" | "project" | "effective", default "effective").
|
|
291
299
|
*/
|
|
@@ -411,6 +411,14 @@ export class RpcClient {
|
|
|
411
411
|
const response = await this.send({ type: "get_commands" });
|
|
412
412
|
return this.getData(response).commands;
|
|
413
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Get the resolved skill catalog (per-skill enabled state) plus the raw
|
|
416
|
+
* global+project merged settings.skills patterns for round-trip toggling.
|
|
417
|
+
*/
|
|
418
|
+
async getSkills() {
|
|
419
|
+
const response = await this.send({ type: "get_skills" });
|
|
420
|
+
return this.getData(response);
|
|
421
|
+
}
|
|
414
422
|
// =========================================================================
|
|
415
423
|
// Settings
|
|
416
424
|
// =========================================================================
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as crypto from "node:crypto";
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
|
-
import { chmodSync, copyFileSync, existsSync, unlinkSync } from "node:fs";
|
|
15
|
+
import { chmodSync, copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
16
16
|
import { tmpdir } from "node:os";
|
|
17
|
-
import { join } from "node:path";
|
|
17
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
18
18
|
import { generateHandoff } from "../../core/handoff.js";
|
|
19
19
|
import { buildGitCommandPrompt } from "../../core/git-command.js";
|
|
20
20
|
import { KeybindingsManager } from "../../core/keybindings.js";
|
|
@@ -27,7 +27,8 @@ import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
|
|
27
27
|
import { createSyntheticSourceInfo } from "../../core/source-info.js";
|
|
28
28
|
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
|
29
29
|
import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
|
|
30
|
-
import {
|
|
30
|
+
import { parseFrontmatter } from "../../utils/frontmatter.js";
|
|
31
|
+
import { VERSION, CONFIG_DIR_NAME, getAgentDir, getBundledDefaultsDir, getShareViewerUrl } from "../../config.js";
|
|
31
32
|
import { openBrowser } from "../../utils/open-browser.js";
|
|
32
33
|
import { theme } from "../interactive/theme/theme.js";
|
|
33
34
|
import { getAvailableThemesWithPaths, setRegisteredThemes } from "../interactive/theme/theme.js";
|
|
@@ -164,14 +165,34 @@ export async function runRpcMode(runtimeHost) {
|
|
|
164
165
|
statusText: text,
|
|
165
166
|
});
|
|
166
167
|
},
|
|
167
|
-
setWorkingMessage(
|
|
168
|
-
//
|
|
168
|
+
setWorkingMessage(message) {
|
|
169
|
+
// Fire and forget - host renders the working loader row
|
|
170
|
+
output({
|
|
171
|
+
type: "extension_ui_request",
|
|
172
|
+
id: crypto.randomUUID(),
|
|
173
|
+
method: "working",
|
|
174
|
+
...(message !== undefined ? { message } : {}),
|
|
175
|
+
});
|
|
169
176
|
},
|
|
170
|
-
setWorkingVisible(
|
|
171
|
-
//
|
|
177
|
+
setWorkingVisible(visible) {
|
|
178
|
+
// Fire and forget - host shows/hides the working loader row
|
|
179
|
+
output({
|
|
180
|
+
type: "extension_ui_request",
|
|
181
|
+
id: crypto.randomUUID(),
|
|
182
|
+
method: "working",
|
|
183
|
+
visible,
|
|
184
|
+
});
|
|
172
185
|
},
|
|
173
|
-
setWorkingIndicator(
|
|
174
|
-
//
|
|
186
|
+
setWorkingIndicator(options) {
|
|
187
|
+
// Fire and forget - host customizes the working indicator. Custom frames only
|
|
188
|
+
// affect the normal streaming loader; compaction/retry keep built-in styling.
|
|
189
|
+
output({
|
|
190
|
+
type: "extension_ui_request",
|
|
191
|
+
id: crypto.randomUUID(),
|
|
192
|
+
method: "working",
|
|
193
|
+
...(options?.frames !== undefined ? { frames: options.frames } : {}),
|
|
194
|
+
...(options?.intervalMs !== undefined ? { intervalMs: options.intervalMs } : {}),
|
|
195
|
+
});
|
|
175
196
|
},
|
|
176
197
|
setHiddenThinkingLabel(_label) {
|
|
177
198
|
// Hidden thinking label not supported in RPC mode - requires TUI message rendering access
|
|
@@ -880,6 +901,57 @@ export async function runRpcMode(runtimeHost) {
|
|
|
880
901
|
return success(id, "get_commands", { commands });
|
|
881
902
|
}
|
|
882
903
|
// =================================================================
|
|
904
|
+
// Skills (resolved catalog + raw toggle patterns)
|
|
905
|
+
// =================================================================
|
|
906
|
+
case "get_skills": {
|
|
907
|
+
const resolvedSkills = session.resourceLoader.getResolvedSkills();
|
|
908
|
+
const agentDir = getAgentDir();
|
|
909
|
+
const cwd = session.sessionManager.getCwd();
|
|
910
|
+
const skills = [];
|
|
911
|
+
// Mirror the TUI skill-toggle resolution (buildSkillToggleItems):
|
|
912
|
+
// name/pattern from the SKILL.md parent dir, scope from resource metadata,
|
|
913
|
+
// enabled per isEnabledByOverrides via the resource loader.
|
|
914
|
+
for (const resource of resolvedSkills) {
|
|
915
|
+
const pathStr = resource.path;
|
|
916
|
+
const isSkillFile = basename(pathStr) === "SKILL.md";
|
|
917
|
+
const name = isSkillFile ? basename(dirname(pathStr)) : basename(pathStr);
|
|
918
|
+
const scope = resource.metadata.scope === "project" ? "project" : "user";
|
|
919
|
+
const baseDir = resource.metadata.baseDir ??
|
|
920
|
+
(scope === "project" ? join(cwd, CONFIG_DIR_NAME) : agentDir);
|
|
921
|
+
const pattern = relative(baseDir, pathStr) || name;
|
|
922
|
+
let frontmatterName;
|
|
923
|
+
let description;
|
|
924
|
+
try {
|
|
925
|
+
const raw = readFileSync(pathStr, "utf-8");
|
|
926
|
+
const { frontmatter } = parseFrontmatter(raw);
|
|
927
|
+
if (typeof frontmatter.name === "string" && frontmatter.name.trim()) {
|
|
928
|
+
frontmatterName = frontmatter.name.trim();
|
|
929
|
+
}
|
|
930
|
+
if (typeof frontmatter.description === "string" && frontmatter.description.trim()) {
|
|
931
|
+
description = frontmatter.description.trim();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
// Unreadable skill file: keep path-derived name, no description.
|
|
936
|
+
}
|
|
937
|
+
skills.push({
|
|
938
|
+
name: frontmatterName ?? name,
|
|
939
|
+
description,
|
|
940
|
+
scope: scope === "project" ? "project" : "global",
|
|
941
|
+
pattern,
|
|
942
|
+
enabled: resource.enabled,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
946
|
+
// Raw global+project merged settings.skills patterns so the caller can
|
|
947
|
+
// round-trip the exact write shape via set_settings + reload.
|
|
948
|
+
const patterns = [
|
|
949
|
+
...(session.settingsManager.getGlobalSettings().skills ?? []),
|
|
950
|
+
...(session.settingsManager.getProjectSettings().skills ?? []),
|
|
951
|
+
];
|
|
952
|
+
return success(id, "get_skills", { skills, patterns });
|
|
953
|
+
}
|
|
954
|
+
// =================================================================
|
|
883
955
|
// Settings
|
|
884
956
|
// =================================================================
|
|
885
957
|
case "get_settings": {
|
|
@@ -257,6 +257,9 @@ export type RpcCommand = {
|
|
|
257
257
|
} | {
|
|
258
258
|
id?: string;
|
|
259
259
|
type: "get_commands";
|
|
260
|
+
} | {
|
|
261
|
+
id?: string;
|
|
262
|
+
type: "get_skills";
|
|
260
263
|
};
|
|
261
264
|
/** A command available for invocation via prompt */
|
|
262
265
|
export interface RpcSlashCommand {
|
|
@@ -271,6 +274,19 @@ export interface RpcSlashCommand {
|
|
|
271
274
|
/** True for interactive-TUI-only builtins that cannot be invoked via prompt */
|
|
272
275
|
interactiveOnly?: boolean;
|
|
273
276
|
}
|
|
277
|
+
/** A resolved skill in the get_skills catalog (mirrors the TUI skill-toggle resolution). */
|
|
278
|
+
export interface RpcSkillInfo {
|
|
279
|
+
/** Skill name (frontmatter name, falling back to the SKILL.md parent directory name). */
|
|
280
|
+
name: string;
|
|
281
|
+
/** Skill description from frontmatter. */
|
|
282
|
+
description?: string;
|
|
283
|
+
/** "global" for user-level skills, "project" for project-level skills. */
|
|
284
|
+
scope: "global" | "project";
|
|
285
|
+
/** Toggle pattern relative to the skill base dir (same shape the TUI writes to settings.skills). */
|
|
286
|
+
pattern: string;
|
|
287
|
+
/** Resolved enabled state per isEnabledByOverrides semantics. */
|
|
288
|
+
enabled: boolean;
|
|
289
|
+
}
|
|
274
290
|
/** Session listing entry (SessionInfo with transcript text stripped). */
|
|
275
291
|
export interface RpcSessionInfo {
|
|
276
292
|
path: string;
|
|
@@ -555,6 +571,15 @@ export type RpcResponse = {
|
|
|
555
571
|
data: {
|
|
556
572
|
commands: RpcSlashCommand[];
|
|
557
573
|
};
|
|
574
|
+
} | {
|
|
575
|
+
id?: string;
|
|
576
|
+
type: "response";
|
|
577
|
+
command: "get_skills";
|
|
578
|
+
success: true;
|
|
579
|
+
data: {
|
|
580
|
+
skills: RpcSkillInfo[];
|
|
581
|
+
patterns: string[];
|
|
582
|
+
};
|
|
558
583
|
} | {
|
|
559
584
|
id?: string;
|
|
560
585
|
type: "response";
|
|
@@ -843,6 +868,18 @@ export type RpcExtensionUIRequest = {
|
|
|
843
868
|
id: string;
|
|
844
869
|
method: "set_editor_text";
|
|
845
870
|
text: string;
|
|
871
|
+
} | {
|
|
872
|
+
type: "extension_ui_request";
|
|
873
|
+
id: string;
|
|
874
|
+
method: "working";
|
|
875
|
+
/** Working/loading message shown during streaming. Omit to restore the default. */
|
|
876
|
+
message?: string;
|
|
877
|
+
/** Show or hide the built-in working loader row during streaming. */
|
|
878
|
+
visible?: boolean;
|
|
879
|
+
/** Custom animation frames for the normal streaming loader. Empty array hides the indicator; omit to restore the default spinner. */
|
|
880
|
+
frames?: string[];
|
|
881
|
+
/** Frame interval in milliseconds for animated indicators. */
|
|
882
|
+
intervalMs?: number;
|
|
846
883
|
};
|
|
847
884
|
/** Response to an extension UI request */
|
|
848
885
|
export type RpcExtensionUIResponse = {
|
package/docs/rpc.md
CHANGED
|
@@ -897,6 +897,38 @@ Each command has:
|
|
|
897
897
|
|
|
898
898
|
**Note**: Built-in TUI commands (`/settings`, `/hotkeys`, etc.) are included with `source: "builtin"` and `interactiveOnly: true`. They are display-only: they are handled only in interactive mode and would not execute if sent via `prompt`.
|
|
899
899
|
|
|
900
|
+
#### get_skills
|
|
901
|
+
|
|
902
|
+
Get the resolved skill catalog (one entry per discovered skill with its effective enablement) plus the raw `settings.skills` patterns for round-trip toggling. Enables the extension to mirror the TUI `/skills` toggle surface without reimplementing `isEnabledByOverrides` semantics.
|
|
903
|
+
|
|
904
|
+
```json
|
|
905
|
+
{"type": "get_skills"}
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
Response:
|
|
909
|
+
```json
|
|
910
|
+
{
|
|
911
|
+
"type": "response",
|
|
912
|
+
"command": "get_skills",
|
|
913
|
+
"success": true,
|
|
914
|
+
"data": {
|
|
915
|
+
"skills": [
|
|
916
|
+
{"name": "fix-tests", "description": "Fix failing tests", "scope": "global", "pattern": "fix-tests", "enabled": true}
|
|
917
|
+
],
|
|
918
|
+
"patterns": ["+fix-tests", "-legacy-skill"]
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
Each skill entry has:
|
|
924
|
+
- `name`: Skill name (frontmatter `name`, falling back to the SKILL.md parent directory name)
|
|
925
|
+
- `description`: Frontmatter description (optional)
|
|
926
|
+
- `scope`: `"global"` (user-level) or `"project"` (project-level)
|
|
927
|
+
- `pattern`: Path relative to the skill base dir — the same pattern the TUI writes as `+pattern`/`-pattern` into `settings.skills`
|
|
928
|
+
- `enabled`: Resolved enablement per `isEnabledByOverrides` semantics (package-manager.ts)
|
|
929
|
+
|
|
930
|
+
`patterns` is the raw merged `global + project` `settings.skills` array. To toggle a skill, write `set_settings {scope, values:{skills:[...]}}` with `+pattern`/`-pattern` entries applied, then send `reload` (the TUI reloads after every toggle).
|
|
931
|
+
|
|
900
932
|
## Events
|
|
901
933
|
|
|
902
934
|
Events are streamed to stdout as JSON lines during agent operation. Events do not generally include an `id` field; `bash_execution_update` includes the `id` of its originating `bash` command when one was provided.
|
|
@@ -1179,13 +1211,13 @@ Extensions can request user interaction via `ctx.ui.select()`, `ctx.ui.confirm()
|
|
|
1179
1211
|
There are two categories of extension UI methods:
|
|
1180
1212
|
|
|
1181
1213
|
- **Dialog methods** (`select`, `multiselect`, `confirm`, `input`, `editor`): emit an `extension_ui_request` on stdout and block until the client sends back an `extension_ui_response` on stdin with the matching `id`.
|
|
1182
|
-
- **Fire-and-forget methods** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`): emit an `extension_ui_request` on stdout but do not expect a response. The client can display the information or ignore it.
|
|
1214
|
+
- **Fire-and-forget methods** (`notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`, `working`): emit an `extension_ui_request` on stdout but do not expect a response. The client can display the information or ignore it.
|
|
1183
1215
|
|
|
1184
1216
|
If a dialog method includes a `timeout` field, the agent-side will auto-resolve with a default value when the timeout expires. The client does not need to track timeouts.
|
|
1185
1217
|
|
|
1186
1218
|
Some `ExtensionUIContext` methods are not supported or degraded in RPC mode because they require direct TUI access:
|
|
1187
1219
|
- `custom()` returns `undefined`
|
|
1188
|
-
- `setWorkingMessage()`, `
|
|
1220
|
+
- `setWorkingMessage()`, `setWorkingVisible()`, `setWorkingIndicator()` emit a `working` extension_ui_request (see below); `setFooter()`, `setHeader()`, `setEditorComponent()`, `setToolsExpanded()` are no-ops
|
|
1189
1221
|
- `getEditorText()` returns `""`
|
|
1190
1222
|
- `getToolsExpanded()` returns `false`
|
|
1191
1223
|
- `pasteToEditor()` delegates to `setEditorText()` (no paste/collapse handling)
|
|
@@ -1357,6 +1389,28 @@ Set the text in the input editor. Fire-and-forget.
|
|
|
1357
1389
|
}
|
|
1358
1390
|
```
|
|
1359
1391
|
|
|
1392
|
+
#### working
|
|
1393
|
+
|
|
1394
|
+
Control the streaming "working" loader row. Fire-and-forget; the host renders the loader. This mirrors the TUI's `setWorkingMessage()` / `setWorkingVisible()` / `setWorkingIndicator()` extension methods.
|
|
1395
|
+
|
|
1396
|
+
```json
|
|
1397
|
+
{
|
|
1398
|
+
"type": "extension_ui_request",
|
|
1399
|
+
"id": "uuid-10",
|
|
1400
|
+
"method": "working",
|
|
1401
|
+
"message": "Analyzing repository...",
|
|
1402
|
+
"visible": true,
|
|
1403
|
+
"frames": ["⠋", "⠙", "⠹"],
|
|
1404
|
+
"intervalMs": 80
|
|
1405
|
+
}
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
Fields (all optional, applied as a patch):
|
|
1409
|
+
- `message`: phase text shown next to the loader. Omit to keep (or restore to default).
|
|
1410
|
+
- `visible`: show/hide the loader row.
|
|
1411
|
+
- `frames`: custom animation frames for the normal streaming loader. An empty array hides the indicator; omitting restores the default spinner. Compaction/retry loaders keep their built-in styling.
|
|
1412
|
+
- `intervalMs`: frame interval in milliseconds for animated indicators.
|
|
1413
|
+
|
|
1360
1414
|
### Extension UI Responses (stdin)
|
|
1361
1415
|
|
|
1362
1416
|
Responses are sent for dialog methods only (`select`, `multiselect`, `confirm`, `input`, `editor`). The `id` must match the request.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.11",
|
|
4
4
|
"description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|