@docyrus/docyrus 0.0.34 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/agent-loader.js +3 -2
- package/agent-loader.js.map +2 -2
- package/main.js +82162 -46093
- package/main.js.map +4 -4
- package/package.json +12 -3
- package/resources/chrome-tools/browser-content.js +46 -46
- package/resources/chrome-tools/browser-cookies.js +16 -16
- package/resources/chrome-tools/browser-eval.js +27 -27
- package/resources/chrome-tools/browser-hn-scraper.js +1 -1
- package/resources/chrome-tools/browser-nav.js +23 -23
- package/resources/chrome-tools/browser-pick.js +127 -127
- package/resources/chrome-tools/browser-screenshot.js +10 -10
- package/resources/chrome-tools/browser-start.js +38 -38
- package/resources/pi-agent/extensions/answer.ts +392 -384
- package/resources/pi-agent/extensions/context.ts +415 -415
- package/resources/pi-agent/extensions/control.ts +1287 -1287
- package/resources/pi-agent/extensions/diff.ts +171 -171
- package/resources/pi-agent/extensions/files.ts +155 -155
- package/resources/pi-agent/extensions/knowledge.ts +664 -0
- package/resources/pi-agent/extensions/loop.ts +375 -375
- package/resources/pi-agent/extensions/pi-bash-live-view/index.ts +1 -1
- package/resources/pi-agent/extensions/pi-bash-live-view/package.json +22 -22
- package/resources/pi-agent/extensions/pi-bash-live-view/pty-execute.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/pty-session.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/spawn-helper.ts +2 -2
- package/resources/pi-agent/extensions/pi-bash-live-view/terminal-emulator.ts +18 -18
- package/resources/pi-agent/extensions/pi-bash-live-view/truncate.ts +1 -1
- package/resources/pi-agent/extensions/pi-bash-live-view/widget.ts +4 -4
- package/resources/pi-agent/extensions/pi-custom-compaction/package.json +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +14 -14
- package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +9 -9
- package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +13 -13
- package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +5 -5
- package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +13 -13
- package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +14 -14
- package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +9 -9
- package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +35 -35
- package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +1 -1
- package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +12 -12
- package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +4 -4
- package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +2 -2
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +6 -6
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +17 -17
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +22 -22
- package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +2 -2
- package/resources/pi-agent/extensions/prompt-editor.ts +900 -900
- package/resources/pi-agent/extensions/prompt-url-widget.ts +122 -122
- package/resources/pi-agent/extensions/redraws.ts +14 -14
- package/resources/pi-agent/extensions/review.ts +1533 -1533
- package/resources/pi-agent/extensions/todos.ts +1735 -1735
- package/resources/pi-agent/extensions/tps.ts +40 -40
- package/resources/pi-agent/extensions/whimsical.ts +3 -3
- package/resources/pi-agent/prompts/agent-system.md +2 -0
- package/resources/pi-agent/prompts/coder-system.md +2 -0
- package/server-loader.js +82 -1
- package/server-loader.js.map +3 -3
- package/tui.mjs +2 -0
- package/tui.mjs.map +1 -1
|
@@ -64,33 +64,33 @@ const SESSION_MESSAGE_TYPE = "session-message";
|
|
|
64
64
|
const SENDER_INFO_PATTERN = /<sender_info>[\s\S]*?<\/sender_info>/g;
|
|
65
65
|
|
|
66
66
|
function expandUserPath(inputPath: string): string {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
if (inputPath === "~") {return os.homedir();}
|
|
68
|
+
if (inputPath.startsWith("~/")) {return path.join(os.homedir(), inputPath.slice(2));}
|
|
69
|
+
return inputPath;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function getScopedAgentDir(): string {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
73
|
+
const envCandidates = ["PI_CODING_AGENT_DIR", "TAU_CODING_AGENT_DIR"];
|
|
74
|
+
for (const key of envCandidates) {
|
|
75
|
+
const value = process.env[key]?.trim();
|
|
76
|
+
if (value) {
|
|
77
|
+
return expandUserPath(value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
82
|
+
if (!key.endsWith("_CODING_AGENT_DIR") || !value?.trim()) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return expandUserPath(value.trim());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return path.join(os.homedir(), ".pi", "agent");
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
export function getControlDir(): string {
|
|
93
|
-
|
|
93
|
+
return path.join(getScopedAgentDir(), "session-control");
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
const CONTROL_DIR = getControlDir();
|
|
@@ -195,25 +195,25 @@ const TURN_SUMMARY_PROMPT = `Summarize what happened in this conversation since
|
|
|
195
195
|
Be concise but comprehensive. Preserve exact file paths, function names, and error messages.`;
|
|
196
196
|
|
|
197
197
|
async function selectSummarizationModel(
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
currentModel: Model<Api> | undefined,
|
|
199
|
+
modelRegistry: {
|
|
200
200
|
find: (provider: string, modelId: string) => Model<Api> | undefined;
|
|
201
201
|
getApiKey: (model: Model<Api>) => Promise<string | undefined>;
|
|
202
202
|
},
|
|
203
203
|
): Promise<Model<Api> | undefined> {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
204
|
+
const codexModel = modelRegistry.find("openai-codex", CODEX_MODEL_ID);
|
|
205
|
+
if (codexModel) {
|
|
206
|
+
const apiKey = await modelRegistry.getApiKey(codexModel);
|
|
207
|
+
if (apiKey) {return codexModel;}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const haikuModel = modelRegistry.find("anthropic", HAIKU_MODEL_ID);
|
|
211
|
+
if (haikuModel) {
|
|
212
|
+
const apiKey = await modelRegistry.getApiKey(haikuModel);
|
|
213
|
+
if (apiKey) {return haikuModel;}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return currentModel;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
// ============================================================================
|
|
@@ -223,155 +223,155 @@ async function selectSummarizationModel(
|
|
|
223
223
|
const STATUS_KEY = "session-control";
|
|
224
224
|
|
|
225
225
|
function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
|
|
226
|
-
|
|
226
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
function getSocketPath(sessionId: string): string {
|
|
230
|
-
|
|
230
|
+
return path.join(CONTROL_DIR, `${sessionId}${SOCKET_SUFFIX}`);
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
function isSafeSessionId(sessionId: string): boolean {
|
|
234
|
-
|
|
234
|
+
return !sessionId.includes("/") && !sessionId.includes("\\") && !sessionId.includes("..") && sessionId.length > 0;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
function isSafeAlias(alias: string): boolean {
|
|
238
|
-
|
|
238
|
+
return !alias.includes("/") && !alias.includes("\\") && !alias.includes("..") && alias.length > 0;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
function getAliasPath(alias: string): string {
|
|
242
|
-
|
|
242
|
+
return path.join(CONTROL_DIR, `${alias}.alias`);
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
function getSessionAlias(ctx: ExtensionContext): string | null {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
const sessionName = ctx.sessionManager.getSessionName();
|
|
247
|
+
const alias = sessionName ? sessionName.trim() : "";
|
|
248
|
+
if (!alias || !isSafeAlias(alias)) {return null;}
|
|
249
|
+
return alias;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
async function ensureControlDir(): Promise<void> {
|
|
253
|
-
|
|
253
|
+
await fs.mkdir(CONTROL_DIR, { recursive: true });
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
async function removeSocket(socketPath: string | null): Promise<void> {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
257
|
+
if (!socketPath) {return;}
|
|
258
|
+
try {
|
|
259
|
+
await fs.unlink(socketPath);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (isErrnoException(error) && error.code !== "ENOENT") {
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
// TODO: add GC for stale sockets/aliases older than 7 days.
|
|
268
268
|
async function removeAliasesForSocket(socketPath: string | null): Promise<void> {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
269
|
+
if (!socketPath) {return;}
|
|
270
|
+
try {
|
|
271
|
+
const entries = await fs.readdir(CONTROL_DIR, { withFileTypes: true });
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
if (!entry.isSymbolicLink()) {continue;}
|
|
274
|
+
const aliasPath = path.join(CONTROL_DIR, entry.name);
|
|
275
|
+
let target: string;
|
|
276
|
+
try {
|
|
277
|
+
target = await fs.readlink(aliasPath);
|
|
278
|
+
} catch {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const resolvedTarget = path.resolve(CONTROL_DIR, target);
|
|
282
|
+
if (resolvedTarget === socketPath) {
|
|
283
|
+
await fs.unlink(aliasPath);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (isErrnoException(error) && error.code === "ENOENT") {return;}
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
async function createAliasSymlink(sessionId: string, alias: string): Promise<void> {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
293
|
+
if (!alias || !isSafeAlias(alias)) {return;}
|
|
294
|
+
const aliasPath = getAliasPath(alias);
|
|
295
|
+
const target = `${sessionId}${SOCKET_SUFFIX}`;
|
|
296
|
+
try {
|
|
297
|
+
await fs.unlink(aliasPath);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (isErrnoException(error) && error.code !== "ENOENT") {
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
await fs.symlink(target, aliasPath);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (isErrnoException(error) && error.code !== "EEXIST") {
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
async function resolveSessionIdFromAlias(alias: string): Promise<string | null> {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
313
|
+
if (!alias || !isSafeAlias(alias)) {return null;}
|
|
314
|
+
const aliasPath = getAliasPath(alias);
|
|
315
|
+
try {
|
|
316
|
+
const target = await fs.readlink(aliasPath);
|
|
317
|
+
const resolvedTarget = path.resolve(CONTROL_DIR, target);
|
|
318
|
+
const base = path.basename(resolvedTarget);
|
|
319
|
+
if (!base.endsWith(SOCKET_SUFFIX)) {return null;}
|
|
320
|
+
const sessionId = base.slice(0, -SOCKET_SUFFIX.length);
|
|
321
|
+
return isSafeSessionId(sessionId) ? sessionId : null;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (isErrnoException(error) && error.code === "ENOENT") {return null;}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
async function getAliasMap(): Promise<Map<string, string[]>> {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
329
|
+
const aliasMap = new Map<string, string[]>();
|
|
330
|
+
const entries = await fs.readdir(CONTROL_DIR, { withFileTypes: true });
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
if (!entry.isSymbolicLink()) {continue;}
|
|
333
|
+
if (!entry.name.endsWith(".alias")) {continue;}
|
|
334
|
+
const aliasPath = path.join(CONTROL_DIR, entry.name);
|
|
335
|
+
let target: string;
|
|
336
|
+
try {
|
|
337
|
+
target = await fs.readlink(aliasPath);
|
|
338
|
+
} catch {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const resolvedTarget = path.resolve(CONTROL_DIR, target);
|
|
342
|
+
const aliases = aliasMap.get(resolvedTarget);
|
|
343
|
+
const aliasName = entry.name.slice(0, -".alias".length);
|
|
344
|
+
if (aliases) {
|
|
345
|
+
aliases.push(aliasName);
|
|
346
|
+
} else {
|
|
347
|
+
aliasMap.set(resolvedTarget, [aliasName]);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return aliasMap;
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
async function isSocketAlive(socketPath: string): Promise<boolean> {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
354
|
+
return await new Promise((resolve) => {
|
|
355
|
+
const socket = net.createConnection(socketPath);
|
|
356
|
+
const timeout = setTimeout(() => {
|
|
357
|
+
socket.destroy();
|
|
358
|
+
resolve(false);
|
|
359
|
+
}, 300);
|
|
360
|
+
|
|
361
|
+
const cleanup = (alive: boolean) => {
|
|
362
|
+
clearTimeout(timeout);
|
|
363
|
+
socket.removeAllListeners();
|
|
364
|
+
resolve(alive);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
socket.once("connect", () => {
|
|
368
|
+
socket.end();
|
|
369
|
+
cleanup(true);
|
|
370
|
+
});
|
|
371
|
+
socket.once("error", () => {
|
|
372
|
+
cleanup(false);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
type LiveSessionInfo = {
|
|
@@ -382,71 +382,71 @@ type LiveSessionInfo = {
|
|
|
382
382
|
};
|
|
383
383
|
|
|
384
384
|
async function getLiveSessions(): Promise<LiveSessionInfo[]> {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
385
|
+
await ensureControlDir();
|
|
386
|
+
const entries = await fs.readdir(CONTROL_DIR, { withFileTypes: true });
|
|
387
|
+
const aliasMap = await getAliasMap();
|
|
388
|
+
const sessions: LiveSessionInfo[] = [];
|
|
389
|
+
|
|
390
|
+
for (const entry of entries) {
|
|
391
|
+
if (!entry.name.endsWith(SOCKET_SUFFIX)) {continue;}
|
|
392
|
+
const socketPath = path.join(CONTROL_DIR, entry.name);
|
|
393
|
+
const alive = await isSocketAlive(socketPath);
|
|
394
|
+
if (!alive) {continue;}
|
|
395
|
+
const sessionId = entry.name.slice(0, -SOCKET_SUFFIX.length);
|
|
396
|
+
if (!isSafeSessionId(sessionId)) {continue;}
|
|
397
|
+
const aliases = aliasMap.get(socketPath) ?? [];
|
|
398
|
+
const name = aliases[0];
|
|
399
|
+
sessions.push({ sessionId, name, aliases, socketPath });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
sessions.sort((a, b) => (a.name ?? a.sessionId).localeCompare(b.name ?? b.sessionId));
|
|
403
|
+
return sessions;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
async function syncAlias(state: SocketState, ctx: ExtensionContext): Promise<void> {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
407
|
+
if (!state.server || !state.socketPath) {return;}
|
|
408
|
+
const alias = getSessionAlias(ctx);
|
|
409
|
+
if (alias && alias !== state.alias) {
|
|
410
|
+
await removeAliasesForSocket(state.socketPath);
|
|
411
|
+
await createAliasSymlink(ctx.sessionManager.getSessionId(), alias);
|
|
412
|
+
state.alias = alias;
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!alias && state.alias) {
|
|
416
|
+
await removeAliasesForSocket(state.socketPath);
|
|
417
|
+
state.alias = null;
|
|
418
|
+
}
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
function writeResponse(socket: net.Socket, response: RpcResponse): void {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
422
|
+
try {
|
|
423
|
+
socket.write(`${JSON.stringify(response)}\n`);
|
|
424
|
+
} catch {
|
|
425
425
|
// Socket may be closed
|
|
426
|
-
|
|
426
|
+
}
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
function writeEvent(socket: net.Socket, event: RpcEvent): void {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
430
|
+
try {
|
|
431
|
+
socket.write(`${JSON.stringify(event)}\n`);
|
|
432
|
+
} catch {
|
|
433
433
|
// Socket may be closed
|
|
434
|
-
|
|
434
|
+
}
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
function parseCommand(line: string): { command?: RpcCommand; error?: string } {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
438
|
+
try {
|
|
439
|
+
const parsed = JSON.parse(line) as RpcCommand;
|
|
440
|
+
if (!parsed || typeof parsed !== "object") {
|
|
441
|
+
return { error: "Invalid command" };
|
|
442
|
+
}
|
|
443
|
+
if (typeof parsed.type !== "string") {
|
|
444
|
+
return { error: "Missing command type" };
|
|
445
|
+
}
|
|
446
|
+
return { command: parsed };
|
|
447
|
+
} catch (error) {
|
|
448
|
+
return { error: error instanceof Error ? error.message : "Failed to parse command" };
|
|
449
|
+
}
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
// ============================================================================
|
|
@@ -460,83 +460,83 @@ interface ExtractedMessage {
|
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
function getLastAssistantMessage(ctx: ExtensionContext): ExtractedMessage | undefined {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
463
|
+
const branch = ctx.sessionManager.getBranch();
|
|
464
|
+
|
|
465
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
466
|
+
const entry = branch[i];
|
|
467
|
+
if (entry.type === "message") {
|
|
468
|
+
const msg = entry.message;
|
|
469
|
+
if ("role" in msg && msg.role === "assistant") {
|
|
470
|
+
const textParts = msg.content
|
|
471
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
472
|
+
.map((c) => c.text);
|
|
473
|
+
if (textParts.length > 0) {
|
|
474
|
+
return {
|
|
475
|
+
role: "assistant",
|
|
476
|
+
content: textParts.join("\n"),
|
|
477
|
+
timestamp: msg.timestamp,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return undefined;
|
|
484
484
|
}
|
|
485
485
|
|
|
486
486
|
function getMessagesSinceLastPrompt(ctx: ExtensionContext): ExtractedMessage[] {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
487
|
+
const branch = ctx.sessionManager.getBranch();
|
|
488
|
+
const messages: ExtractedMessage[] = [];
|
|
489
|
+
|
|
490
|
+
let lastUserIndex = -1;
|
|
491
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
492
|
+
const entry = branch[i];
|
|
493
|
+
if (entry.type === "message" && "role" in entry.message && entry.message.role === "user") {
|
|
494
|
+
lastUserIndex = i;
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (lastUserIndex === -1) {return [];}
|
|
500
|
+
|
|
501
|
+
for (let i = lastUserIndex; i < branch.length; i++) {
|
|
502
|
+
const entry = branch[i];
|
|
503
|
+
if (entry.type === "message") {
|
|
504
|
+
const msg = entry.message;
|
|
505
|
+
if ("role" in msg && (msg.role === "user" || msg.role === "assistant")) {
|
|
506
|
+
const textParts = msg.content
|
|
507
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
508
|
+
.map((c) => c.text);
|
|
509
|
+
if (textParts.length > 0) {
|
|
510
|
+
messages.push({
|
|
511
|
+
role: msg.role,
|
|
512
|
+
content: textParts.join("\n"),
|
|
513
|
+
timestamp: msg.timestamp,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return messages;
|
|
521
521
|
}
|
|
522
522
|
|
|
523
523
|
function getFirstEntryId(ctx: ExtensionContext): string | undefined {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
524
|
+
const entries = ctx.sessionManager.getEntries();
|
|
525
|
+
if (entries.length === 0) {return undefined;}
|
|
526
|
+
const root = entries.find((e) => e.parentId === null);
|
|
527
|
+
return root?.id ?? entries[0]?.id;
|
|
528
528
|
}
|
|
529
529
|
|
|
530
530
|
function extractTextContent(content: string | Array<TextContent | { type: string }>): string {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
531
|
+
if (typeof content === "string") {return content;}
|
|
532
|
+
return content
|
|
533
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
534
|
+
.map((c) => c.text)
|
|
535
|
+
.join("\n");
|
|
536
536
|
}
|
|
537
537
|
|
|
538
538
|
function stripSenderInfo(text: string): string {
|
|
539
|
-
|
|
539
|
+
return text.replace(SENDER_INFO_PATTERN, "").trim();
|
|
540
540
|
}
|
|
541
541
|
|
|
542
542
|
interface SenderInfo {
|
|
@@ -545,69 +545,69 @@ interface SenderInfo {
|
|
|
545
545
|
}
|
|
546
546
|
|
|
547
547
|
function parseSenderInfo(text: string): SenderInfo | null {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
548
|
+
const match = text.match(/<sender_info>([\s\S]*?)<\/sender_info>/);
|
|
549
|
+
if (!match) {return null;}
|
|
550
|
+
const raw = match[1].trim();
|
|
551
|
+
if (!raw) {return null;}
|
|
552
|
+
|
|
553
|
+
if (raw.startsWith("{")) {
|
|
554
|
+
try {
|
|
555
|
+
const parsed = JSON.parse(raw) as { sessionId?: unknown; sessionName?: unknown };
|
|
556
|
+
const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId.trim() : "";
|
|
557
|
+
const sessionName = typeof parsed.sessionName === "string" ? parsed.sessionName.trim() : "";
|
|
558
|
+
if (sessionId || sessionName) {
|
|
559
|
+
return {
|
|
560
|
+
sessionId: sessionId || undefined,
|
|
561
|
+
sessionName: sessionName || undefined,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
565
|
// Ignore JSON parse errors, fall back to legacy parsing.
|
|
566
|
-
|
|
567
|
-
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
568
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
569
|
+
const legacyIdMatch = raw.match(/session\s+([a-f0-9-]{6,})/i);
|
|
570
|
+
if (legacyIdMatch) {
|
|
571
|
+
return { sessionId: legacyIdMatch[1] };
|
|
572
|
+
}
|
|
573
573
|
|
|
574
|
-
|
|
574
|
+
return null;
|
|
575
575
|
}
|
|
576
576
|
|
|
577
577
|
function formatSenderInfo(info: SenderInfo | null): string | null {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
578
|
+
if (!info) {return null;}
|
|
579
|
+
const { sessionName, sessionId } = info;
|
|
580
|
+
if (sessionName && sessionId) {return `${sessionName} (${sessionId})`;}
|
|
581
|
+
if (sessionName) {return sessionName;}
|
|
582
|
+
if (sessionId) {return sessionId;}
|
|
583
|
+
return null;
|
|
584
584
|
}
|
|
585
585
|
|
|
586
586
|
const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) => {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
587
|
+
const rawContent = extractTextContent(message.content);
|
|
588
|
+
const senderInfo = parseSenderInfo(rawContent);
|
|
589
|
+
let text = stripSenderInfo(rawContent);
|
|
590
|
+
if (!text) {text = "(no content)";}
|
|
591
|
+
|
|
592
|
+
if (!expanded) {
|
|
593
|
+
const lines = text.split("\n");
|
|
594
|
+
if (lines.length > 5) {
|
|
595
|
+
text = `${lines.slice(0, 5).join("\n")}\n...`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
600
|
+
const labelBase = theme.fg("customMessageLabel", `\x1b[1m[${message.customType}]\x1b[22m`);
|
|
601
|
+
const senderText = formatSenderInfo(senderInfo);
|
|
602
|
+
const label = senderText ? `${labelBase} ${theme.fg("dim", `from ${senderText}`)}` : labelBase;
|
|
603
|
+
box.addChild(new Text(label, 0, 0));
|
|
604
|
+
box.addChild(new Spacer(1));
|
|
605
|
+
box.addChild(
|
|
606
|
+
new Markdown(text, 0, 0, getMarkdownTheme(), {
|
|
607
|
+
color: (value: string) => theme.fg("customMessageText", value),
|
|
608
|
+
}),
|
|
609
|
+
);
|
|
610
|
+
return box;
|
|
611
611
|
};
|
|
612
612
|
|
|
613
613
|
// ============================================================================
|
|
@@ -615,187 +615,187 @@ const renderSessionMessage: MessageRenderer = (message, { expanded }, theme) =>
|
|
|
615
615
|
// ============================================================================
|
|
616
616
|
|
|
617
617
|
async function handleCommand(
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
618
|
+
pi: ExtensionAPI,
|
|
619
|
+
state: SocketState,
|
|
620
|
+
command: RpcCommand,
|
|
621
|
+
socket: net.Socket,
|
|
622
622
|
): Promise<void> {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
623
|
+
const id = "id" in command && typeof command.id === "string" ? command.id : undefined;
|
|
624
|
+
const respond = (success: boolean, commandName: string, data?: unknown, error?: string) => {
|
|
625
|
+
if (state.context) {
|
|
626
|
+
void syncAlias(state, state.context);
|
|
627
|
+
}
|
|
628
|
+
writeResponse(socket, { type: "response", command: commandName, success, data, error, id });
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const ctx = state.context;
|
|
632
|
+
if (!ctx) {
|
|
633
|
+
respond(false, command.type, undefined, "Session not ready");
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
void syncAlias(state, ctx);
|
|
638
638
|
|
|
639
639
|
// Abort
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
640
|
+
if (command.type === "abort") {
|
|
641
|
+
ctx.abort();
|
|
642
|
+
respond(true, "abort");
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
645
|
|
|
646
646
|
// Subscribe to turn_end
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
647
|
+
if (command.type === "subscribe") {
|
|
648
|
+
if (command.event === "turn_end") {
|
|
649
|
+
const subscriptionId = id ?? `sub_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
650
|
+
state.turnEndSubscriptions.push({ socket, subscriptionId });
|
|
651
|
+
|
|
652
|
+
const cleanup = () => {
|
|
653
|
+
const idx = state.turnEndSubscriptions.findIndex((s) => s.subscriptionId === subscriptionId);
|
|
654
|
+
if (idx !== -1) {state.turnEndSubscriptions.splice(idx, 1);}
|
|
655
|
+
};
|
|
656
|
+
socket.once("close", cleanup);
|
|
657
|
+
socket.once("error", cleanup);
|
|
658
|
+
|
|
659
|
+
respond(true, "subscribe", { subscriptionId, event: "turn_end" });
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
respond(false, "subscribe", undefined, `Unknown event type: ${command.event}`);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
665
|
|
|
666
666
|
// Get last message
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
667
|
+
if (command.type === "get_message") {
|
|
668
|
+
const message = getLastAssistantMessage(ctx);
|
|
669
|
+
if (!message) {
|
|
670
|
+
respond(true, "get_message", { message: null });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
respond(true, "get_message", { message });
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
676
|
|
|
677
677
|
// Get summary
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
678
|
+
if (command.type === "get_summary") {
|
|
679
|
+
const messages = getMessagesSinceLastPrompt(ctx);
|
|
680
|
+
if (messages.length === 0) {
|
|
681
|
+
respond(false, "get_summary", undefined, "No messages to summarize");
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const model = await selectSummarizationModel(ctx.model, ctx.modelRegistry);
|
|
686
|
+
if (!model) {
|
|
687
|
+
respond(false, "get_summary", undefined, "No model available for summarization");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const apiKey = await ctx.modelRegistry.getApiKey(model);
|
|
692
|
+
if (!apiKey) {
|
|
693
|
+
respond(false, "get_summary", undefined, "No API key available for summarization model");
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
try {
|
|
698
|
+
const conversationText = messages
|
|
699
|
+
.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`)
|
|
700
|
+
.join("\n\n");
|
|
701
|
+
|
|
702
|
+
const userMessage: UserMessage = {
|
|
703
|
+
role: "user",
|
|
704
|
+
content: [{ type: "text", text: `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_SUMMARY_PROMPT}` }],
|
|
705
|
+
timestamp: Date.now(),
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const response = await complete(
|
|
709
|
+
model,
|
|
710
|
+
{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: [userMessage] },
|
|
711
|
+
{ apiKey },
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
if (response.stopReason === "aborted" || response.stopReason === "error") {
|
|
715
|
+
respond(false, "get_summary", undefined, "Summarization failed");
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const summary = response.content
|
|
720
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
721
|
+
.map((c) => c.text)
|
|
722
|
+
.join("\n");
|
|
723
|
+
|
|
724
|
+
respond(true, "get_summary", { summary, model: model.id });
|
|
725
|
+
} catch (error) {
|
|
726
|
+
respond(false, "get_summary", undefined, error instanceof Error ? error.message : "Summarization failed");
|
|
727
|
+
}
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
730
|
|
|
731
731
|
// Clear session
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
732
|
+
if (command.type === "clear") {
|
|
733
|
+
if (!ctx.isIdle()) {
|
|
734
|
+
respond(false, "clear", undefined, "Session is busy - wait for turn to complete");
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const firstEntryId = getFirstEntryId(ctx);
|
|
739
|
+
if (!firstEntryId) {
|
|
740
|
+
respond(false, "clear", undefined, "No entries in session");
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const currentLeafId = ctx.sessionManager.getLeafId();
|
|
745
|
+
if (currentLeafId === firstEntryId) {
|
|
746
|
+
respond(true, "clear", { cleared: true, alreadyAtRoot: true });
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (command.summarize) {
|
|
751
751
|
// Summarization requires navigateTree which we don't have direct access to
|
|
752
752
|
// Return an error for now - the caller should clear without summarize
|
|
753
753
|
// or use a different approach
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
754
|
+
respond(false, "clear", undefined, "Clear with summarization not supported via RPC - use summarize=false");
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
757
|
|
|
758
758
|
// Access internal session manager to rewind (type assertion to access non-readonly methods)
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
759
|
+
try {
|
|
760
|
+
const sessionManager = ctx.sessionManager as unknown as { rewindTo(id: string): void };
|
|
761
|
+
sessionManager.rewindTo(firstEntryId);
|
|
762
|
+
respond(true, "clear", { cleared: true, targetId: firstEntryId });
|
|
763
|
+
} catch (error) {
|
|
764
|
+
respond(false, "clear", undefined, error instanceof Error ? error.message : "Clear failed");
|
|
765
|
+
}
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
768
|
|
|
769
769
|
// Send message
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
770
|
+
if (command.type === "send") {
|
|
771
|
+
const message = command.message;
|
|
772
|
+
if (typeof message !== "string" || message.trim().length === 0) {
|
|
773
|
+
respond(false, "send", undefined, "Missing message");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const mode = command.mode ?? "steer";
|
|
778
|
+
const isIdle = ctx.isIdle();
|
|
779
|
+
const customMessage = {
|
|
780
|
+
customType: SESSION_MESSAGE_TYPE,
|
|
781
|
+
content: message,
|
|
782
|
+
display: true,
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
if (isIdle) {
|
|
786
|
+
pi.sendMessage(customMessage, { triggerTurn: true });
|
|
787
|
+
} else {
|
|
788
|
+
pi.sendMessage(customMessage, {
|
|
789
|
+
triggerTurn: true,
|
|
790
|
+
deliverAs: mode === "follow_up" ? "followUp" : "steer",
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
respond(true, "send", { delivered: true, mode: isIdle ? "direct" : mode });
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
respond(false, command.type, undefined, `Unsupported command: ${command.type}`);
|
|
799
799
|
}
|
|
800
800
|
|
|
801
801
|
// ============================================================================
|
|
@@ -803,47 +803,47 @@ async function handleCommand(
|
|
|
803
803
|
// ============================================================================
|
|
804
804
|
|
|
805
805
|
async function createServer(pi: ExtensionAPI, state: SocketState, socketPath: string): Promise<net.Server> {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
806
|
+
const server = net.createServer((socket) => {
|
|
807
|
+
socket.setEncoding("utf8");
|
|
808
|
+
let buffer = "";
|
|
809
|
+
socket.on("data", (chunk) => {
|
|
810
|
+
buffer += chunk;
|
|
811
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
812
|
+
while (newlineIndex !== -1) {
|
|
813
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
814
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
815
|
+
newlineIndex = buffer.indexOf("\n");
|
|
816
|
+
if (!line) {continue;}
|
|
817
|
+
|
|
818
|
+
const parsed = parseCommand(line);
|
|
819
|
+
if (parsed.error) {
|
|
820
|
+
if (state.context) {
|
|
821
|
+
void syncAlias(state, state.context);
|
|
822
|
+
}
|
|
823
|
+
writeResponse(socket, {
|
|
824
|
+
type: "response",
|
|
825
|
+
command: "parse",
|
|
826
|
+
success: false,
|
|
827
|
+
error: `Failed to parse command: ${parsed.error}`,
|
|
828
|
+
});
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
handleCommand(pi, state, parsed.command!, socket);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
836
|
|
|
837
837
|
// Wait for server to start listening, with error handling
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
838
|
+
await new Promise<void>((resolve, reject) => {
|
|
839
|
+
server.once("error", reject);
|
|
840
|
+
server.listen(socketPath, () => {
|
|
841
|
+
server.removeListener("error", reject);
|
|
842
|
+
resolve();
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
return server;
|
|
847
847
|
}
|
|
848
848
|
|
|
849
849
|
interface RpcClientOptions {
|
|
@@ -852,280 +852,280 @@ interface RpcClientOptions {
|
|
|
852
852
|
}
|
|
853
853
|
|
|
854
854
|
async function sendRpcCommand(
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
855
|
+
socketPath: string,
|
|
856
|
+
command: RpcCommand,
|
|
857
|
+
options: RpcClientOptions = {},
|
|
858
858
|
): Promise<{ response: RpcResponse; event?: { message?: ExtractedMessage; turnIndex?: number } }> {
|
|
859
|
-
|
|
859
|
+
const { timeout = 5000, waitForEvent } = options;
|
|
860
860
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
861
|
+
return new Promise((resolve, reject) => {
|
|
862
|
+
const socket = net.createConnection(socketPath);
|
|
863
|
+
socket.setEncoding("utf8");
|
|
864
864
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
865
|
+
const timeoutHandle = setTimeout(() => {
|
|
866
|
+
socket.destroy(new Error("timeout"));
|
|
867
|
+
}, timeout);
|
|
868
868
|
|
|
869
|
-
|
|
870
|
-
|
|
869
|
+
let buffer = "";
|
|
870
|
+
let response: RpcResponse | null = null;
|
|
871
871
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
872
|
+
const cleanup = () => {
|
|
873
|
+
clearTimeout(timeoutHandle);
|
|
874
|
+
socket.removeAllListeners();
|
|
875
|
+
};
|
|
876
876
|
|
|
877
|
-
|
|
878
|
-
|
|
877
|
+
socket.on("connect", () => {
|
|
878
|
+
socket.write(`${JSON.stringify(command)}\n`);
|
|
879
879
|
|
|
880
880
|
// If waiting for turn_end, also subscribe
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
881
|
+
if (waitForEvent === "turn_end") {
|
|
882
|
+
const subscribeCmd: RpcSubscribeCommand = { type: "subscribe", event: "turn_end" };
|
|
883
|
+
socket.write(`${JSON.stringify(subscribeCmd)}\n`);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
socket.on("data", (chunk) => {
|
|
888
|
+
buffer += chunk;
|
|
889
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
890
|
+
while (newlineIndex !== -1) {
|
|
891
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
892
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
893
|
+
newlineIndex = buffer.indexOf("\n");
|
|
894
|
+
if (!line) {continue;}
|
|
895
|
+
|
|
896
|
+
try {
|
|
897
|
+
const msg = JSON.parse(line);
|
|
898
898
|
|
|
899
899
|
// Handle response
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
900
|
+
if (msg.type === "response") {
|
|
901
|
+
if (msg.command === command.type) {
|
|
902
|
+
response = msg;
|
|
903
903
|
// If not waiting for event, we're done
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
904
|
+
if (!waitForEvent) {
|
|
905
|
+
cleanup();
|
|
906
|
+
socket.end();
|
|
907
|
+
resolve({ response });
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
911
|
// Ignore subscribe response
|
|
912
|
-
|
|
913
|
-
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
914
|
|
|
915
915
|
// Handle turn_end event
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
916
|
+
if (msg.type === "event" && msg.event === "turn_end" && waitForEvent === "turn_end") {
|
|
917
|
+
cleanup();
|
|
918
|
+
socket.end();
|
|
919
|
+
if (!response) {
|
|
920
|
+
reject(new Error("Received event before response"));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
resolve({ response, event: msg.data || {} });
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
} catch {
|
|
927
927
|
// Ignore parse errors, keep waiting
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
socket.on("error", (error) => {
|
|
933
|
+
cleanup();
|
|
934
|
+
reject(error);
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
937
|
}
|
|
938
938
|
|
|
939
939
|
async function startControlServer(pi: ExtensionAPI, state: SocketState, ctx: ExtensionContext): Promise<void> {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
940
|
+
await ensureControlDir();
|
|
941
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
942
|
+
const socketPath = getSocketPath(sessionId);
|
|
943
|
+
|
|
944
|
+
if (state.socketPath === socketPath && state.server) {
|
|
945
|
+
state.context = ctx;
|
|
946
|
+
await syncAlias(state, ctx);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
await stopControlServer(state);
|
|
951
|
+
await removeSocket(socketPath);
|
|
952
|
+
|
|
953
|
+
state.context = ctx;
|
|
954
|
+
state.socketPath = socketPath;
|
|
955
|
+
state.server = await createServer(pi, state, socketPath);
|
|
956
|
+
state.alias = null;
|
|
957
|
+
await syncAlias(state, ctx);
|
|
958
958
|
}
|
|
959
959
|
|
|
960
960
|
async function stopControlServer(state: SocketState): Promise<void> {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
961
|
+
if (!state.server) {
|
|
962
|
+
await removeAliasesForSocket(state.socketPath);
|
|
963
|
+
await removeSocket(state.socketPath);
|
|
964
|
+
state.socketPath = null;
|
|
965
|
+
state.alias = null;
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const socketPath = state.socketPath;
|
|
970
|
+
state.socketPath = null;
|
|
971
|
+
state.turnEndSubscriptions = [];
|
|
972
|
+
await new Promise<void>((resolve) => state.server?.close(() => resolve()));
|
|
973
|
+
state.server = null;
|
|
974
|
+
await removeAliasesForSocket(socketPath);
|
|
975
|
+
await removeSocket(socketPath);
|
|
976
|
+
state.alias = null;
|
|
977
977
|
}
|
|
978
978
|
|
|
979
979
|
function updateStatus(ctx: ExtensionContext | null, enabled: boolean): void {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
980
|
+
if (!ctx?.hasUI) {return;}
|
|
981
|
+
if (!enabled) {
|
|
982
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
986
|
+
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("dim", `session ${sessionId}`));
|
|
987
987
|
}
|
|
988
988
|
|
|
989
989
|
function updateSessionEnv(ctx: ExtensionContext | null, enabled: boolean): void {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
990
|
+
if (!enabled) {
|
|
991
|
+
delete process.env.PI_SESSION_ID;
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (!ctx) {return;}
|
|
995
|
+
process.env.PI_SESSION_ID = ctx.sessionManager.getSessionId();
|
|
996
996
|
}
|
|
997
997
|
|
|
998
998
|
// Extension factories run before extension flag values are hydrated into runtime.flagValues,
|
|
999
999
|
// so we inspect argv directly when deciding whether to register tools at load time.
|
|
1000
1000
|
function wasBooleanFlagPassed(flagName: string): boolean {
|
|
1001
|
-
|
|
1002
|
-
|
|
1001
|
+
const flag = `--${flagName}`;
|
|
1002
|
+
return process.argv.slice(2).includes(flag);
|
|
1003
1003
|
}
|
|
1004
1004
|
|
|
1005
1005
|
function shouldRegisterControlTools(pi: ExtensionAPI): boolean {
|
|
1006
|
-
|
|
1006
|
+
return pi.getFlag(CONTROL_FLAG) === true || wasBooleanFlagPassed(CONTROL_FLAG);
|
|
1007
1007
|
}
|
|
1008
1008
|
|
|
1009
1009
|
// ============================================================================
|
|
1010
1010
|
// Extension Export
|
|
1011
1011
|
// ============================================================================
|
|
1012
1012
|
|
|
1013
|
-
export default function
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1013
|
+
export default function(pi: ExtensionAPI) {
|
|
1014
|
+
pi.registerFlag(CONTROL_FLAG, {
|
|
1015
|
+
description: "Enable per-session control socket under the scoped agent directory",
|
|
1016
|
+
type: "boolean",
|
|
1017
|
+
});
|
|
1018
|
+
pi.registerFlag(CONTROL_TARGET_FLAG, {
|
|
1019
|
+
description: "Target session name or session id for startup control send",
|
|
1020
|
+
type: "string",
|
|
1021
|
+
});
|
|
1022
|
+
pi.registerFlag(CONTROL_SEND_MESSAGE_FLAG, {
|
|
1023
|
+
description: "Message to send to --control-session at startup",
|
|
1024
|
+
type: "string",
|
|
1025
|
+
});
|
|
1026
|
+
pi.registerFlag(CONTROL_SEND_MODE_FLAG, {
|
|
1027
|
+
description: "Startup send mode: steer or follow_up",
|
|
1028
|
+
type: "string",
|
|
1029
|
+
default: "steer",
|
|
1030
|
+
});
|
|
1031
|
+
pi.registerFlag(CONTROL_SEND_WAIT_FLAG, {
|
|
1032
|
+
description: "Startup send wait mode: turn_end or message_processed",
|
|
1033
|
+
type: "string",
|
|
1034
|
+
});
|
|
1035
|
+
pi.registerFlag(CONTROL_SEND_INCLUDE_SENDER_FLAG, {
|
|
1036
|
+
description: "Include <sender_info> in startup messages (advanced; default: false)",
|
|
1037
|
+
type: "boolean",
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
let cliSendHandled = false;
|
|
1041
|
+
|
|
1042
|
+
const state: SocketState = {
|
|
1043
|
+
server: null,
|
|
1044
|
+
socketPath: null,
|
|
1045
|
+
context: null,
|
|
1046
|
+
alias: null,
|
|
1047
|
+
aliasTimer: null,
|
|
1048
|
+
turnEndSubscriptions: [],
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
pi.registerMessageRenderer(SESSION_MESSAGE_TYPE, renderSessionMessage);
|
|
1052
|
+
|
|
1053
|
+
if (shouldRegisterControlTools(pi)) {
|
|
1054
|
+
registerSessionTool(pi, state);
|
|
1055
|
+
registerListSessionsTool(pi);
|
|
1056
|
+
}
|
|
1057
|
+
registerControlSessionsCommand(pi);
|
|
1058
|
+
|
|
1059
|
+
const refreshServer = async(ctx: ExtensionContext) => {
|
|
1060
|
+
const enabled = pi.getFlag(CONTROL_FLAG) === true;
|
|
1061
|
+
if (!enabled) {
|
|
1062
|
+
if (state.aliasTimer) {
|
|
1063
|
+
clearInterval(state.aliasTimer);
|
|
1064
|
+
state.aliasTimer = null;
|
|
1065
|
+
}
|
|
1066
|
+
await stopControlServer(state);
|
|
1067
|
+
updateStatus(ctx, false);
|
|
1068
|
+
updateSessionEnv(ctx, false);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
await startControlServer(pi, state, ctx);
|
|
1072
|
+
if (!state.aliasTimer) {
|
|
1073
|
+
state.aliasTimer = setInterval(() => {
|
|
1074
|
+
if (!state.context) {return;}
|
|
1075
|
+
void syncAlias(state, state.context);
|
|
1076
|
+
}, 1000);
|
|
1077
|
+
}
|
|
1078
|
+
updateStatus(ctx, true);
|
|
1079
|
+
updateSessionEnv(ctx, true);
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
pi.on("session_start", async(_event, ctx) => {
|
|
1083
|
+
await refreshServer(ctx);
|
|
1084
|
+
if (!cliSendHandled) {
|
|
1085
|
+
cliSendHandled = true;
|
|
1086
|
+
await maybeHandleStartupControlSend(pi, ctx);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
pi.on("session_switch", async(_event, ctx) => {
|
|
1091
|
+
await refreshServer(ctx);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
pi.on("session_fork", async(_event, ctx) => {
|
|
1095
|
+
await refreshServer(ctx);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
pi.on("session_shutdown", async() => {
|
|
1099
|
+
if (state.aliasTimer) {
|
|
1100
|
+
clearInterval(state.aliasTimer);
|
|
1101
|
+
state.aliasTimer = null;
|
|
1102
|
+
}
|
|
1103
|
+
updateStatus(state.context, false);
|
|
1104
|
+
updateSessionEnv(state.context, false);
|
|
1105
|
+
await stopControlServer(state);
|
|
1106
|
+
});
|
|
1107
1107
|
|
|
1108
1108
|
// Fire turn_end events to subscribers
|
|
1109
|
-
|
|
1110
|
-
|
|
1109
|
+
pi.on("turn_end", (event: TurnEndEvent, ctx: ExtensionContext) => {
|
|
1110
|
+
if (state.turnEndSubscriptions.length === 0) {return;}
|
|
1111
1111
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1112
|
+
void syncAlias(state, ctx);
|
|
1113
|
+
const lastMessage = getLastAssistantMessage(ctx);
|
|
1114
|
+
const eventData = { message: lastMessage, turnIndex: event.turnIndex };
|
|
1115
1115
|
|
|
1116
1116
|
// Fire to all subscribers (one-shot)
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1117
|
+
const subscriptions = [...state.turnEndSubscriptions];
|
|
1118
|
+
state.turnEndSubscriptions = [];
|
|
1119
|
+
|
|
1120
|
+
for (const sub of subscriptions) {
|
|
1121
|
+
writeEvent(sub.socket, {
|
|
1122
|
+
type: "event",
|
|
1123
|
+
event: "turn_end",
|
|
1124
|
+
data: eventData,
|
|
1125
|
+
subscriptionId: sub.subscriptionId,
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
1129
|
}
|
|
1130
1130
|
|
|
1131
1131
|
// ============================================================================
|
|
@@ -1133,10 +1133,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1133
1133
|
// ============================================================================
|
|
1134
1134
|
|
|
1135
1135
|
function registerSessionTool(pi: ExtensionAPI, state: SocketState): void {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1136
|
+
pi.registerTool({
|
|
1137
|
+
name: "send_to_session",
|
|
1138
|
+
label: "Send To Session",
|
|
1139
|
+
description: `Interact with another running pi session via its control socket.
|
|
1140
1140
|
|
|
1141
1141
|
Actions:
|
|
1142
1142
|
- send: Send a message (default). Requires 'message' parameter.
|
|
@@ -1173,366 +1173,366 @@ CLI bridge (for shell scripts/background jobs):
|
|
|
1173
1173
|
Note: If you ask the target session to reply back via sender_info, do not use wait_until; waiting is redundant and can duplicate responses.
|
|
1174
1174
|
|
|
1175
1175
|
Messages automatically include sender session info for replies. When you want a response, instruct the target session to reply directly to the sender by calling send_to_session with the sender_info reference (do not poll get_message).`,
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1176
|
+
parameters: Type.Object({
|
|
1177
|
+
sessionId: Type.Optional(Type.String({ description: "Target session id (UUID)" })),
|
|
1178
|
+
sessionName: Type.Optional(Type.String({ description: "Target session name (alias)" })),
|
|
1179
|
+
action: Type.Optional(
|
|
1180
|
+
StringEnum(["send", "get_message", "get_summary", "clear"] as const, {
|
|
1181
|
+
description: "Action to perform (default: send)",
|
|
1182
|
+
default: "send",
|
|
1183
|
+
}),
|
|
1184
|
+
),
|
|
1185
|
+
message: Type.Optional(Type.String({ description: "Message to send (required for action=send)" })),
|
|
1186
|
+
mode: Type.Optional(
|
|
1187
|
+
StringEnum(["steer", "follow_up"] as const, {
|
|
1188
|
+
description: "Delivery mode for send: steer (immediate) or follow_up (after task)",
|
|
1189
|
+
default: "steer",
|
|
1190
|
+
}),
|
|
1191
|
+
),
|
|
1192
|
+
wait_until: Type.Optional(
|
|
1193
|
+
StringEnum(["turn_end", "message_processed"] as const, {
|
|
1194
|
+
description: "Wait behavior for send action",
|
|
1195
|
+
}),
|
|
1196
|
+
),
|
|
1197
|
+
}),
|
|
1198
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1199
|
+
const action = params.action ?? "send";
|
|
1200
|
+
const sessionName = params.sessionName?.trim();
|
|
1201
|
+
const sessionId = params.sessionId?.trim();
|
|
1202
|
+
let targetSessionId: string | null = null;
|
|
1203
|
+
const displayTarget = sessionName || sessionId || "";
|
|
1204
|
+
|
|
1205
|
+
if (sessionName) {
|
|
1206
|
+
targetSessionId = await resolveSessionIdFromAlias(sessionName);
|
|
1207
|
+
if (!targetSessionId) {
|
|
1208
|
+
return {
|
|
1209
|
+
content: [{ type: "text", text: "Unknown session name" }],
|
|
1210
|
+
isError: true,
|
|
1211
|
+
details: { error: "Unknown session name" },
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (sessionId) {
|
|
1217
|
+
if (!isSafeSessionId(sessionId)) {
|
|
1218
|
+
return {
|
|
1219
|
+
content: [{ type: "text", text: "Invalid session id" }],
|
|
1220
|
+
isError: true,
|
|
1221
|
+
details: { error: "Invalid session id" },
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
if (targetSessionId && targetSessionId !== sessionId) {
|
|
1225
|
+
return {
|
|
1226
|
+
content: [{ type: "text", text: "Session name does not match session id" }],
|
|
1227
|
+
isError: true,
|
|
1228
|
+
details: { error: "Session name does not match session id" },
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
targetSessionId = sessionId;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
if (!targetSessionId) {
|
|
1235
|
+
return {
|
|
1236
|
+
content: [{ type: "text", text: "Missing session id or session name" }],
|
|
1237
|
+
isError: true,
|
|
1238
|
+
details: { error: "Missing session id or session name" },
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const socketPath = getSocketPath(targetSessionId);
|
|
1243
|
+
const senderSessionId = state.context?.sessionManager.getSessionId();
|
|
1244
|
+
|
|
1245
|
+
try {
|
|
1246
1246
|
// Handle each action
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1247
|
+
if (action === "get_message") {
|
|
1248
|
+
const result = await sendRpcCommand(socketPath, { type: "get_message" });
|
|
1249
|
+
if (!result.response.success) {
|
|
1250
|
+
return {
|
|
1251
|
+
content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
|
|
1252
|
+
isError: true,
|
|
1253
|
+
details: result,
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
const data = result.response.data as { message?: ExtractedMessage };
|
|
1257
|
+
if (!data?.message) {
|
|
1258
|
+
return {
|
|
1259
|
+
content: [{ type: "text", text: "No assistant message found in session" }],
|
|
1260
|
+
details: result,
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
content: [{ type: "text", text: data.message.content }],
|
|
1265
|
+
details: { message: data.message },
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (action === "get_summary") {
|
|
1270
|
+
const result = await sendRpcCommand(socketPath, { type: "get_summary" }, { timeout: 60000 });
|
|
1271
|
+
if (!result.response.success) {
|
|
1272
|
+
return {
|
|
1273
|
+
content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
|
|
1274
|
+
isError: true,
|
|
1275
|
+
details: result,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
const data = result.response.data as { summary?: string; model?: string };
|
|
1279
|
+
if (!data?.summary) {
|
|
1280
|
+
return {
|
|
1281
|
+
content: [{ type: "text", text: "No summary generated" }],
|
|
1282
|
+
details: result,
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
return {
|
|
1286
|
+
content: [{ type: "text", text: `Summary (via ${data.model}):\n\n${data.summary}` }],
|
|
1287
|
+
details: { summary: data.summary, model: data.model },
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (action === "clear") {
|
|
1292
|
+
const result = await sendRpcCommand(socketPath, { type: "clear", summarize: false }, { timeout: 10000 });
|
|
1293
|
+
if (!result.response.success) {
|
|
1294
|
+
return {
|
|
1295
|
+
content: [{ type: "text", text: `Failed to clear: ${result.response.error ?? "unknown error"}` }],
|
|
1296
|
+
isError: true,
|
|
1297
|
+
details: result,
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
const data = result.response.data as { cleared?: boolean; alreadyAtRoot?: boolean };
|
|
1301
|
+
const msg = data?.alreadyAtRoot ? "Session already at root" : "Session cleared";
|
|
1302
|
+
return {
|
|
1303
|
+
content: [{ type: "text", text: msg }],
|
|
1304
|
+
details: data,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
1307
|
|
|
1308
1308
|
// action === "send"
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1309
|
+
if (!params.message || params.message.trim().length === 0) {
|
|
1310
|
+
return {
|
|
1311
|
+
content: [{ type: "text", text: "Missing message for send action" }],
|
|
1312
|
+
isError: true,
|
|
1313
|
+
details: { error: "Missing message" },
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const senderSessionName = state.context?.sessionManager.getSessionName()?.trim();
|
|
1318
|
+
const senderInfo = senderSessionId
|
|
1319
|
+
? `\n\n<sender_info>${JSON.stringify({
|
|
1320
|
+
sessionId: senderSessionId,
|
|
1321
|
+
sessionName: senderSessionName || undefined,
|
|
1322
|
+
})}</sender_info>`
|
|
1323
|
+
: "";
|
|
1324
|
+
|
|
1325
|
+
const sendCommand: RpcSendCommand = {
|
|
1326
|
+
type: "send",
|
|
1327
|
+
message: params.message + senderInfo,
|
|
1328
|
+
mode: params.mode ?? "steer",
|
|
1329
|
+
};
|
|
1330
1330
|
|
|
1331
1331
|
// Determine wait behavior
|
|
1332
|
-
|
|
1332
|
+
if (params.wait_until === "message_processed") {
|
|
1333
1333
|
// Just send and confirm delivery
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1334
|
+
const result = await sendRpcCommand(socketPath, sendCommand);
|
|
1335
|
+
if (!result.response.success) {
|
|
1336
|
+
return {
|
|
1337
|
+
content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
|
|
1338
|
+
isError: true,
|
|
1339
|
+
details: result,
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
return {
|
|
1343
|
+
content: [{ type: "text", text: "Message delivered to session" }],
|
|
1344
|
+
details: result.response.data,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (params.wait_until === "turn_end") {
|
|
1349
1349
|
// Send and wait for turn to complete
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1350
|
+
const result = await sendRpcCommand(socketPath, sendCommand, {
|
|
1351
|
+
timeout: 300000, // 5 minutes
|
|
1352
|
+
waitForEvent: "turn_end",
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
if (!result.response.success) {
|
|
1356
|
+
return {
|
|
1357
|
+
content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
|
|
1358
|
+
isError: true,
|
|
1359
|
+
details: result,
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const lastMessage = result.event?.message;
|
|
1364
|
+
if (!lastMessage) {
|
|
1365
|
+
return {
|
|
1366
|
+
content: [{ type: "text", text: "Turn completed but no assistant message found" }],
|
|
1367
|
+
details: { turnIndex: result.event?.turnIndex },
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
return {
|
|
1372
|
+
content: [{ type: "text", text: lastMessage.content }],
|
|
1373
|
+
details: { message: lastMessage, turnIndex: result.event?.turnIndex },
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
1376
|
|
|
1377
1377
|
// No wait - just send
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1378
|
+
const result = await sendRpcCommand(socketPath, sendCommand);
|
|
1379
|
+
if (!result.response.success) {
|
|
1380
|
+
return {
|
|
1381
|
+
content: [{ type: "text", text: `Failed: ${result.response.error ?? "unknown error"}` }],
|
|
1382
|
+
isError: true,
|
|
1383
|
+
details: result,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return {
|
|
1388
|
+
content: [{ type: "text", text: `Message sent to session ${displayTarget || targetSessionId}` }],
|
|
1389
|
+
details: result.response.data,
|
|
1390
|
+
};
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1393
|
+
return {
|
|
1394
|
+
content: [{ type: "text", text: `Failed: ${message}` }],
|
|
1395
|
+
isError: true,
|
|
1396
|
+
details: { error: message },
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
|
|
1401
|
+
renderCall(args, theme) {
|
|
1402
|
+
const action = args.action ?? "send";
|
|
1403
|
+
const sessionRef = args.sessionName ?? args.sessionId ?? "...";
|
|
1404
|
+
const shortSessionRef = sessionRef.length > 12 ? sessionRef.slice(0, 8) + "..." : sessionRef;
|
|
1405
1405
|
|
|
1406
1406
|
// Build the header line
|
|
1407
|
-
|
|
1408
|
-
|
|
1407
|
+
let header = theme.fg("toolTitle", theme.bold("→ session "));
|
|
1408
|
+
header += theme.fg("accent", shortSessionRef);
|
|
1409
1409
|
|
|
1410
1410
|
// Add action-specific info
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1411
|
+
if (action === "send") {
|
|
1412
|
+
const mode = args.mode ?? "steer";
|
|
1413
|
+
const wait = args.wait_until;
|
|
1414
|
+
let info = theme.fg("muted", ` (${mode}`);
|
|
1415
|
+
if (wait) {info += theme.fg("dim", `, wait: ${wait}`);}
|
|
1416
|
+
info += theme.fg("muted", ")");
|
|
1417
|
+
header += info;
|
|
1418
|
+
} else {
|
|
1419
|
+
header += theme.fg("muted", ` (${action})`);
|
|
1420
|
+
}
|
|
1421
1421
|
|
|
1422
1422
|
// For send action, show the message
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1423
|
+
if (action === "send" && args.message) {
|
|
1424
|
+
const msg = args.message;
|
|
1425
|
+
const preview = msg.length > 80 ? msg.slice(0, 80) + "..." : msg;
|
|
1426
1426
|
// Handle multi-line messages
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1427
|
+
const firstLine = preview.split("\n")[0];
|
|
1428
|
+
const hasMore = preview.includes("\n") || msg.length > 80;
|
|
1429
|
+
return new Text(
|
|
1430
|
+
header + "\n " + theme.fg("dim", `"${firstLine}${hasMore ? "..." : ""}"`),
|
|
1431
|
+
0,
|
|
1432
|
+
0,
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
return new Text(header, 0, 0);
|
|
1437
|
+
},
|
|
1438
|
+
|
|
1439
|
+
renderResult(result, { expanded }, theme) {
|
|
1440
|
+
const details = result.details as Record<string, unknown> | undefined;
|
|
1441
|
+
const isError = result.isError === true;
|
|
1442
1442
|
|
|
1443
1443
|
// Error case
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1444
|
+
if (isError || details?.error) {
|
|
1445
|
+
const errorMsg = (details?.error as string) || result.content[0]?.type === "text" ? (result.content[0] as { type: "text"; text: string }).text : "Unknown error";
|
|
1446
|
+
return new Text(theme.fg("error", "✗ ") + theme.fg("error", errorMsg), 0, 0);
|
|
1447
|
+
}
|
|
1448
1448
|
|
|
1449
1449
|
// Detect action from details structure
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1450
|
+
const hasMessage = details && "message" in details && details.message;
|
|
1451
|
+
const hasSummary = details && "summary" in details;
|
|
1452
|
+
const hasCleared = details && "cleared" in details;
|
|
1453
|
+
const hasTurnIndex = details && "turnIndex" in details;
|
|
1454
1454
|
|
|
1455
1455
|
// get_message or turn_end result with message
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1456
|
+
if (hasMessage) {
|
|
1457
|
+
const message = details.message as ExtractedMessage;
|
|
1458
|
+
const icon = theme.fg("success", "✓");
|
|
1459
|
+
|
|
1460
|
+
if (expanded) {
|
|
1461
|
+
const container = new Container();
|
|
1462
|
+
container.addChild(new Text(icon + theme.fg("muted", " Message received"), 0, 0));
|
|
1463
|
+
container.addChild(new Spacer(1));
|
|
1464
|
+
container.addChild(new Markdown(message.content, 0, 0, getMarkdownTheme()));
|
|
1465
|
+
if (hasTurnIndex) {
|
|
1466
|
+
container.addChild(new Spacer(1));
|
|
1467
|
+
container.addChild(new Text(theme.fg("dim", `Turn #${details.turnIndex}`), 0, 0));
|
|
1468
|
+
}
|
|
1469
|
+
return container;
|
|
1470
|
+
}
|
|
1471
1471
|
|
|
1472
1472
|
// Collapsed view - show preview
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1473
|
+
const preview = message.content.length > 200
|
|
1474
|
+
? message.content.slice(0, 200) + "..."
|
|
1475
|
+
: message.content;
|
|
1476
|
+
const lines = preview.split("\n").slice(0, 5);
|
|
1477
|
+
let text = icon + theme.fg("muted", " Message received");
|
|
1478
|
+
if (hasTurnIndex) {text += theme.fg("dim", ` (turn #${details.turnIndex})`);}
|
|
1479
|
+
text += "\n" + theme.fg("toolOutput", lines.join("\n"));
|
|
1480
|
+
if (message.content.split("\n").length > 5 || message.content.length > 200) {
|
|
1481
|
+
text += "\n" + theme.fg("dim", "(Ctrl+O to expand)");
|
|
1482
|
+
}
|
|
1483
|
+
return new Text(text, 0, 0);
|
|
1484
|
+
}
|
|
1485
1485
|
|
|
1486
1486
|
// get_summary result
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1487
|
+
if (hasSummary) {
|
|
1488
|
+
const summary = details.summary as string;
|
|
1489
|
+
const model = details.model as string | undefined;
|
|
1490
|
+
const icon = theme.fg("success", "✓");
|
|
1491
|
+
|
|
1492
|
+
if (expanded) {
|
|
1493
|
+
const container = new Container();
|
|
1494
|
+
let header = icon + theme.fg("muted", " Summary");
|
|
1495
|
+
if (model) {header += theme.fg("dim", ` via ${model}`);}
|
|
1496
|
+
container.addChild(new Text(header, 0, 0));
|
|
1497
|
+
container.addChild(new Spacer(1));
|
|
1498
|
+
container.addChild(new Markdown(summary, 0, 0, getMarkdownTheme()));
|
|
1499
|
+
return container;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const preview = summary.length > 200 ? summary.slice(0, 200) + "..." : summary;
|
|
1503
|
+
const lines = preview.split("\n").slice(0, 5);
|
|
1504
|
+
let text = icon + theme.fg("muted", " Summary");
|
|
1505
|
+
if (model) {text += theme.fg("dim", ` via ${model}`);}
|
|
1506
|
+
text += "\n" + theme.fg("toolOutput", lines.join("\n"));
|
|
1507
|
+
if (summary.split("\n").length > 5 || summary.length > 200) {
|
|
1508
|
+
text += "\n" + theme.fg("dim", "(Ctrl+O to expand)");
|
|
1509
|
+
}
|
|
1510
|
+
return new Text(text, 0, 0);
|
|
1511
|
+
}
|
|
1512
1512
|
|
|
1513
1513
|
// clear result
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1514
|
+
if (hasCleared) {
|
|
1515
|
+
const alreadyAtRoot = details.alreadyAtRoot as boolean | undefined;
|
|
1516
|
+
const icon = theme.fg("success", "✓");
|
|
1517
|
+
const msg = alreadyAtRoot ? "Session already at root" : "Session cleared";
|
|
1518
|
+
return new Text(icon + " " + theme.fg("muted", msg), 0, 0);
|
|
1519
|
+
}
|
|
1520
1520
|
|
|
1521
1521
|
// send result (no wait or message_processed)
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1522
|
+
if (details && "delivered" in details) {
|
|
1523
|
+
const mode = details.mode as string | undefined;
|
|
1524
|
+
const icon = theme.fg("success", "✓");
|
|
1525
|
+
let text = icon + theme.fg("muted", " Message delivered");
|
|
1526
|
+
if (mode) {text += theme.fg("dim", ` (${mode})`);}
|
|
1527
|
+
return new Text(text, 0, 0);
|
|
1528
|
+
}
|
|
1529
1529
|
|
|
1530
1530
|
// Fallback - just show the text content
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1531
|
+
const text = result.content[0];
|
|
1532
|
+
const content = text?.type === "text" ? text.text : "(no output)";
|
|
1533
|
+
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", content), 0, 0);
|
|
1534
|
+
},
|
|
1535
|
+
});
|
|
1536
1536
|
}
|
|
1537
1537
|
|
|
1538
1538
|
// ============================================================================
|
|
@@ -1540,32 +1540,32 @@ Messages automatically include sender session info for replies. When you want a
|
|
|
1540
1540
|
// ============================================================================
|
|
1541
1541
|
|
|
1542
1542
|
function registerListSessionsTool(pi: ExtensionAPI): void {
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1543
|
+
pi.registerTool({
|
|
1544
|
+
name: "list_sessions",
|
|
1545
|
+
label: "List Sessions",
|
|
1546
|
+
description: "List live sessions that expose a control socket (optionally with session names). Use this for discovery only; for the current session id in shell/bash use $PI_SESSION_ID.",
|
|
1547
|
+
parameters: Type.Object({}),
|
|
1548
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
|
|
1549
|
+
const sessions = await getLiveSessions();
|
|
1550
|
+
|
|
1551
|
+
if (sessions.length === 0) {
|
|
1552
|
+
return {
|
|
1553
|
+
content: [{ type: "text", text: "No live sessions found." }],
|
|
1554
|
+
details: { sessions: [] },
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const lines = sessions.map((session) => {
|
|
1559
|
+
const name = session.name ? ` (${session.name})` : "";
|
|
1560
|
+
return `- ${session.sessionId}${name}`;
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
return {
|
|
1564
|
+
content: [{ type: "text", text: `Live sessions:\n${lines.join("\n")}` }],
|
|
1565
|
+
details: { sessions },
|
|
1566
|
+
};
|
|
1567
|
+
},
|
|
1568
|
+
});
|
|
1569
1569
|
}
|
|
1570
1570
|
|
|
1571
1571
|
type StartupControlSendOptions = {
|
|
@@ -1577,203 +1577,203 @@ type StartupControlSendOptions = {
|
|
|
1577
1577
|
};
|
|
1578
1578
|
|
|
1579
1579
|
function normalizeMode(raw: string): "steer" | "follow_up" | null {
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1580
|
+
const value = raw.trim().toLowerCase();
|
|
1581
|
+
if (value === "steer") {return "steer";}
|
|
1582
|
+
if (value === "follow_up" || value === "follow-up" || value === "followup") {return "follow_up";}
|
|
1583
|
+
return null;
|
|
1584
1584
|
}
|
|
1585
1585
|
|
|
1586
1586
|
function normalizeWaitUntil(raw: string): "turn_end" | "message_processed" | null {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1587
|
+
const value = raw.trim().toLowerCase();
|
|
1588
|
+
if (value === "turn_end" || value === "turn-end") {return "turn_end";}
|
|
1589
|
+
if (value === "message_processed" || value === "message-processed") {return "message_processed";}
|
|
1590
|
+
return null;
|
|
1591
1591
|
}
|
|
1592
1592
|
|
|
1593
1593
|
function getStringFlag(pi: ExtensionAPI, name: string): string | undefined {
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1594
|
+
const value = pi.getFlag(name);
|
|
1595
|
+
if (typeof value !== "string") {return undefined;}
|
|
1596
|
+
const trimmed = value.trim();
|
|
1597
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
1598
1598
|
}
|
|
1599
1599
|
|
|
1600
1600
|
function parseStartupControlSendOptions(pi: ExtensionAPI): { options?: StartupControlSendOptions; error?: string } {
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1601
|
+
const target = getStringFlag(pi, CONTROL_TARGET_FLAG);
|
|
1602
|
+
const message = getStringFlag(pi, CONTROL_SEND_MESSAGE_FLAG);
|
|
1603
|
+
|
|
1604
|
+
if (!target && !message) {
|
|
1605
|
+
return {};
|
|
1606
|
+
}
|
|
1607
|
+
if (target && !message) {
|
|
1608
|
+
return { error: `Missing --${CONTROL_SEND_MESSAGE_FLAG} (required with --${CONTROL_TARGET_FLAG})` };
|
|
1609
|
+
}
|
|
1610
|
+
if (!target && message) {
|
|
1611
|
+
return { error: `Missing --${CONTROL_TARGET_FLAG} (required with --${CONTROL_SEND_MESSAGE_FLAG})` };
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const rawMode = getStringFlag(pi, CONTROL_SEND_MODE_FLAG) ?? "steer";
|
|
1615
|
+
const mode = normalizeMode(rawMode);
|
|
1616
|
+
if (!mode) {
|
|
1617
|
+
return { error: `Invalid --${CONTROL_SEND_MODE_FLAG}: ${rawMode}. Use steer|follow_up.` };
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const rawWait = getStringFlag(pi, CONTROL_SEND_WAIT_FLAG);
|
|
1621
|
+
let waitUntil: "turn_end" | "message_processed" | undefined;
|
|
1622
|
+
if (rawWait) {
|
|
1623
|
+
const normalized = normalizeWaitUntil(rawWait);
|
|
1624
|
+
if (!normalized) {
|
|
1625
|
+
return {
|
|
1626
|
+
error: `Invalid --${CONTROL_SEND_WAIT_FLAG}: ${rawWait}. Use turn_end|message_processed.`,
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
waitUntil = normalized;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
const includeSenderInfo = pi.getFlag(CONTROL_SEND_INCLUDE_SENDER_FLAG) === true;
|
|
1633
|
+
|
|
1634
|
+
return {
|
|
1635
|
+
options: {
|
|
1636
|
+
target: target!,
|
|
1637
|
+
message: message!,
|
|
1638
|
+
mode,
|
|
1639
|
+
waitUntil,
|
|
1640
|
+
includeSenderInfo,
|
|
1641
|
+
},
|
|
1642
|
+
};
|
|
1643
1643
|
}
|
|
1644
1644
|
|
|
1645
1645
|
function reportStartupControlSend(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1646
|
+
if (ctx.hasUI) {
|
|
1647
|
+
ctx.ui.notify(message, level);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
if (level === "error") {
|
|
1651
|
+
console.error(message);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
console.log(message);
|
|
1655
1655
|
}
|
|
1656
1656
|
|
|
1657
1657
|
async function maybeHandleStartupControlSend(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1658
|
+
const parsed = parseStartupControlSendOptions(pi);
|
|
1659
|
+
if (!parsed.options) {
|
|
1660
|
+
if (parsed.error) {
|
|
1661
|
+
reportStartupControlSend(ctx, parsed.error, "error");
|
|
1662
|
+
}
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const { target, message, mode, waitUntil, includeSenderInfo } = parsed.options;
|
|
1667
|
+
let targetSessionId = await resolveSessionIdFromAlias(target);
|
|
1668
|
+
if (!targetSessionId && isSafeSessionId(target)) {
|
|
1669
|
+
targetSessionId = target;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (!targetSessionId) {
|
|
1673
|
+
reportStartupControlSend(ctx, `Unknown target session: ${target}`, "error");
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const socketPath = getSocketPath(targetSessionId);
|
|
1678
|
+
const alive = await isSocketAlive(socketPath);
|
|
1679
|
+
if (!alive) {
|
|
1680
|
+
reportStartupControlSend(ctx, `Target session not reachable: ${target}`, "error");
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const senderInfo = includeSenderInfo
|
|
1685
|
+
? (() => {
|
|
1686
|
+
const senderSessionId = ctx.sessionManager.getSessionId();
|
|
1687
|
+
const senderSessionName = ctx.sessionManager.getSessionName()?.trim();
|
|
1688
|
+
return senderSessionId
|
|
1689
|
+
? `\n\n<sender_info>${JSON.stringify({
|
|
1690
|
+
sessionId: senderSessionId,
|
|
1691
|
+
sessionName: senderSessionName || undefined,
|
|
1692
|
+
})}</sender_info>`
|
|
1693
|
+
: "";
|
|
1694
|
+
})()
|
|
1695
|
+
: "";
|
|
1696
|
+
|
|
1697
|
+
const sendCommand: RpcSendCommand = {
|
|
1698
|
+
type: "send",
|
|
1699
|
+
message: message + senderInfo,
|
|
1700
|
+
mode,
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
try {
|
|
1704
|
+
if (waitUntil === "turn_end") {
|
|
1705
|
+
const result = await sendRpcCommand(socketPath, sendCommand, {
|
|
1706
|
+
timeout: 300000,
|
|
1707
|
+
waitForEvent: "turn_end",
|
|
1708
|
+
});
|
|
1709
|
+
if (!result.response.success) {
|
|
1710
|
+
reportStartupControlSend(ctx, `Failed to send: ${result.response.error ?? "unknown error"}`, "error");
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
const lastMessage = result.event?.message;
|
|
1714
|
+
if (!lastMessage?.content) {
|
|
1715
|
+
reportStartupControlSend(ctx, `Message delivered to ${target}; turn completed without assistant output.`);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
if (ctx.hasUI) {
|
|
1719
|
+
pi.sendMessage(
|
|
1720
|
+
{
|
|
1721
|
+
customType: "control-send",
|
|
1722
|
+
content: `Startup response from ${target}:\n\n${lastMessage.content}`,
|
|
1723
|
+
display: true,
|
|
1724
|
+
},
|
|
1725
|
+
{ triggerTurn: false },
|
|
1726
|
+
);
|
|
1727
|
+
} else {
|
|
1728
|
+
console.log(lastMessage.content);
|
|
1729
|
+
}
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
const result = await sendRpcCommand(socketPath, sendCommand, { timeout: 30000 });
|
|
1734
|
+
if (!result.response.success) {
|
|
1735
|
+
reportStartupControlSend(ctx, `Failed to send: ${result.response.error ?? "unknown error"}`, "error");
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
const waitLabel = waitUntil === "message_processed" ? " (message processed)" : "";
|
|
1740
|
+
reportStartupControlSend(ctx, `Message sent to ${target}${waitLabel}`);
|
|
1741
|
+
} catch (error) {
|
|
1742
|
+
const msg = error instanceof Error ? error.message : "unknown error";
|
|
1743
|
+
reportStartupControlSend(ctx, `Failed to send to ${target}: ${msg}`, "error");
|
|
1744
|
+
}
|
|
1745
1745
|
}
|
|
1746
1746
|
|
|
1747
1747
|
function registerControlSessionsCommand(pi: ExtensionAPI): void {
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1748
|
+
pi.registerCommand("control-sessions", {
|
|
1749
|
+
description: "List controllable sessions (from session-control sockets)",
|
|
1750
|
+
handler: async(_args, ctx) => {
|
|
1751
|
+
if (pi.getFlag(CONTROL_FLAG) !== true) {
|
|
1752
|
+
if (ctx.hasUI) {
|
|
1753
|
+
ctx.ui.notify("Session control not enabled (use --session-control)", "warning");
|
|
1754
|
+
}
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
const sessions = await getLiveSessions();
|
|
1759
|
+
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
1760
|
+
const lines = sessions.map((session) => {
|
|
1761
|
+
const name = session.name ? ` (${session.name})` : "";
|
|
1762
|
+
const current = session.sessionId === currentSessionId ? " (current)" : "";
|
|
1763
|
+
return `- ${session.sessionId}${name}${current}`;
|
|
1764
|
+
});
|
|
1765
|
+
const content = sessions.length === 0
|
|
1766
|
+
? "No live sessions found."
|
|
1767
|
+
: `Controllable sessions:\n${lines.join("\n")}`;
|
|
1768
|
+
|
|
1769
|
+
pi.sendMessage(
|
|
1770
|
+
{
|
|
1771
|
+
customType: "control-sessions",
|
|
1772
|
+
content,
|
|
1773
|
+
display: true,
|
|
1774
|
+
},
|
|
1775
|
+
{ triggerTurn: false },
|
|
1776
|
+
);
|
|
1777
|
+
},
|
|
1778
|
+
});
|
|
1779
1779
|
}
|