agent-sh 0.12.25 → 0.12.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-loop.js +2 -2
- package/dist/agent/conversation-state.js +22 -1
- package/dist/index.js +3 -1
- package/dist/install.js +84 -4
- package/dist/shell/index.d.ts +5 -0
- package/dist/shell/index.js +13 -8
- package/dist/shell/input-handler.js +75 -27
- package/dist/shell/tui-input-view.d.ts +5 -0
- package/dist/shell/tui-input-view.js +137 -96
- package/dist/utils/floating-panel.d.ts +16 -4
- package/dist/utils/floating-panel.js +209 -66
- package/dist/utils/terminal-buffer.d.ts +6 -9
- package/dist/utils/terminal-buffer.js +21 -53
- package/examples/extensions/emacs-buffer.ts +364 -0
- package/examples/extensions/opencode-bridge/index.ts +255 -37
- package/examples/extensions/overlay-agent.ts +28 -5
- package/examples/extensions/terminal-buffer.ts +174 -33
- package/examples/extensions/tunnel-vision.ts +405 -0
- package/examples/extensions/web-access.ts +3 -108
- package/package.json +1 -1
|
@@ -5,9 +5,20 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Requires opencode authenticated locally (`opencode auth login`).
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
createOpencode,
|
|
10
|
+
type OpencodeClient,
|
|
11
|
+
type Event,
|
|
12
|
+
type Part,
|
|
13
|
+
type ToolPart,
|
|
14
|
+
type QuestionRequest,
|
|
15
|
+
type QuestionInfo,
|
|
16
|
+
} from "@opencode-ai/sdk/v2";
|
|
9
17
|
import type { ExtensionContext } from "agent-sh/types";
|
|
18
|
+
import type { InteractiveSession } from "agent-sh/agent/types";
|
|
10
19
|
import { computeDiff, type DiffResult } from "agent-sh/utils/diff";
|
|
20
|
+
import { createToolUI } from "agent-sh/utils/tool-interactive";
|
|
21
|
+
import { palette as p } from "agent-sh/utils/palette";
|
|
11
22
|
|
|
12
23
|
function parseUnifiedDiff(patch: string): DiffResult | null {
|
|
13
24
|
if (!patch) return null;
|
|
@@ -49,7 +60,7 @@ function parseUnifiedDiff(patch: string): DiffResult | null {
|
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
export default function activate(ctx: ExtensionContext): void {
|
|
52
|
-
const { bus, call } = ctx;
|
|
63
|
+
const { bus, call, compositor } = ctx;
|
|
53
64
|
|
|
54
65
|
const cwd = (): string => {
|
|
55
66
|
const v = call("cwd");
|
|
@@ -81,6 +92,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
81
92
|
// prompt() and SSE deltas race; resolve the turn on session.idle.
|
|
82
93
|
let pendingTurnEnd: (() => void) | null = null;
|
|
83
94
|
let turnIdleSeen = false;
|
|
95
|
+
let turnError: string | null = null;
|
|
84
96
|
|
|
85
97
|
const listeners: Array<{ event: string; fn: Function }> = [];
|
|
86
98
|
|
|
@@ -112,6 +124,9 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
112
124
|
|
|
113
125
|
function handleToolPart(part: ToolPart): void {
|
|
114
126
|
const { callID, tool: toolName, state } = part;
|
|
127
|
+
// Question tool is presented via an interactive picker (see question.asked) —
|
|
128
|
+
// skip the timeline entry to avoid a duplicate "running" bar.
|
|
129
|
+
if (toolName === "question") return;
|
|
115
130
|
const kind = toolKind(toolName);
|
|
116
131
|
|
|
117
132
|
if (state.status !== "pending" && !announcedTools.has(callID)) {
|
|
@@ -177,6 +192,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
177
192
|
turnText += text;
|
|
178
193
|
}
|
|
179
194
|
|
|
195
|
+
|
|
180
196
|
function handleEvent(event: Event): void {
|
|
181
197
|
if (!sessionId) return;
|
|
182
198
|
const evType = (event as any).type as string;
|
|
@@ -209,23 +225,86 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
209
225
|
}
|
|
210
226
|
case "session.error": {
|
|
211
227
|
const err = props.error as { message?: string } | undefined;
|
|
212
|
-
|
|
228
|
+
const message = err?.message ?? "opencode session error";
|
|
229
|
+
// session.prompt() does not always reject on session error;
|
|
230
|
+
// drive turn-end ourselves and abort to unstick a hanging prompt().
|
|
231
|
+
turnError = message;
|
|
232
|
+
bus.emit("agent:error", { message });
|
|
233
|
+
turnIdleSeen = true;
|
|
234
|
+
pendingTurnEnd?.();
|
|
235
|
+
if (runtime && sessionId) {
|
|
236
|
+
runtime.client.session
|
|
237
|
+
.abort({ sessionID: sessionId, directory: sessionDirectory ?? undefined })
|
|
238
|
+
.catch(() => { /* abort is best-effort */ });
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "question.asked": {
|
|
243
|
+
const req = props as QuestionRequest;
|
|
244
|
+
if (!runtime) break;
|
|
245
|
+
const ui = createToolUI(bus, compositor.surface("agent"));
|
|
246
|
+
ui.custom(createQuestionSession(req.questions)).then(async (result: QuestionResult) => {
|
|
247
|
+
if (!runtime) return;
|
|
248
|
+
// Record the question + answer as a synthetic tool entry so the
|
|
249
|
+
// timeline shows what was asked and what the user picked.
|
|
250
|
+
const callID = `question-${req.id}`;
|
|
251
|
+
const detail = req.questions.length === 1
|
|
252
|
+
? req.questions[0]!.question
|
|
253
|
+
: req.questions.map((q, i) => `${q.header || `Q${i + 1}`}: ${q.question}`).join("; ");
|
|
254
|
+
bus.emit("agent:tool-started", {
|
|
255
|
+
title: "question",
|
|
256
|
+
toolCallId: callID,
|
|
257
|
+
kind: "execute",
|
|
258
|
+
displayDetail: detail,
|
|
259
|
+
});
|
|
260
|
+
if (result.cancelled) {
|
|
261
|
+
bus.emitTransform("agent:tool-completed", {
|
|
262
|
+
toolCallId: callID,
|
|
263
|
+
exitCode: 1,
|
|
264
|
+
rawOutput: "cancelled",
|
|
265
|
+
kind: "execute",
|
|
266
|
+
resultDisplay: { summary: "cancelled" },
|
|
267
|
+
});
|
|
268
|
+
runtime.client.question
|
|
269
|
+
.reject({ requestID: req.id, directory: sessionDirectory ?? undefined })
|
|
270
|
+
.catch(() => { /* best-effort */ });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const summary = result.answers.length === 1
|
|
274
|
+
? result.answers[0]!.join(", ")
|
|
275
|
+
: result.answers
|
|
276
|
+
.map((ans, i) => `${req.questions[i]!.header || `Q${i + 1}`}: ${ans.join(", ")}`)
|
|
277
|
+
.join("; ");
|
|
278
|
+
bus.emitTransform("agent:tool-completed", {
|
|
279
|
+
toolCallId: callID,
|
|
280
|
+
exitCode: 0,
|
|
281
|
+
rawOutput: summary,
|
|
282
|
+
kind: "execute",
|
|
283
|
+
resultDisplay: { summary },
|
|
284
|
+
});
|
|
285
|
+
try {
|
|
286
|
+
await runtime.client.question.reply({
|
|
287
|
+
requestID: req.id,
|
|
288
|
+
answers: result.answers,
|
|
289
|
+
directory: sessionDirectory ?? undefined,
|
|
290
|
+
});
|
|
291
|
+
} catch (err) {
|
|
292
|
+
bus.emit("agent:error", {
|
|
293
|
+
message: err instanceof Error ? err.message : String(err),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
});
|
|
213
297
|
break;
|
|
214
298
|
}
|
|
215
299
|
// Without a reply the gated tool hangs forever. The bridge has no
|
|
216
300
|
// interactive approval UI, so auto-approve — mirrors claude-code-
|
|
217
301
|
// bridge's permissionMode: "acceptEdits". Set permission.edit:
|
|
218
302
|
// "allow" in opencode.json to skip the round-trip entirely.
|
|
219
|
-
case "permission.asked":
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
.postSessionIdPermissionsPermissionId({
|
|
225
|
-
path: { id: sessionId, permissionID },
|
|
226
|
-
query: sessionDirectory ? { directory: sessionDirectory } : undefined,
|
|
227
|
-
body: { response: "once" },
|
|
228
|
-
})
|
|
303
|
+
case "permission.asked": {
|
|
304
|
+
const requestID = props.id as string | undefined;
|
|
305
|
+
if (!requestID || !runtime) break;
|
|
306
|
+
runtime.client.permission
|
|
307
|
+
.reply({ requestID, reply: "once", directory: sessionDirectory ?? undefined })
|
|
229
308
|
.catch(() => { /* approval is best-effort */ });
|
|
230
309
|
break;
|
|
231
310
|
}
|
|
@@ -235,7 +314,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
235
314
|
async function consumeEvents(client: OpencodeClient, signal: AbortSignal): Promise<void> {
|
|
236
315
|
while (!signal.aborted) {
|
|
237
316
|
try {
|
|
238
|
-
const result = await client.event.subscribe({ signal });
|
|
317
|
+
const result = await client.event.subscribe({}, { signal });
|
|
239
318
|
for await (const ev of result.stream) {
|
|
240
319
|
if (signal.aborted) return;
|
|
241
320
|
handleEvent(ev as Event);
|
|
@@ -261,6 +340,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
261
340
|
bus.emit("agent:processing-start", {});
|
|
262
341
|
turnText = "";
|
|
263
342
|
turnIdleSeen = false;
|
|
343
|
+
turnError = null;
|
|
264
344
|
// Set the idle waiter BEFORE prompt() so a fast session.idle can't
|
|
265
345
|
// race in before we're listening.
|
|
266
346
|
const idlePromise = new Promise<void>((resolve) => {
|
|
@@ -272,11 +352,9 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
272
352
|
|
|
273
353
|
try {
|
|
274
354
|
const res = await runtime.client.session.prompt({
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
parts: [{ type: "text", text: finalPrompt }],
|
|
279
|
-
},
|
|
355
|
+
sessionID: sessionId,
|
|
356
|
+
directory: sessionDirectory ?? undefined,
|
|
357
|
+
parts: [{ type: "text", text: finalPrompt }],
|
|
280
358
|
});
|
|
281
359
|
if (!turnIdleSeen) {
|
|
282
360
|
await Promise.race([
|
|
@@ -284,23 +362,29 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
284
362
|
new Promise<void>((r) => setTimeout(r, 60_000)),
|
|
285
363
|
]);
|
|
286
364
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
365
|
+
if (turnError) {
|
|
366
|
+
bus.emitTransform("agent:response-done", { response: "" });
|
|
367
|
+
} else {
|
|
368
|
+
// Fallback if SSE never delivered text (network blip, missed
|
|
369
|
+
// partKinds entry); the prompt response always carries the final.
|
|
370
|
+
if (!turnText && res.data?.parts) {
|
|
371
|
+
for (const p of res.data.parts) {
|
|
372
|
+
if (p.type === "text" && p.text) turnText += p.text;
|
|
373
|
+
}
|
|
374
|
+
if (turnText) {
|
|
375
|
+
bus.emitTransform("agent:response-chunk", {
|
|
376
|
+
blocks: [{ type: "text" as const, text: turnText }],
|
|
377
|
+
});
|
|
378
|
+
}
|
|
297
379
|
}
|
|
380
|
+
bus.emitTransform("agent:response-done", { response: turnText });
|
|
298
381
|
}
|
|
299
|
-
bus.emitTransform("agent:response-done", { response: turnText });
|
|
300
382
|
} catch (err) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
383
|
+
if (!turnError) {
|
|
384
|
+
bus.emit("agent:error", {
|
|
385
|
+
message: err instanceof Error ? err.message : String(err),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
304
388
|
} finally {
|
|
305
389
|
pendingTurnEnd = null;
|
|
306
390
|
bus.emit("agent:processing-done", {});
|
|
@@ -310,7 +394,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
310
394
|
const onCancel = async () => {
|
|
311
395
|
if (!runtime || !sessionId) return;
|
|
312
396
|
try {
|
|
313
|
-
await runtime.client.session.abort({
|
|
397
|
+
await runtime.client.session.abort({ sessionID: sessionId, directory: sessionDirectory ?? undefined });
|
|
314
398
|
} catch { /* abort is best-effort */ }
|
|
315
399
|
};
|
|
316
400
|
|
|
@@ -321,7 +405,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
321
405
|
partKinds.clear();
|
|
322
406
|
// /reset is the one moment we deliberately let the project switch.
|
|
323
407
|
sessionDirectory = cwd();
|
|
324
|
-
const res = await runtime.client.session.create({
|
|
408
|
+
const res = await runtime.client.session.create({ directory: sessionDirectory });
|
|
325
409
|
sessionId = res.data?.id ?? null;
|
|
326
410
|
};
|
|
327
411
|
|
|
@@ -352,13 +436,13 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
352
436
|
void consumeEvents(runtime.client, streamAbort.signal);
|
|
353
437
|
|
|
354
438
|
sessionDirectory = cwd();
|
|
355
|
-
const res = await runtime.client.session.create({
|
|
439
|
+
const res = await runtime.client.session.create({ directory: sessionDirectory });
|
|
356
440
|
sessionId = res.data?.id ?? null;
|
|
357
441
|
if (!sessionId) throw new Error("session.create returned no id");
|
|
358
442
|
|
|
359
443
|
wireListeners();
|
|
360
444
|
booting = false;
|
|
361
|
-
bus.emit("agent:info", { name: "opencode", version: "
|
|
445
|
+
bus.emit("agent:info", { name: "opencode", version: "2.x" });
|
|
362
446
|
} catch (err) {
|
|
363
447
|
booting = false;
|
|
364
448
|
bus.emit("ui:error", {
|
|
@@ -381,3 +465,137 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
381
465
|
},
|
|
382
466
|
});
|
|
383
467
|
}
|
|
468
|
+
|
|
469
|
+
// ── Interactive question picker ──────────────────────────────────
|
|
470
|
+
|
|
471
|
+
type QuestionResult = { answers: string[][]; cancelled: boolean };
|
|
472
|
+
|
|
473
|
+
function isKey(data: string, key: string): boolean {
|
|
474
|
+
switch (key) {
|
|
475
|
+
case "up": return data === "\x1b[A" || data === "\x1bOA";
|
|
476
|
+
case "down": return data === "\x1b[B" || data === "\x1bOB";
|
|
477
|
+
case "left": return data === "\x1b[D" || data === "\x1bOD";
|
|
478
|
+
case "right": return data === "\x1b[C" || data === "\x1bOC";
|
|
479
|
+
case "enter": return data === "\r" || data === "\n";
|
|
480
|
+
case "escape": return data === "\x1b";
|
|
481
|
+
case "tab": return data === "\t";
|
|
482
|
+
default: return data === key;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function createQuestionSession(questions: QuestionInfo[]): InteractiveSession<QuestionResult> {
|
|
487
|
+
const isMulti = questions.length > 1;
|
|
488
|
+
let tab = 0;
|
|
489
|
+
let optionIdx = 0;
|
|
490
|
+
// Per-question selected option indices (set, to support `multiple`).
|
|
491
|
+
const selections: Set<number>[] = questions.map(() => new Set());
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
render(width) {
|
|
495
|
+
const w = Math.min(80, width);
|
|
496
|
+
const lines: string[] = [];
|
|
497
|
+
const q = questions[tab]!;
|
|
498
|
+
const sel = selections[tab]!;
|
|
499
|
+
|
|
500
|
+
lines.push(`${p.muted}${"─".repeat(w)}${p.reset}`);
|
|
501
|
+
|
|
502
|
+
if (isMulti) {
|
|
503
|
+
const tabs = questions.map((qq, i) => {
|
|
504
|
+
const answered = selections[i]!.size > 0;
|
|
505
|
+
const active = i === tab;
|
|
506
|
+
const box = answered ? "■" : "□";
|
|
507
|
+
const label = ` ${box} ${qq.header || `Q${i + 1}`} `;
|
|
508
|
+
return active
|
|
509
|
+
? `${p.accent}${p.bold}${label}${p.reset}`
|
|
510
|
+
: `${p.muted}${label}${p.reset}`;
|
|
511
|
+
});
|
|
512
|
+
lines.push(` ${tabs.join(" ")}`);
|
|
513
|
+
lines.push("");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
lines.push(` ${q.question}`);
|
|
517
|
+
lines.push("");
|
|
518
|
+
for (let i = 0; i < q.options.length; i++) {
|
|
519
|
+
const opt = q.options[i]!;
|
|
520
|
+
const cursor = i === optionIdx ? p.accent : "";
|
|
521
|
+
const reset = i === optionIdx ? p.reset : "";
|
|
522
|
+
const arrow = i === optionIdx ? `${p.accent}>${p.reset} ` : " ";
|
|
523
|
+
const mark = q.multiple
|
|
524
|
+
? (sel.has(i) ? "[x]" : "[ ]")
|
|
525
|
+
: (sel.has(i) ? "(o)" : "( )");
|
|
526
|
+
lines.push(`${arrow}${cursor}${mark} ${i + 1}. ${opt.label}${reset}`);
|
|
527
|
+
if (opt.description) {
|
|
528
|
+
lines.push(` ${p.muted}${opt.description}${p.reset}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
lines.push("");
|
|
533
|
+
const navKeys = isMulti ? "Tab/←→ switch • " : "";
|
|
534
|
+
const actionKeys = q.multiple
|
|
535
|
+
? "↑↓ navigate • Space toggle • Enter confirm • Esc cancel"
|
|
536
|
+
: "↑↓ navigate • Enter select • Esc cancel";
|
|
537
|
+
lines.push(` ${p.dim}${navKeys}${actionKeys}${p.reset}`);
|
|
538
|
+
lines.push(`${p.muted}${"─".repeat(w)}${p.reset}`);
|
|
539
|
+
return lines;
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
handleInput(data, done) {
|
|
543
|
+
const q = questions[tab]!;
|
|
544
|
+
const sel = selections[tab]!;
|
|
545
|
+
|
|
546
|
+
if (isKey(data, "escape")) {
|
|
547
|
+
done({ answers: [], cancelled: true });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (isMulti) {
|
|
552
|
+
if (isKey(data, "tab") || isKey(data, "right")) {
|
|
553
|
+
tab = (tab + 1) % questions.length;
|
|
554
|
+
optionIdx = 0;
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (isKey(data, "left")) {
|
|
558
|
+
tab = (tab - 1 + questions.length) % questions.length;
|
|
559
|
+
optionIdx = 0;
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (isKey(data, "up")) {
|
|
565
|
+
optionIdx = Math.max(0, optionIdx - 1);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (isKey(data, "down")) {
|
|
569
|
+
optionIdx = Math.min(q.options.length - 1, optionIdx + 1);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (q.multiple && data === " ") {
|
|
574
|
+
if (sel.has(optionIdx)) sel.delete(optionIdx); else sel.add(optionIdx);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (isKey(data, "enter")) {
|
|
579
|
+
if (!q.multiple) {
|
|
580
|
+
sel.clear();
|
|
581
|
+
sel.add(optionIdx);
|
|
582
|
+
}
|
|
583
|
+
if (sel.size === 0) return;
|
|
584
|
+
|
|
585
|
+
const allAnswered = selections.every((s) => s.size > 0);
|
|
586
|
+
if (!isMulti || allAnswered) {
|
|
587
|
+
const answers = questions.map((qq, i) =>
|
|
588
|
+
Array.from(selections[i]!).map((idx) => qq.options[idx]!.label),
|
|
589
|
+
);
|
|
590
|
+
done({ answers, cancelled: false });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const next = selections.findIndex((s) => s.size === 0);
|
|
594
|
+
if (next !== -1) {
|
|
595
|
+
tab = next;
|
|
596
|
+
optionIdx = 0;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
@@ -104,8 +104,18 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
104
104
|
surface: panelSurface,
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
if (query.startsWith("/")) {
|
|
108
|
+
// Sync commands (/model, /help) render via ui:info and leave us in
|
|
109
|
+
// input phase; ones that fan out to agent:submit flip the phase via
|
|
110
|
+
// the agent:processing-start listener below.
|
|
111
|
+
const spaceIdx = query.indexOf(" ");
|
|
112
|
+
const name = spaceIdx === -1 ? query : query.slice(0, spaceIdx);
|
|
113
|
+
const args = spaceIdx === -1 ? "" : query.slice(spaceIdx + 1).trim();
|
|
114
|
+
bus.emit("command:execute", { name, args });
|
|
115
|
+
} else {
|
|
116
|
+
panel.setActive();
|
|
117
|
+
session.submit(query);
|
|
118
|
+
}
|
|
109
119
|
});
|
|
110
120
|
|
|
111
121
|
panel.handlers.advise("panel:show", (_next) => {
|
|
@@ -114,9 +124,9 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
114
124
|
}
|
|
115
125
|
});
|
|
116
126
|
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
panel.handlers.advise("panel:
|
|
127
|
+
// While the agent is still working, keep the session open so output and
|
|
128
|
+
// tool calls survive a hide. Once it's idle, close to release redirects.
|
|
129
|
+
panel.handlers.advise("panel:hide", (next) => {
|
|
120
130
|
next();
|
|
121
131
|
if (session && !panel.processing) {
|
|
122
132
|
session.close();
|
|
@@ -124,6 +134,19 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
124
134
|
}
|
|
125
135
|
});
|
|
126
136
|
|
|
137
|
+
panel.handlers.advise("panel:reset", (next) => {
|
|
138
|
+
next();
|
|
139
|
+
if (session) {
|
|
140
|
+
session.close();
|
|
141
|
+
session = null;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Picks up turns triggered indirectly (e.g. /skill:foo → agent:submit).
|
|
146
|
+
bus.on("agent:processing-start", () => {
|
|
147
|
+
if (panel.active && !panel.processing) panel.setActive();
|
|
148
|
+
});
|
|
149
|
+
|
|
127
150
|
bus.on("agent:processing-done", () => {
|
|
128
151
|
if (panel.active) panel.setDone();
|
|
129
152
|
});
|