@selesai/code 0.13.8 → 0.13.9
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 +5 -0
- package/dist/core/settings-manager.d.ts +11 -0
- package/dist/core/settings-manager.js +48 -0
- package/dist/extensions/auto-session-name.test.ts +13 -1
- package/dist/extensions/auto-session-name.ts +8 -4
- package/dist/modes/rpc/rpc-client.d.ts +130 -4
- package/dist/modes/rpc/rpc-client.js +156 -4
- package/dist/modes/rpc/rpc-mode.js +484 -1
- package/dist/modes/rpc/rpc-types.d.ts +249 -1
- package/docs/rpc.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.9] - 2026-09-01
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Auto session naming only names unnamed sessions.** The bundled `auto-session-name` extension now skips naming entirely when the current session already has a name (user-set via `/name` or generated earlier), instead of re-naming on every user message. Previously generated names are never clobbered; empty sessions are still auto-named as before.
|
|
9
|
+
|
|
5
10
|
## [0.13.8] - 2026-09-01
|
|
6
11
|
|
|
7
12
|
### Changed
|
|
@@ -178,6 +178,17 @@ export declare class SettingsManager {
|
|
|
178
178
|
private static migrateSettings;
|
|
179
179
|
getGlobalSettings(): Settings;
|
|
180
180
|
getProjectSettings(): Settings;
|
|
181
|
+
/** Merged effective settings (project overrides global, nested objects merge recursively). */
|
|
182
|
+
getEffectiveSettings(): Settings;
|
|
183
|
+
/**
|
|
184
|
+
* Bulk-update global settings and persist. Nested object fields merge
|
|
185
|
+
* recursively; undefined values are ignored (use null to clear a field).
|
|
186
|
+
*/
|
|
187
|
+
setGlobalSettings(values: Partial<Settings>): void;
|
|
188
|
+
/**
|
|
189
|
+
* Bulk-update project settings and persist. Throws when the project is not trusted.
|
|
190
|
+
*/
|
|
191
|
+
setProjectSettings(values: Partial<Settings>): void;
|
|
181
192
|
isProjectTrusted(): boolean;
|
|
182
193
|
setProjectTrusted(trusted: boolean): void;
|
|
183
194
|
reload(): Promise<void>;
|
|
@@ -263,6 +263,54 @@ export class SettingsManager {
|
|
|
263
263
|
getProjectSettings() {
|
|
264
264
|
return structuredClone(this.projectSettings);
|
|
265
265
|
}
|
|
266
|
+
/** Merged effective settings (project overrides global, nested objects merge recursively). */
|
|
267
|
+
getEffectiveSettings() {
|
|
268
|
+
return structuredClone(this.settings);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Bulk-update global settings and persist. Nested object fields merge
|
|
272
|
+
* recursively; undefined values are ignored (use null to clear a field).
|
|
273
|
+
*/
|
|
274
|
+
setGlobalSettings(values) {
|
|
275
|
+
const merged = deepMergeSettings(this.globalSettings, values);
|
|
276
|
+
this.globalSettings = merged;
|
|
277
|
+
for (const field of Object.keys(values)) {
|
|
278
|
+
const value = values[field];
|
|
279
|
+
if (value === undefined)
|
|
280
|
+
continue;
|
|
281
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
282
|
+
for (const nestedKey of Object.keys(value)) {
|
|
283
|
+
this.markModified(field, nestedKey);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
this.markModified(field);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
this.save();
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Bulk-update project settings and persist. Throws when the project is not trusted.
|
|
294
|
+
*/
|
|
295
|
+
setProjectSettings(values) {
|
|
296
|
+
this.assertProjectTrustedForWrite();
|
|
297
|
+
const projectSettings = deepMergeSettings(this.projectSettings, values);
|
|
298
|
+
this.projectSettings = projectSettings;
|
|
299
|
+
for (const field of Object.keys(values)) {
|
|
300
|
+
const value = values[field];
|
|
301
|
+
if (value === undefined)
|
|
302
|
+
continue;
|
|
303
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
304
|
+
for (const nestedKey of Object.keys(value)) {
|
|
305
|
+
this.markProjectModified(field, nestedKey);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
this.markProjectModified(field);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
this.saveProjectSettings(this.projectSettings);
|
|
313
|
+
}
|
|
266
314
|
isProjectTrusted() {
|
|
267
315
|
return this.projectTrusted;
|
|
268
316
|
}
|
|
@@ -35,9 +35,11 @@ function userEntry(text: string, id: string): SessionEntry {
|
|
|
35
35
|
function createHarness(branch: SessionEntry[] = []) {
|
|
36
36
|
const handlers = new Map<string, Handler>();
|
|
37
37
|
const setSessionName = vi.fn();
|
|
38
|
+
const getSessionName = vi.fn(() => undefined);
|
|
38
39
|
const pi = {
|
|
39
40
|
on: vi.fn((event: string, handler: Handler) => handlers.set(event, handler)),
|
|
40
41
|
setSessionName,
|
|
42
|
+
getSessionName,
|
|
41
43
|
} as unknown as ExtensionAPI;
|
|
42
44
|
const model = { provider: "openai", id: "gpt-5", name: "GPT-5" };
|
|
43
45
|
const ctx = {
|
|
@@ -49,7 +51,7 @@ function createHarness(branch: SessionEntry[] = []) {
|
|
|
49
51
|
} as unknown as ExtensionContext;
|
|
50
52
|
|
|
51
53
|
autoSessionNameExtension(pi);
|
|
52
|
-
return { handlers, setSessionName, ctx };
|
|
54
|
+
return { handlers, setSessionName, getSessionName, ctx };
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
function textResponse(text: string) {
|
|
@@ -70,6 +72,16 @@ describe("auto-session-name", () => {
|
|
|
70
72
|
expect(handlers.has("input")).toBe(true);
|
|
71
73
|
});
|
|
72
74
|
|
|
75
|
+
it("skips naming when the session already has a name", async () => {
|
|
76
|
+
const { handlers, getSessionName, setSessionName, ctx } = createHarness();
|
|
77
|
+
getSessionName.mockReturnValue("Existing name");
|
|
78
|
+
|
|
79
|
+
await handlers.get("input")!({ type: "input", text: "hi", source: "interactive" }, ctx);
|
|
80
|
+
|
|
81
|
+
expect(completeMock).not.toHaveBeenCalled();
|
|
82
|
+
expect(setSessionName).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
73
85
|
it("names the session from the user prompt with a small system prompt and 10-token cap", async () => {
|
|
74
86
|
const { handlers, setSessionName, ctx } = createHarness();
|
|
75
87
|
completeMock.mockResolvedValue(textResponse("Refactor auth"));
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* auto-session-name
|
|
3
3
|
*
|
|
4
|
-
* Names the session
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Names the session using the currently selected model, but only when the
|
|
5
|
+
* session has no name yet — a user-set or previously generated name is never
|
|
6
|
+
* clobbered. Sends only user messages (current prompt plus previous user
|
|
7
|
+
* messages, truncated) for context, capped at 10 output tokens (1-5 word
|
|
8
|
+
* name). Thinking is never enabled. Equivalent to running /name automatically.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { complete } from "@earendil-works/pi-ai/compat";
|
|
@@ -52,6 +53,9 @@ export default function autoSessionNameExtension(pi: ExtensionAPI): void {
|
|
|
52
53
|
const text = event.text.trim();
|
|
53
54
|
if (!text || text.startsWith("/")) return;
|
|
54
55
|
if (!ctx.model) return;
|
|
56
|
+
// Only auto-name sessions that have no name yet; never clobber a
|
|
57
|
+
// user-set or previously generated name.
|
|
58
|
+
if (pi.getSessionName()) return;
|
|
55
59
|
|
|
56
60
|
const seq = ++namingSeq;
|
|
57
61
|
void (async () => {
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* Spawns the agent in RPC mode and provides a typed API for all operations.
|
|
5
5
|
*/
|
|
6
6
|
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
7
|
-
import type { ImageContent } from "@earendil-works/pi-ai";
|
|
7
|
+
import type { ImageContent, Model } from "@earendil-works/pi-ai";
|
|
8
8
|
import type { AgentSessionEvent, SessionStats } from "../../core/agent-session.ts";
|
|
9
|
+
import type { KeybindingsConfig } from "../../core/keybindings.ts";
|
|
10
|
+
import type { Settings } from "../../core/settings-manager.ts";
|
|
9
11
|
import type { BashResult } from "../../core/bash-executor.ts";
|
|
10
12
|
import type { CompactionResult } from "../../core/compaction/index.ts";
|
|
11
13
|
import type { SessionEntry, SessionTreeNode } from "../../core/session-manager.ts";
|
|
14
|
+
import type { ChangelogEntry } from "../../utils/changelog.ts";
|
|
12
15
|
import type { RpcSessionState, RpcSlashCommand } from "./rpc-types.ts";
|
|
13
16
|
export interface RpcClientOptions {
|
|
14
17
|
/** Path to the CLI entry point (default: searches for dist/cli.js) */
|
|
@@ -217,9 +220,9 @@ export declare class RpcClient {
|
|
|
217
220
|
leafId: string | null;
|
|
218
221
|
}>;
|
|
219
222
|
/**
|
|
220
|
-
* Get the session entry tree.
|
|
223
|
+
* Get the session entry tree, optionally filtered.
|
|
221
224
|
*/
|
|
222
|
-
getTree(): Promise<{
|
|
225
|
+
getTree(filter?: "default" | "no-tools" | "user-only" | "labeled-only" | "all"): Promise<{
|
|
223
226
|
tree: SessionTreeNode[];
|
|
224
227
|
leafId: string | null;
|
|
225
228
|
}>;
|
|
@@ -236,9 +239,132 @@ export declare class RpcClient {
|
|
|
236
239
|
*/
|
|
237
240
|
getMessages(): Promise<AgentMessage[]>;
|
|
238
241
|
/**
|
|
239
|
-
* Get available commands (extension commands, prompt templates).
|
|
242
|
+
* Get available commands (extension commands, prompt templates, builtins).
|
|
240
243
|
*/
|
|
241
244
|
getCommands(): Promise<RpcSlashCommand[]>;
|
|
245
|
+
/**
|
|
246
|
+
* Get settings for a scope ("global" | "project" | "effective", default "effective").
|
|
247
|
+
*/
|
|
248
|
+
getSettings(scope?: "global" | "project" | "effective"): Promise<{
|
|
249
|
+
scope: string;
|
|
250
|
+
settings: Settings;
|
|
251
|
+
}>;
|
|
252
|
+
/**
|
|
253
|
+
* Bulk-update settings and persist.
|
|
254
|
+
*/
|
|
255
|
+
setSettings(values: Partial<Settings>, scope?: "global" | "project"): Promise<void>;
|
|
256
|
+
/**
|
|
257
|
+
* Reset settings to bundled factory defaults (backup preserved).
|
|
258
|
+
*/
|
|
259
|
+
factoryResetSettings(): Promise<void>;
|
|
260
|
+
/**
|
|
261
|
+
* List providers and the login methods they support.
|
|
262
|
+
*/
|
|
263
|
+
getAuthProviders(): Promise<Array<{
|
|
264
|
+
providerId: string;
|
|
265
|
+
name: string;
|
|
266
|
+
authTypes: Array<"oauth" | "api_key">;
|
|
267
|
+
}>>;
|
|
268
|
+
/**
|
|
269
|
+
* Login to a provider. API-key flows emit `extension_ui_request: input` with
|
|
270
|
+
* `inputKind: "secret"`; OAuth flows open a browser and emit notify events.
|
|
271
|
+
*/
|
|
272
|
+
login(providerId: string, authType: "oauth" | "api_key"): Promise<{
|
|
273
|
+
providerId: string;
|
|
274
|
+
authType: "oauth" | "api_key";
|
|
275
|
+
message: string;
|
|
276
|
+
}>;
|
|
277
|
+
/**
|
|
278
|
+
* Log out of a provider, removing stored credentials.
|
|
279
|
+
*/
|
|
280
|
+
logout(providerId: string): Promise<void>;
|
|
281
|
+
/**
|
|
282
|
+
* Get providers with configured auth (stored credential or environment).
|
|
283
|
+
*/
|
|
284
|
+
getAuthState(): Promise<Array<{
|
|
285
|
+
providerId: string;
|
|
286
|
+
authType: "oauth" | "api_key";
|
|
287
|
+
source: string;
|
|
288
|
+
}>>;
|
|
289
|
+
/**
|
|
290
|
+
* Get enabled model patterns (Ctrl+P cycling scope) and available models.
|
|
291
|
+
*/
|
|
292
|
+
getScopedModels(): Promise<{
|
|
293
|
+
enabled: string[];
|
|
294
|
+
models: Model<any>[];
|
|
295
|
+
}>;
|
|
296
|
+
/**
|
|
297
|
+
* Set enabled model patterns and/or reorder them (Ctrl+P cycling order).
|
|
298
|
+
*/
|
|
299
|
+
setScopedModels(options?: {
|
|
300
|
+
enabled?: string[];
|
|
301
|
+
reorder?: string[];
|
|
302
|
+
}): Promise<string[]>;
|
|
303
|
+
/**
|
|
304
|
+
* Import and resume a session from a JSONL file (replaces current session).
|
|
305
|
+
*/
|
|
306
|
+
importJsonl(path: string): Promise<string>;
|
|
307
|
+
/**
|
|
308
|
+
* Run the git helper (worktrees/checkpoints/commits/push). Same behavior as
|
|
309
|
+
* the `prompt` command: success is reported on preflight, then the normal
|
|
310
|
+
* agent/message event stream follows.
|
|
311
|
+
*/
|
|
312
|
+
git(command: string): Promise<void>;
|
|
313
|
+
/**
|
|
314
|
+
* Reload keybindings, extensions, skills, prompts, themes, and context files.
|
|
315
|
+
*/
|
|
316
|
+
reload(): Promise<void>;
|
|
317
|
+
/**
|
|
318
|
+
* Set or clear a label on a session entry (undefined/empty clears).
|
|
319
|
+
*/
|
|
320
|
+
setEntryLabel(entryId: string, label?: string): Promise<string>;
|
|
321
|
+
/**
|
|
322
|
+
* Get the current project trust decision and status.
|
|
323
|
+
*/
|
|
324
|
+
getTrust(): Promise<{
|
|
325
|
+
cwd: string;
|
|
326
|
+
savedDecision: {
|
|
327
|
+
path: string;
|
|
328
|
+
decision: boolean;
|
|
329
|
+
} | null;
|
|
330
|
+
projectTrusted: boolean;
|
|
331
|
+
trustRequired: boolean;
|
|
332
|
+
}>;
|
|
333
|
+
/**
|
|
334
|
+
* Save a project trust decision. Decision takes full effect after restart.
|
|
335
|
+
*/
|
|
336
|
+
setTrust(decision: boolean | null): Promise<{
|
|
337
|
+
decision: boolean | null;
|
|
338
|
+
restartRequired: boolean;
|
|
339
|
+
}>;
|
|
340
|
+
/**
|
|
341
|
+
* Get the effective keybinding map.
|
|
342
|
+
*/
|
|
343
|
+
getHotkeys(): Promise<KeybindingsConfig>;
|
|
344
|
+
/**
|
|
345
|
+
* Get available themes (built-in + custom) and the current theme setting.
|
|
346
|
+
*/
|
|
347
|
+
getAvailableThemes(): Promise<{
|
|
348
|
+
themes: Array<{
|
|
349
|
+
name: string;
|
|
350
|
+
path?: string;
|
|
351
|
+
}>;
|
|
352
|
+
current: string | undefined;
|
|
353
|
+
}>;
|
|
354
|
+
/**
|
|
355
|
+
* Get the current version and parsed changelog entries.
|
|
356
|
+
*/
|
|
357
|
+
getVersionInfo(): Promise<{
|
|
358
|
+
version: string;
|
|
359
|
+
changelog: ChangelogEntry[];
|
|
360
|
+
}>;
|
|
361
|
+
/**
|
|
362
|
+
* Share the current session as a secret GitHub gist (requires `gh` CLI).
|
|
363
|
+
*/
|
|
364
|
+
shareGist(): Promise<{
|
|
365
|
+
url: string;
|
|
366
|
+
gistUrl: string;
|
|
367
|
+
}>;
|
|
242
368
|
/**
|
|
243
369
|
* Wait for agent to become idle (no streaming).
|
|
244
370
|
* Resolves when agent_settled event is received.
|
|
@@ -332,10 +332,10 @@ export class RpcClient {
|
|
|
332
332
|
return this.getData(response);
|
|
333
333
|
}
|
|
334
334
|
/**
|
|
335
|
-
* Get the session entry tree.
|
|
335
|
+
* Get the session entry tree, optionally filtered.
|
|
336
336
|
*/
|
|
337
|
-
async getTree() {
|
|
338
|
-
const response = await this.send({ type: "get_tree" });
|
|
337
|
+
async getTree(filter) {
|
|
338
|
+
const response = await this.send({ type: "get_tree", ...(filter ? { filter } : {}) });
|
|
339
339
|
return this.getData(response);
|
|
340
340
|
}
|
|
341
341
|
/**
|
|
@@ -359,13 +359,165 @@ export class RpcClient {
|
|
|
359
359
|
return this.getData(response).messages;
|
|
360
360
|
}
|
|
361
361
|
/**
|
|
362
|
-
* Get available commands (extension commands, prompt templates).
|
|
362
|
+
* Get available commands (extension commands, prompt templates, builtins).
|
|
363
363
|
*/
|
|
364
364
|
async getCommands() {
|
|
365
365
|
const response = await this.send({ type: "get_commands" });
|
|
366
366
|
return this.getData(response).commands;
|
|
367
367
|
}
|
|
368
368
|
// =========================================================================
|
|
369
|
+
// Settings
|
|
370
|
+
// =========================================================================
|
|
371
|
+
/**
|
|
372
|
+
* Get settings for a scope ("global" | "project" | "effective", default "effective").
|
|
373
|
+
*/
|
|
374
|
+
async getSettings(scope) {
|
|
375
|
+
const response = await this.send({ type: "get_settings", ...(scope ? { scope } : {}) });
|
|
376
|
+
return this.getData(response);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Bulk-update settings and persist.
|
|
380
|
+
*/
|
|
381
|
+
async setSettings(values, scope = "global") {
|
|
382
|
+
await this.send({ type: "set_settings", scope, values });
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Reset settings to bundled factory defaults (backup preserved).
|
|
386
|
+
*/
|
|
387
|
+
async factoryResetSettings() {
|
|
388
|
+
await this.send({ type: "factory_reset_settings" });
|
|
389
|
+
}
|
|
390
|
+
// =========================================================================
|
|
391
|
+
// Auth
|
|
392
|
+
// =========================================================================
|
|
393
|
+
/**
|
|
394
|
+
* List providers and the login methods they support.
|
|
395
|
+
*/
|
|
396
|
+
async getAuthProviders() {
|
|
397
|
+
const response = await this.send({ type: "get_auth_providers" });
|
|
398
|
+
return this.getData(response).providers;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Login to a provider. API-key flows emit `extension_ui_request: input` with
|
|
402
|
+
* `inputKind: "secret"`; OAuth flows open a browser and emit notify events.
|
|
403
|
+
*/
|
|
404
|
+
async login(providerId, authType) {
|
|
405
|
+
const response = await this.send({ type: "login", providerId, authType });
|
|
406
|
+
return this.getData(response);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Log out of a provider, removing stored credentials.
|
|
410
|
+
*/
|
|
411
|
+
async logout(providerId) {
|
|
412
|
+
await this.send({ type: "logout", providerId });
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Get providers with configured auth (stored credential or environment).
|
|
416
|
+
*/
|
|
417
|
+
async getAuthState() {
|
|
418
|
+
const response = await this.send({ type: "get_auth_state" });
|
|
419
|
+
return this.getData(response).providers;
|
|
420
|
+
}
|
|
421
|
+
// =========================================================================
|
|
422
|
+
// Scoped models
|
|
423
|
+
// =========================================================================
|
|
424
|
+
/**
|
|
425
|
+
* Get enabled model patterns (Ctrl+P cycling scope) and available models.
|
|
426
|
+
*/
|
|
427
|
+
async getScopedModels() {
|
|
428
|
+
const response = await this.send({ type: "get_scoped_models" });
|
|
429
|
+
return this.getData(response);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Set enabled model patterns and/or reorder them (Ctrl+P cycling order).
|
|
433
|
+
*/
|
|
434
|
+
async setScopedModels(options = {}) {
|
|
435
|
+
const response = await this.send({ type: "set_scoped_models", ...options });
|
|
436
|
+
return this.getData(response).enabled;
|
|
437
|
+
}
|
|
438
|
+
// =========================================================================
|
|
439
|
+
// Session import
|
|
440
|
+
// =========================================================================
|
|
441
|
+
/**
|
|
442
|
+
* Import and resume a session from a JSONL file (replaces current session).
|
|
443
|
+
*/
|
|
444
|
+
async importJsonl(path) {
|
|
445
|
+
const response = await this.send({ type: "import_jsonl", path });
|
|
446
|
+
return this.getData(response).sessionPath;
|
|
447
|
+
}
|
|
448
|
+
// =========================================================================
|
|
449
|
+
// Git helper / reload / tree ops
|
|
450
|
+
// =========================================================================
|
|
451
|
+
/**
|
|
452
|
+
* Run the git helper (worktrees/checkpoints/commits/push). Same behavior as
|
|
453
|
+
* the `prompt` command: success is reported on preflight, then the normal
|
|
454
|
+
* agent/message event stream follows.
|
|
455
|
+
*/
|
|
456
|
+
async git(command) {
|
|
457
|
+
await this.send({ type: "git", command });
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Reload keybindings, extensions, skills, prompts, themes, and context files.
|
|
461
|
+
*/
|
|
462
|
+
async reload() {
|
|
463
|
+
await this.send({ type: "reload" });
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Set or clear a label on a session entry (undefined/empty clears).
|
|
467
|
+
*/
|
|
468
|
+
async setEntryLabel(entryId, label) {
|
|
469
|
+
const response = await this.send({ type: "set_entry_label", entryId, label });
|
|
470
|
+
return this.getData(response).labelId;
|
|
471
|
+
}
|
|
472
|
+
// =========================================================================
|
|
473
|
+
// Trust
|
|
474
|
+
// =========================================================================
|
|
475
|
+
/**
|
|
476
|
+
* Get the current project trust decision and status.
|
|
477
|
+
*/
|
|
478
|
+
async getTrust() {
|
|
479
|
+
const response = await this.send({ type: "get_trust" });
|
|
480
|
+
return this.getData(response);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Save a project trust decision. Decision takes full effect after restart.
|
|
484
|
+
*/
|
|
485
|
+
async setTrust(decision) {
|
|
486
|
+
const response = await this.send({ type: "set_trust", decision });
|
|
487
|
+
return this.getData(response);
|
|
488
|
+
}
|
|
489
|
+
// =========================================================================
|
|
490
|
+
// Hotkeys / version / share
|
|
491
|
+
// =========================================================================
|
|
492
|
+
/**
|
|
493
|
+
* Get the effective keybinding map.
|
|
494
|
+
*/
|
|
495
|
+
async getHotkeys() {
|
|
496
|
+
const response = await this.send({ type: "get_hotkeys" });
|
|
497
|
+
return this.getData(response).hotkeys;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Get available themes (built-in + custom) and the current theme setting.
|
|
501
|
+
*/
|
|
502
|
+
async getAvailableThemes() {
|
|
503
|
+
const response = await this.send({ type: "get_available_themes" });
|
|
504
|
+
return this.getData(response);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Get the current version and parsed changelog entries.
|
|
508
|
+
*/
|
|
509
|
+
async getVersionInfo() {
|
|
510
|
+
const response = await this.send({ type: "get_version_info" });
|
|
511
|
+
return this.getData(response);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Share the current session as a secret GitHub gist (requires `gh` CLI).
|
|
515
|
+
*/
|
|
516
|
+
async shareGist() {
|
|
517
|
+
const response = await this.send({ type: "share_gist", public: false });
|
|
518
|
+
return this.getData(response);
|
|
519
|
+
}
|
|
520
|
+
// =========================================================================
|
|
369
521
|
// Helpers
|
|
370
522
|
// =========================================================================
|
|
371
523
|
/**
|
|
@@ -11,10 +11,23 @@
|
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
13
|
import * as crypto from "node:crypto";
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { chmodSync, copyFileSync, existsSync, unlinkSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
14
18
|
import { generateHandoff } from "../../core/handoff.js";
|
|
19
|
+
import { buildGitCommandPrompt } from "../../core/git-command.js";
|
|
20
|
+
import { KeybindingsManager } from "../../core/keybindings.js";
|
|
15
21
|
import { flushRawStdout, takeOverStdout, waitForRawStdoutBackpressure, writeRawStdout, } from "../../core/output-guard.js";
|
|
22
|
+
import { ProjectTrustStore, hasTrustRequiringProjectResources } from "../../core/trust-manager.js";
|
|
23
|
+
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
|
24
|
+
import { createSyntheticSourceInfo } from "../../core/source-info.js";
|
|
16
25
|
import { killTrackedDetachedChildren } from "../../utils/shell.js";
|
|
26
|
+
import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
|
|
27
|
+
import { VERSION, getAgentDir, getBundledDefaultsDir, getShareViewerUrl } from "../../config.js";
|
|
28
|
+
import { openBrowser } from "../../utils/open-browser.js";
|
|
17
29
|
import { theme } from "../interactive/theme/theme.js";
|
|
30
|
+
import { getAvailableThemesWithPaths, setRegisteredThemes } from "../interactive/theme/theme.js";
|
|
18
31
|
import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
|
|
19
32
|
/**
|
|
20
33
|
* Run in RPC mode.
|
|
@@ -28,6 +41,43 @@ export async function runRpcMode(runtimeHost) {
|
|
|
28
41
|
const output = (obj) => {
|
|
29
42
|
writeRawStdout(serializeJsonLine(obj));
|
|
30
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* Filter a session tree with the same semantics as the TUI tree selector
|
|
46
|
+
* (tree-selector.ts FilterMode). Hidden leaf pruning is preserved.
|
|
47
|
+
*/
|
|
48
|
+
const filterTree = (nodes, mode) => {
|
|
49
|
+
const passes = (node) => {
|
|
50
|
+
const entry = node.entry;
|
|
51
|
+
const isSettingsEntry = entry.type === "label" ||
|
|
52
|
+
entry.type === "custom" ||
|
|
53
|
+
entry.type === "model_change" ||
|
|
54
|
+
entry.type === "thinking_level_change" ||
|
|
55
|
+
entry.type === "session_info";
|
|
56
|
+
switch (mode) {
|
|
57
|
+
case "user-only":
|
|
58
|
+
return entry.type === "message" && entry.message.role === "user";
|
|
59
|
+
case "no-tools":
|
|
60
|
+
return !isSettingsEntry && !(entry.type === "message" && entry.message.role === "toolResult");
|
|
61
|
+
case "labeled-only":
|
|
62
|
+
return node.label !== undefined;
|
|
63
|
+
case "all":
|
|
64
|
+
return true;
|
|
65
|
+
default:
|
|
66
|
+
return !isSettingsEntry;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const visit = (nodes) => {
|
|
70
|
+
const result = [];
|
|
71
|
+
for (const node of nodes) {
|
|
72
|
+
if (!passes(node))
|
|
73
|
+
continue;
|
|
74
|
+
const children = visit(node.children);
|
|
75
|
+
result.push({ ...node, children });
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
};
|
|
79
|
+
return visit(nodes);
|
|
80
|
+
};
|
|
31
81
|
const success = (id, command, data) => {
|
|
32
82
|
if (data === undefined) {
|
|
33
83
|
return { id, type: "response", command, success: true };
|
|
@@ -576,7 +626,10 @@ export async function runRpcMode(runtimeHost) {
|
|
|
576
626
|
}
|
|
577
627
|
case "get_tree": {
|
|
578
628
|
const sessionManager = session.sessionManager;
|
|
579
|
-
|
|
629
|
+
const tree = sessionManager.getTree();
|
|
630
|
+
const filter = command.filter ?? "default";
|
|
631
|
+
const filtered = filter === "all" ? tree : filterTree(tree, filter);
|
|
632
|
+
return success(id, "get_tree", { tree: filtered, leafId: sessionManager.getLeafId() });
|
|
580
633
|
}
|
|
581
634
|
case "get_last_assistant_text": {
|
|
582
635
|
const text = session.getLastAssistantText();
|
|
@@ -617,8 +670,438 @@ export async function runRpcMode(runtimeHost) {
|
|
|
617
670
|
sourceInfo: template.sourceInfo,
|
|
618
671
|
});
|
|
619
672
|
}
|
|
673
|
+
// Built-in TUI commands: display-only, cannot be invoked via prompt.
|
|
674
|
+
for (const builtin of BUILTIN_SLASH_COMMANDS) {
|
|
675
|
+
commands.push({
|
|
676
|
+
name: builtin.name,
|
|
677
|
+
description: builtin.description
|
|
678
|
+
+ (builtin.argumentHint ? ` ${builtin.argumentHint}` : ""),
|
|
679
|
+
source: "builtin",
|
|
680
|
+
sourceInfo: createSyntheticSourceInfo(`builtin:${builtin.name}`, { source: "builtin" }),
|
|
681
|
+
interactiveOnly: true,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
620
684
|
return success(id, "get_commands", { commands });
|
|
621
685
|
}
|
|
686
|
+
// =================================================================
|
|
687
|
+
// Settings
|
|
688
|
+
// =================================================================
|
|
689
|
+
case "get_settings": {
|
|
690
|
+
const scope = command.scope ?? "effective";
|
|
691
|
+
const settings = scope === "global"
|
|
692
|
+
? session.settingsManager.getGlobalSettings()
|
|
693
|
+
: scope === "project"
|
|
694
|
+
? session.settingsManager.getProjectSettings()
|
|
695
|
+
: session.settingsManager.getEffectiveSettings();
|
|
696
|
+
return success(id, "get_settings", { scope, settings });
|
|
697
|
+
}
|
|
698
|
+
case "set_settings": {
|
|
699
|
+
const scope = command.scope ?? "global";
|
|
700
|
+
try {
|
|
701
|
+
if (scope === "project") {
|
|
702
|
+
session.settingsManager.setProjectSettings(command.values);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
session.settingsManager.setGlobalSettings(command.values);
|
|
706
|
+
}
|
|
707
|
+
await session.settingsManager.flush();
|
|
708
|
+
return success(id, "set_settings", { scope, values: structuredClone(command.values) });
|
|
709
|
+
}
|
|
710
|
+
catch (writeError) {
|
|
711
|
+
return error(id, "set_settings", writeError instanceof Error ? writeError.message : String(writeError));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
case "factory_reset_settings": {
|
|
715
|
+
if (session.isStreaming) {
|
|
716
|
+
return error(id, "factory_reset_settings", "Wait for the current response to finish before resetting settings.");
|
|
717
|
+
}
|
|
718
|
+
if (session.isCompacting) {
|
|
719
|
+
return error(id, "factory_reset_settings", "Wait for compaction to finish before resetting settings.");
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
const agentDir = getAgentDir();
|
|
723
|
+
const settingsPath = join(agentDir, "settings.json");
|
|
724
|
+
const bundledPath = join(getBundledDefaultsDir(), "settings.json");
|
|
725
|
+
if (!existsSync(bundledPath)) {
|
|
726
|
+
return error(id, "factory_reset_settings", `Bundled defaults not found at ${bundledPath}`);
|
|
727
|
+
}
|
|
728
|
+
if (existsSync(settingsPath)) {
|
|
729
|
+
copyFileSync(settingsPath, `${settingsPath}.bak`);
|
|
730
|
+
}
|
|
731
|
+
copyFileSync(bundledPath, settingsPath);
|
|
732
|
+
try {
|
|
733
|
+
chmodSync(settingsPath, 0o600);
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
// best-effort
|
|
737
|
+
}
|
|
738
|
+
await session.reload();
|
|
739
|
+
return success(id, "factory_reset_settings");
|
|
740
|
+
}
|
|
741
|
+
catch (resetError) {
|
|
742
|
+
return error(id, "factory_reset_settings", resetError instanceof Error ? resetError.message : String(resetError));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// =================================================================
|
|
746
|
+
// Auth
|
|
747
|
+
// =================================================================
|
|
748
|
+
case "get_auth_providers": {
|
|
749
|
+
const providers = session.modelRuntime
|
|
750
|
+
.getProviders()
|
|
751
|
+
.map((provider) => {
|
|
752
|
+
const authTypes = [];
|
|
753
|
+
if (provider.auth.oauth)
|
|
754
|
+
authTypes.push("oauth");
|
|
755
|
+
if (provider.auth.apiKey)
|
|
756
|
+
authTypes.push("api_key");
|
|
757
|
+
return { providerId: provider.id, name: provider.name, authTypes };
|
|
758
|
+
})
|
|
759
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
760
|
+
return success(id, "get_auth_providers", { providers });
|
|
761
|
+
}
|
|
762
|
+
case "login": {
|
|
763
|
+
const provider = session.modelRuntime.getProvider(command.providerId);
|
|
764
|
+
if (!provider) {
|
|
765
|
+
return error(id, "login", `Unknown provider: ${command.providerId}`);
|
|
766
|
+
}
|
|
767
|
+
if (command.authType === "oauth" && !provider.auth.oauth) {
|
|
768
|
+
return error(id, "login", `Provider ${command.providerId} does not support OAuth login`);
|
|
769
|
+
}
|
|
770
|
+
if (command.authType === "api_key" && !provider.auth.apiKey) {
|
|
771
|
+
return error(id, "login", `Provider ${command.providerId} does not support API key login`);
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const abortController = new AbortController();
|
|
775
|
+
await session.modelRuntime.login(command.providerId, command.authType, {
|
|
776
|
+
signal: abortController.signal,
|
|
777
|
+
prompt: async (prompt) => {
|
|
778
|
+
if (prompt.signal?.aborted)
|
|
779
|
+
throw new Error("Login cancelled");
|
|
780
|
+
const id = crypto.randomUUID();
|
|
781
|
+
const result = await new Promise((resolve, reject) => {
|
|
782
|
+
const onAbort = () => {
|
|
783
|
+
cleanup();
|
|
784
|
+
resolve({ type: "extension_ui_response", id, cancelled: true });
|
|
785
|
+
};
|
|
786
|
+
const cleanup = () => {
|
|
787
|
+
prompt.signal?.removeEventListener("abort", onAbort);
|
|
788
|
+
pendingExtensionRequests.delete(id);
|
|
789
|
+
};
|
|
790
|
+
prompt.signal?.addEventListener("abort", onAbort, { once: true });
|
|
791
|
+
pendingExtensionRequests.set(id, {
|
|
792
|
+
resolve: (response) => {
|
|
793
|
+
cleanup();
|
|
794
|
+
resolve(response);
|
|
795
|
+
},
|
|
796
|
+
reject: (err) => {
|
|
797
|
+
cleanup();
|
|
798
|
+
reject(err);
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
const request = {
|
|
802
|
+
type: "extension_ui_request",
|
|
803
|
+
id,
|
|
804
|
+
method: prompt.type === "select" ? "select" : "input",
|
|
805
|
+
title: prompt.message,
|
|
806
|
+
};
|
|
807
|
+
if (prompt.type === "select") {
|
|
808
|
+
request.options = prompt.options.map((option) => option.description ? `${option.label} (${option.description})` : option.label);
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
if (prompt.placeholder)
|
|
812
|
+
request.placeholder = prompt.placeholder;
|
|
813
|
+
if (prompt.type === "secret")
|
|
814
|
+
request.inputKind = "secret";
|
|
815
|
+
}
|
|
816
|
+
output(request);
|
|
817
|
+
});
|
|
818
|
+
if (result.type === "extension_ui_response" && "cancelled" in result && result.cancelled) {
|
|
819
|
+
throw new Error("Login cancelled");
|
|
820
|
+
}
|
|
821
|
+
if ("value" in result)
|
|
822
|
+
return result.value;
|
|
823
|
+
if ("values" in result)
|
|
824
|
+
return (result.values ? result.values[0] : undefined);
|
|
825
|
+
return undefined;
|
|
826
|
+
},
|
|
827
|
+
notify: (event) => {
|
|
828
|
+
if (event.type === "auth_url") {
|
|
829
|
+
openBrowser(event.url);
|
|
830
|
+
output({
|
|
831
|
+
type: "extension_ui_request",
|
|
832
|
+
id: crypto.randomUUID(),
|
|
833
|
+
method: "notify",
|
|
834
|
+
message: event.instructions
|
|
835
|
+
? `Open in your browser: ${event.url}\n${event.instructions}`
|
|
836
|
+
: `Open in your browser: ${event.url}`,
|
|
837
|
+
notifyType: "info",
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
else if (event.type === "device_code") {
|
|
841
|
+
openBrowser(event.verificationUri);
|
|
842
|
+
output({
|
|
843
|
+
type: "extension_ui_request",
|
|
844
|
+
id: crypto.randomUUID(),
|
|
845
|
+
method: "notify",
|
|
846
|
+
message: `Open ${event.verificationUri} and enter code: ${event.userCode}`,
|
|
847
|
+
notifyType: "info",
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
else if (event.type === "info") {
|
|
851
|
+
output({
|
|
852
|
+
type: "extension_ui_request",
|
|
853
|
+
id: crypto.randomUUID(),
|
|
854
|
+
method: "notify",
|
|
855
|
+
message: event.message,
|
|
856
|
+
notifyType: "info",
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
else if (event.type === "progress") {
|
|
860
|
+
output({
|
|
861
|
+
type: "extension_ui_request",
|
|
862
|
+
id: crypto.randomUUID(),
|
|
863
|
+
method: "notify",
|
|
864
|
+
message: event.message,
|
|
865
|
+
notifyType: "info",
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
});
|
|
870
|
+
const actionLabel = command.authType === "oauth" ? `Logged in to ${provider.name}` : `Saved API key for ${provider.name}`;
|
|
871
|
+
const authType = command.authType;
|
|
872
|
+
const isUnknownModel = session.model?.provider === "unknown" && session.model?.id === "unknown";
|
|
873
|
+
if (isUnknownModel) {
|
|
874
|
+
const available = await session.modelRuntime.getAvailable();
|
|
875
|
+
const providerModels = available.filter((model) => model.provider === command.providerId);
|
|
876
|
+
if (providerModels.length > 0) {
|
|
877
|
+
await session.setModel(providerModels[0]);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return success(id, "login", {
|
|
881
|
+
providerId: command.providerId,
|
|
882
|
+
authType,
|
|
883
|
+
message: `${actionLabel}. Credentials saved.`,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
catch (loginError) {
|
|
887
|
+
const message = loginError instanceof Error ? loginError.message : String(loginError);
|
|
888
|
+
if (message === "Login cancelled") {
|
|
889
|
+
return error(id, "login", "Login cancelled");
|
|
890
|
+
}
|
|
891
|
+
return error(id, "login", message);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
case "logout": {
|
|
895
|
+
const provider = session.modelRuntime.getProvider(command.providerId);
|
|
896
|
+
if (!provider) {
|
|
897
|
+
return error(id, "logout", `Unknown provider: ${command.providerId}`);
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
await session.modelRuntime.logout(command.providerId);
|
|
901
|
+
return success(id, "logout", { providerId: command.providerId });
|
|
902
|
+
}
|
|
903
|
+
catch (logoutError) {
|
|
904
|
+
return error(id, "logout", logoutError instanceof Error ? logoutError.message : String(logoutError));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
case "get_auth_state": {
|
|
908
|
+
try {
|
|
909
|
+
const credentials = await session.modelRuntime.listCredentials();
|
|
910
|
+
const providers = credentials.map(({ providerId, type }) => ({
|
|
911
|
+
providerId,
|
|
912
|
+
authType: type,
|
|
913
|
+
source: "stored",
|
|
914
|
+
}));
|
|
915
|
+
// Environment/configured auth not in the credential store.
|
|
916
|
+
for (const provider of session.modelRuntime.getProviders()) {
|
|
917
|
+
if (providers.some((entry) => entry.providerId === provider.id))
|
|
918
|
+
continue;
|
|
919
|
+
const status = session.modelRuntime.getProviderAuthStatus(provider.id);
|
|
920
|
+
if (status.configured) {
|
|
921
|
+
providers.push({
|
|
922
|
+
providerId: provider.id,
|
|
923
|
+
authType: session.modelRuntime.isUsingOAuth(provider.id) ? "oauth" : "api_key",
|
|
924
|
+
source: status.label ?? status.source ?? "environment",
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return success(id, "get_auth_state", { providers });
|
|
929
|
+
}
|
|
930
|
+
catch (authStateError) {
|
|
931
|
+
return error(id, "get_auth_state", authStateError instanceof Error ? authStateError.message : String(authStateError));
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
// =================================================================
|
|
935
|
+
// Scoped Models
|
|
936
|
+
// =================================================================
|
|
937
|
+
case "get_scoped_models": {
|
|
938
|
+
const enabled = session.settingsManager.getEnabledModels() ?? [];
|
|
939
|
+
const scoped = session.scopedModels.map((entry) => entry.model);
|
|
940
|
+
const models = scoped.length > 0 ? scoped : await session.modelRuntime.getAvailable();
|
|
941
|
+
return success(id, "get_scoped_models", { enabled, models: [...models] });
|
|
942
|
+
}
|
|
943
|
+
case "set_scoped_models": {
|
|
944
|
+
if (command.enabled !== undefined) {
|
|
945
|
+
session.settingsManager.setEnabledModels(command.enabled.length > 0 ? command.enabled : undefined);
|
|
946
|
+
}
|
|
947
|
+
const enabled = session.settingsManager.getEnabledModels() ?? [];
|
|
948
|
+
if (command.reorder !== undefined && command.reorder.length > 0) {
|
|
949
|
+
const patternSet = new Set(command.reorder);
|
|
950
|
+
const kept = enabled.filter((pattern) => patternSet.has(pattern));
|
|
951
|
+
const added = command.reorder.filter((pattern) => !kept.includes(pattern));
|
|
952
|
+
session.settingsManager.setEnabledModels([...kept, ...added]);
|
|
953
|
+
}
|
|
954
|
+
await session.settingsManager.flush();
|
|
955
|
+
return success(id, "set_scoped_models", { enabled: session.settingsManager.getEnabledModels() ?? [] });
|
|
956
|
+
}
|
|
957
|
+
// =================================================================
|
|
958
|
+
// Session Import
|
|
959
|
+
// =================================================================
|
|
960
|
+
case "import_jsonl": {
|
|
961
|
+
try {
|
|
962
|
+
await runtimeHost.importFromJsonl(command.path);
|
|
963
|
+
await rebindSession();
|
|
964
|
+
const sessionFile = session.sessionFile;
|
|
965
|
+
return success(id, "import_jsonl", { sessionPath: sessionFile ?? command.path });
|
|
966
|
+
}
|
|
967
|
+
catch (importError) {
|
|
968
|
+
return error(id, "import_jsonl", importError instanceof Error ? importError.message : String(importError));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
// =================================================================
|
|
972
|
+
// Git helper
|
|
973
|
+
// =================================================================
|
|
974
|
+
case "git": {
|
|
975
|
+
const prompt = buildGitCommandPrompt(command.command);
|
|
976
|
+
let gitPreflightSucceeded = false;
|
|
977
|
+
void session
|
|
978
|
+
.prompt(prompt, {
|
|
979
|
+
streamingBehavior: "steer",
|
|
980
|
+
source: "rpc",
|
|
981
|
+
preflightResult: (didSucceed) => {
|
|
982
|
+
if (didSucceed) {
|
|
983
|
+
gitPreflightSucceeded = true;
|
|
984
|
+
output(success(id, "git"));
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
})
|
|
988
|
+
.catch((e) => {
|
|
989
|
+
if (!gitPreflightSucceeded) {
|
|
990
|
+
output(error(id, "git", e.message));
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
return undefined;
|
|
994
|
+
}
|
|
995
|
+
// =================================================================
|
|
996
|
+
// Reload
|
|
997
|
+
// =================================================================
|
|
998
|
+
case "reload": {
|
|
999
|
+
if (session.isStreaming) {
|
|
1000
|
+
return error(id, "reload", "Wait for the current response to finish before reloading.");
|
|
1001
|
+
}
|
|
1002
|
+
if (session.isCompacting) {
|
|
1003
|
+
return error(id, "reload", "Wait for compaction to finish before reloading.");
|
|
1004
|
+
}
|
|
1005
|
+
try {
|
|
1006
|
+
await session.reload();
|
|
1007
|
+
return success(id, "reload");
|
|
1008
|
+
}
|
|
1009
|
+
catch (reloadError) {
|
|
1010
|
+
return error(id, "reload", reloadError instanceof Error ? reloadError.message : String(reloadError));
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
// =================================================================
|
|
1014
|
+
// Tree ops
|
|
1015
|
+
// =================================================================
|
|
1016
|
+
case "set_entry_label": {
|
|
1017
|
+
try {
|
|
1018
|
+
const labelId = session.sessionManager.appendLabelChange(command.entryId, command.label);
|
|
1019
|
+
return success(id, "set_entry_label", { labelId });
|
|
1020
|
+
}
|
|
1021
|
+
catch (labelError) {
|
|
1022
|
+
return error(id, "set_entry_label", labelError instanceof Error ? labelError.message : String(labelError));
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// =================================================================
|
|
1026
|
+
// Trust
|
|
1027
|
+
// =================================================================
|
|
1028
|
+
case "get_trust": {
|
|
1029
|
+
const cwd = session.sessionManager.getCwd();
|
|
1030
|
+
const trustStore = new ProjectTrustStore(runtimeHost.services.agentDir);
|
|
1031
|
+
const entry = trustStore.getEntry(cwd);
|
|
1032
|
+
return success(id, "get_trust", {
|
|
1033
|
+
cwd,
|
|
1034
|
+
savedDecision: entry ? { path: entry.path, decision: entry.decision } : null,
|
|
1035
|
+
projectTrusted: session.settingsManager.isProjectTrusted(),
|
|
1036
|
+
trustRequired: hasTrustRequiringProjectResources(cwd),
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
case "set_trust": {
|
|
1040
|
+
const cwd = session.sessionManager.getCwd();
|
|
1041
|
+
const trustStore = new ProjectTrustStore(runtimeHost.services.agentDir);
|
|
1042
|
+
try {
|
|
1043
|
+
if (command.decision === null) {
|
|
1044
|
+
trustStore.set(cwd, null);
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
trustStore.set(cwd, command.decision);
|
|
1048
|
+
}
|
|
1049
|
+
return success(id, "set_trust", { decision: command.decision, restartRequired: command.decision !== null });
|
|
1050
|
+
}
|
|
1051
|
+
catch (trustError) {
|
|
1052
|
+
return error(id, "set_trust", trustError instanceof Error ? trustError.message : String(trustError));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
// =================================================================
|
|
1056
|
+
// Hotkeys / version / share
|
|
1057
|
+
// =================================================================
|
|
1058
|
+
case "get_hotkeys": {
|
|
1059
|
+
const keybindings = KeybindingsManager.create();
|
|
1060
|
+
const hotkeys = keybindings.getEffectiveConfig();
|
|
1061
|
+
return success(id, "get_hotkeys", { hotkeys });
|
|
1062
|
+
}
|
|
1063
|
+
case "get_available_themes": {
|
|
1064
|
+
// Mirror TUI startup: register extension/package themes before listing.
|
|
1065
|
+
setRegisteredThemes(session.resourceLoader.getThemes().themes);
|
|
1066
|
+
const themes = getAvailableThemesWithPaths().map(({ name, path }) => ({ name, path }));
|
|
1067
|
+
const currentSetting = session.settingsManager.getThemeSetting();
|
|
1068
|
+
const current = currentSetting?.includes("/") ? undefined : currentSetting;
|
|
1069
|
+
return success(id, "get_available_themes", { themes, current });
|
|
1070
|
+
}
|
|
1071
|
+
case "get_version_info": {
|
|
1072
|
+
const changelog = parseChangelog(getChangelogPath());
|
|
1073
|
+
return success(id, "get_version_info", { version: VERSION, changelog });
|
|
1074
|
+
}
|
|
1075
|
+
case "share_gist": {
|
|
1076
|
+
try {
|
|
1077
|
+
const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
|
|
1078
|
+
if (authResult.status !== 0) {
|
|
1079
|
+
return error(id, "share_gist", "GitHub CLI is not logged in. Run 'gh auth login' first.");
|
|
1080
|
+
}
|
|
1081
|
+
const tmpFile = join(tmpdir(), `selesai-session-${crypto.randomUUID()}.html`);
|
|
1082
|
+
await session.exportToHtml(tmpFile);
|
|
1083
|
+
const result = spawnSync("gh", ["gist", "create", "--public=false", tmpFile], { encoding: "utf-8" });
|
|
1084
|
+
try {
|
|
1085
|
+
unlinkSync(tmpFile);
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
// best-effort cleanup
|
|
1089
|
+
}
|
|
1090
|
+
if (result.status !== 0) {
|
|
1091
|
+
return error(id, "share_gist", (result.stderr ?? "Unknown error").trim() || "Failed to create gist");
|
|
1092
|
+
}
|
|
1093
|
+
const gistUrl = (result.stdout ?? "").trim();
|
|
1094
|
+
const gistId = gistUrl.split("/").pop();
|
|
1095
|
+
if (!gistId) {
|
|
1096
|
+
return error(id, "share_gist", "Failed to parse gist ID from gh output");
|
|
1097
|
+
}
|
|
1098
|
+
const previewUrl = getShareViewerUrl(gistId);
|
|
1099
|
+
return success(id, "share_gist", { url: previewUrl, gistUrl });
|
|
1100
|
+
}
|
|
1101
|
+
catch (shareError) {
|
|
1102
|
+
return error(id, "share_gist", shareError instanceof Error ? shareError.message : String(shareError));
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
622
1105
|
default: {
|
|
623
1106
|
const unknownCommand = command;
|
|
624
1107
|
return error(id, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);
|
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
8
8
|
import type { ImageContent, Model } from "@earendil-works/pi-ai";
|
|
9
9
|
import type { SessionStats } from "../../core/agent-session.ts";
|
|
10
|
+
import type { KeybindingsConfig } from "../../core/keybindings.ts";
|
|
11
|
+
import type { Settings, SettingsScope } from "../../core/settings-manager.ts";
|
|
10
12
|
import type { BashResult } from "../../core/bash-executor.ts";
|
|
11
13
|
import type { CompactionResult } from "../../core/compaction/index.ts";
|
|
12
14
|
import type { SessionEntry, SessionTreeNode } from "../../core/session-manager.ts";
|
|
13
15
|
import type { SourceInfo } from "../../core/source-info.ts";
|
|
16
|
+
import type { ChangelogEntry } from "../../utils/changelog.ts";
|
|
14
17
|
export type RpcCommand = {
|
|
15
18
|
id?: string;
|
|
16
19
|
type: "prompt";
|
|
@@ -137,6 +140,7 @@ export type RpcCommand = {
|
|
|
137
140
|
} | {
|
|
138
141
|
id?: string;
|
|
139
142
|
type: "get_tree";
|
|
143
|
+
filter?: "default" | "no-tools" | "user-only" | "labeled-only" | "all";
|
|
140
144
|
} | {
|
|
141
145
|
id?: string;
|
|
142
146
|
type: "get_last_assistant_text";
|
|
@@ -144,6 +148,77 @@ export type RpcCommand = {
|
|
|
144
148
|
id?: string;
|
|
145
149
|
type: "set_session_name";
|
|
146
150
|
name: string;
|
|
151
|
+
} | {
|
|
152
|
+
id?: string;
|
|
153
|
+
type: "get_settings";
|
|
154
|
+
scope?: "global" | "project" | "effective";
|
|
155
|
+
} | {
|
|
156
|
+
id?: string;
|
|
157
|
+
type: "set_settings";
|
|
158
|
+
scope?: SettingsScope;
|
|
159
|
+
values: Partial<Settings>;
|
|
160
|
+
} | {
|
|
161
|
+
id?: string;
|
|
162
|
+
type: "factory_reset_settings";
|
|
163
|
+
} | {
|
|
164
|
+
id?: string;
|
|
165
|
+
type: "get_auth_providers";
|
|
166
|
+
} | {
|
|
167
|
+
id?: string;
|
|
168
|
+
type: "login";
|
|
169
|
+
providerId: string;
|
|
170
|
+
authType: "oauth" | "api_key";
|
|
171
|
+
} | {
|
|
172
|
+
id?: string;
|
|
173
|
+
type: "logout";
|
|
174
|
+
providerId: string;
|
|
175
|
+
} | {
|
|
176
|
+
id?: string;
|
|
177
|
+
type: "get_auth_state";
|
|
178
|
+
} | {
|
|
179
|
+
id?: string;
|
|
180
|
+
type: "get_scoped_models";
|
|
181
|
+
} | {
|
|
182
|
+
id?: string;
|
|
183
|
+
type: "set_scoped_models";
|
|
184
|
+
enabled?: string[];
|
|
185
|
+
reorder?: string[];
|
|
186
|
+
} | {
|
|
187
|
+
id?: string;
|
|
188
|
+
type: "import_jsonl";
|
|
189
|
+
path: string;
|
|
190
|
+
} | {
|
|
191
|
+
id?: string;
|
|
192
|
+
type: "git";
|
|
193
|
+
command: string;
|
|
194
|
+
} | {
|
|
195
|
+
id?: string;
|
|
196
|
+
type: "reload";
|
|
197
|
+
} | {
|
|
198
|
+
id?: string;
|
|
199
|
+
type: "set_entry_label";
|
|
200
|
+
entryId: string;
|
|
201
|
+
label?: string;
|
|
202
|
+
} | {
|
|
203
|
+
id?: string;
|
|
204
|
+
type: "get_trust";
|
|
205
|
+
} | {
|
|
206
|
+
id?: string;
|
|
207
|
+
type: "set_trust";
|
|
208
|
+
decision: boolean | null;
|
|
209
|
+
} | {
|
|
210
|
+
id?: string;
|
|
211
|
+
type: "get_hotkeys";
|
|
212
|
+
} | {
|
|
213
|
+
id?: string;
|
|
214
|
+
type: "get_available_themes";
|
|
215
|
+
} | {
|
|
216
|
+
id?: string;
|
|
217
|
+
type: "get_version_info";
|
|
218
|
+
} | {
|
|
219
|
+
id?: string;
|
|
220
|
+
type: "share_gist";
|
|
221
|
+
public?: boolean;
|
|
147
222
|
} | {
|
|
148
223
|
id?: string;
|
|
149
224
|
type: "get_messages";
|
|
@@ -158,9 +233,11 @@ export interface RpcSlashCommand {
|
|
|
158
233
|
/** Human-readable description */
|
|
159
234
|
description?: string;
|
|
160
235
|
/** What kind of command this is */
|
|
161
|
-
source: "extension" | "prompt" | "skill";
|
|
236
|
+
source: "extension" | "prompt" | "skill" | "builtin";
|
|
162
237
|
/** Source metadata for the owning resource */
|
|
163
238
|
sourceInfo: SourceInfo;
|
|
239
|
+
/** True for interactive-TUI-only builtins that cannot be invoked via prompt */
|
|
240
|
+
interactiveOnly?: boolean;
|
|
164
241
|
}
|
|
165
242
|
export interface RpcSessionState {
|
|
166
243
|
model?: Model<any>;
|
|
@@ -434,6 +511,175 @@ export type RpcResponse = {
|
|
|
434
511
|
data: {
|
|
435
512
|
commands: RpcSlashCommand[];
|
|
436
513
|
};
|
|
514
|
+
} | {
|
|
515
|
+
id?: string;
|
|
516
|
+
type: "response";
|
|
517
|
+
command: "get_settings";
|
|
518
|
+
success: true;
|
|
519
|
+
data: {
|
|
520
|
+
scope: "global" | "project" | "effective";
|
|
521
|
+
settings: Settings;
|
|
522
|
+
};
|
|
523
|
+
} | {
|
|
524
|
+
id?: string;
|
|
525
|
+
type: "response";
|
|
526
|
+
command: "set_settings";
|
|
527
|
+
success: true;
|
|
528
|
+
data: {
|
|
529
|
+
scope: SettingsScope;
|
|
530
|
+
values: Partial<Settings>;
|
|
531
|
+
};
|
|
532
|
+
} | {
|
|
533
|
+
id?: string;
|
|
534
|
+
type: "response";
|
|
535
|
+
command: "factory_reset_settings";
|
|
536
|
+
success: true;
|
|
537
|
+
} | {
|
|
538
|
+
id?: string;
|
|
539
|
+
type: "response";
|
|
540
|
+
command: "get_auth_providers";
|
|
541
|
+
success: true;
|
|
542
|
+
data: {
|
|
543
|
+
providers: Array<{
|
|
544
|
+
providerId: string;
|
|
545
|
+
name: string;
|
|
546
|
+
authTypes: Array<"oauth" | "api_key">;
|
|
547
|
+
}>;
|
|
548
|
+
};
|
|
549
|
+
} | {
|
|
550
|
+
id?: string;
|
|
551
|
+
type: "response";
|
|
552
|
+
command: "login";
|
|
553
|
+
success: true;
|
|
554
|
+
data: {
|
|
555
|
+
providerId: string;
|
|
556
|
+
authType: "oauth" | "api_key";
|
|
557
|
+
message: string;
|
|
558
|
+
};
|
|
559
|
+
} | {
|
|
560
|
+
id?: string;
|
|
561
|
+
type: "response";
|
|
562
|
+
command: "logout";
|
|
563
|
+
success: true;
|
|
564
|
+
data: {
|
|
565
|
+
providerId: string;
|
|
566
|
+
};
|
|
567
|
+
} | {
|
|
568
|
+
id?: string;
|
|
569
|
+
type: "response";
|
|
570
|
+
command: "get_auth_state";
|
|
571
|
+
success: true;
|
|
572
|
+
data: {
|
|
573
|
+
providers: Array<{
|
|
574
|
+
providerId: string;
|
|
575
|
+
authType: "oauth" | "api_key";
|
|
576
|
+
source: string;
|
|
577
|
+
}>;
|
|
578
|
+
};
|
|
579
|
+
} | {
|
|
580
|
+
id?: string;
|
|
581
|
+
type: "response";
|
|
582
|
+
command: "get_scoped_models";
|
|
583
|
+
success: true;
|
|
584
|
+
data: {
|
|
585
|
+
enabled: string[];
|
|
586
|
+
models: Model<any>[];
|
|
587
|
+
};
|
|
588
|
+
} | {
|
|
589
|
+
id?: string;
|
|
590
|
+
type: "response";
|
|
591
|
+
command: "set_scoped_models";
|
|
592
|
+
success: true;
|
|
593
|
+
data: {
|
|
594
|
+
enabled: string[];
|
|
595
|
+
};
|
|
596
|
+
} | {
|
|
597
|
+
id?: string;
|
|
598
|
+
type: "response";
|
|
599
|
+
command: "import_jsonl";
|
|
600
|
+
success: true;
|
|
601
|
+
data: {
|
|
602
|
+
sessionPath: string;
|
|
603
|
+
};
|
|
604
|
+
} | {
|
|
605
|
+
id?: string;
|
|
606
|
+
type: "response";
|
|
607
|
+
command: "git";
|
|
608
|
+
success: true;
|
|
609
|
+
} | {
|
|
610
|
+
id?: string;
|
|
611
|
+
type: "response";
|
|
612
|
+
command: "reload";
|
|
613
|
+
success: true;
|
|
614
|
+
} | {
|
|
615
|
+
id?: string;
|
|
616
|
+
type: "response";
|
|
617
|
+
command: "set_entry_label";
|
|
618
|
+
success: true;
|
|
619
|
+
data: {
|
|
620
|
+
labelId: string;
|
|
621
|
+
};
|
|
622
|
+
} | {
|
|
623
|
+
id?: string;
|
|
624
|
+
type: "response";
|
|
625
|
+
command: "get_trust";
|
|
626
|
+
success: true;
|
|
627
|
+
data: {
|
|
628
|
+
cwd: string;
|
|
629
|
+
savedDecision: {
|
|
630
|
+
path: string;
|
|
631
|
+
decision: boolean;
|
|
632
|
+
} | null;
|
|
633
|
+
projectTrusted: boolean;
|
|
634
|
+
trustRequired: boolean;
|
|
635
|
+
};
|
|
636
|
+
} | {
|
|
637
|
+
id?: string;
|
|
638
|
+
type: "response";
|
|
639
|
+
command: "set_trust";
|
|
640
|
+
success: true;
|
|
641
|
+
data: {
|
|
642
|
+
decision: boolean | null;
|
|
643
|
+
restartRequired: boolean;
|
|
644
|
+
};
|
|
645
|
+
} | {
|
|
646
|
+
id?: string;
|
|
647
|
+
type: "response";
|
|
648
|
+
command: "get_hotkeys";
|
|
649
|
+
success: true;
|
|
650
|
+
data: {
|
|
651
|
+
hotkeys: KeybindingsConfig;
|
|
652
|
+
};
|
|
653
|
+
} | {
|
|
654
|
+
id?: string;
|
|
655
|
+
type: "response";
|
|
656
|
+
command: "get_available_themes";
|
|
657
|
+
success: true;
|
|
658
|
+
data: {
|
|
659
|
+
themes: Array<{
|
|
660
|
+
name: string;
|
|
661
|
+
path?: string;
|
|
662
|
+
}>;
|
|
663
|
+
current: string | undefined;
|
|
664
|
+
};
|
|
665
|
+
} | {
|
|
666
|
+
id?: string;
|
|
667
|
+
type: "response";
|
|
668
|
+
command: "get_version_info";
|
|
669
|
+
success: true;
|
|
670
|
+
data: {
|
|
671
|
+
version: string;
|
|
672
|
+
changelog: ChangelogEntry[];
|
|
673
|
+
};
|
|
674
|
+
} | {
|
|
675
|
+
id?: string;
|
|
676
|
+
type: "response";
|
|
677
|
+
command: "share_gist";
|
|
678
|
+
success: true;
|
|
679
|
+
data: {
|
|
680
|
+
url: string;
|
|
681
|
+
gistUrl: string;
|
|
682
|
+
};
|
|
437
683
|
} | {
|
|
438
684
|
id?: string;
|
|
439
685
|
type: "response";
|
|
@@ -469,6 +715,8 @@ export type RpcExtensionUIRequest = {
|
|
|
469
715
|
method: "input";
|
|
470
716
|
title: string;
|
|
471
717
|
placeholder?: string;
|
|
718
|
+
/** "secret": render as single-line password (e.g. API key entry). Defaults to "text". */
|
|
719
|
+
inputKind?: "text" | "secret";
|
|
472
720
|
timeout?: number;
|
|
473
721
|
} | {
|
|
474
722
|
type: "extension_ui_request";
|
package/docs/rpc.md
CHANGED
|
@@ -740,7 +740,7 @@ Each command has:
|
|
|
740
740
|
- `"path"`: Explicit path via CLI or settings
|
|
741
741
|
- `path`: Absolute file path to the command source (optional)
|
|
742
742
|
|
|
743
|
-
**Note**: Built-in TUI commands (`/settings`, `/hotkeys`, etc.) are
|
|
743
|
+
**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`.
|
|
744
744
|
|
|
745
745
|
## Events
|
|
746
746
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.9",
|
|
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": {
|