@spencerboucher/switchyard 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/bridge/pi-model.js +44 -0
- package/dist/bridge/switchyard-bridge.js +24 -2
- package/dist/bridge/switchyard-terminal.js +608 -0
- package/dist/switchyard.mjs +3811 -1811
- package/dist/terminal/holder.js +305 -0
- package/package.json +6 -3
- package/web-dist/assets/{AutomationView-BhEfjAtI.js → AutomationView-DgZn3ltn.js} +2 -2
- package/web-dist/assets/{AutomationView-BhEfjAtI.js.map → AutomationView-DgZn3ltn.js.map} +1 -1
- package/web-dist/assets/{InteractionCard-DxEZUNrD.js → InteractionCard-cvPWKSu1.js} +2 -2
- package/web-dist/assets/{InteractionCard-DxEZUNrD.js.map → InteractionCard-cvPWKSu1.js.map} +1 -1
- package/web-dist/assets/{Menu-C4GUY_ym.js → Menu-GymfuHGP.js} +2 -2
- package/web-dist/assets/Menu-GymfuHGP.js.map +1 -0
- package/web-dist/assets/{Overlay-C-GqmwUn.js → Overlay-oYnnwvFM.js} +2 -2
- package/web-dist/assets/{Overlay-C-GqmwUn.js.map → Overlay-oYnnwvFM.js.map} +1 -1
- package/web-dist/assets/{attention-Dzqdk9Gz.js → attention-Bxg9W15n.js} +2 -2
- package/web-dist/assets/{attention-Dzqdk9Gz.js.map → attention-Bxg9W15n.js.map} +1 -1
- package/web-dist/assets/{automations-DzTTwxBl.js → automations-DdB97dnL.js} +2 -2
- package/web-dist/assets/{automations-DzTTwxBl.js.map → automations-DdB97dnL.js.map} +1 -1
- package/web-dist/assets/{board-C5mG_8rg.js → board-s4LuDPcg.js} +2 -2
- package/web-dist/assets/{board-C5mG_8rg.js.map → board-s4LuDPcg.js.map} +1 -1
- package/web-dist/assets/code-B0KXn7lD.js +8 -0
- package/web-dist/assets/code-B0KXn7lD.js.map +1 -0
- package/web-dist/assets/{describe-BbCZVvJP.js → describe-C_3FJuSs.js} +2 -2
- package/web-dist/assets/{describe-BbCZVvJP.js.map → describe-C_3FJuSs.js.map} +1 -1
- package/web-dist/assets/dist-Bgkh2hc5.js +56 -0
- package/web-dist/assets/dist-Bgkh2hc5.js.map +1 -0
- package/web-dist/assets/{enroll-DWJANJtw.js → enroll-DijmoolY.js} +2 -2
- package/web-dist/assets/{enroll-DWJANJtw.js.map → enroll-DijmoolY.js.map} +1 -1
- package/web-dist/assets/index-C5n2QB-Y.css +2 -0
- package/web-dist/assets/{index-afOJ8NRR.js → index-Cf6ZUCfK.js} +3 -3
- package/web-dist/assets/{index-afOJ8NRR.js.map → index-Cf6ZUCfK.js.map} +1 -1
- package/web-dist/assets/{more-B6qxC6M-.js → more-CRGJsz6E.js} +2 -2
- package/web-dist/assets/{more-B6qxC6M-.js.map → more-CRGJsz6E.js.map} +1 -1
- package/web-dist/assets/{now-BOWxgbt3.js → now-D6lDofYy.js} +2 -2
- package/web-dist/assets/{now-BOWxgbt3.js.map → now-D6lDofYy.js.map} +1 -1
- package/web-dist/assets/{settings-BpkmIv_4.js → settings-Csal1F-W.js} +6 -6
- package/web-dist/assets/settings-Csal1F-W.js.map +1 -0
- package/web-dist/assets/{useNow-D9Zxr5Jb.js → useNow-Bc_NlDY3.js} +2 -2
- package/web-dist/assets/{useNow-D9Zxr5Jb.js.map → useNow-Bc_NlDY3.js.map} +1 -1
- package/web-dist/index.html +2 -2
- package/web-dist/sw.js +1 -1
- package/web-dist/assets/Menu-C4GUY_ym.js.map +0 -1
- package/web-dist/assets/code-9shyh1Gw.js +0 -8
- package/web-dist/assets/code-9shyh1Gw.js.map +0 -1
- package/web-dist/assets/dist-PaQekTP8.js +0 -56
- package/web-dist/assets/dist-PaQekTP8.js.map +0 -1
- package/web-dist/assets/index-ByiuDoG0.css +0 -2
- package/web-dist/assets/settings-BpkmIv_4.js.map +0 -1
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Switchyard terminal extension, loaded with `-e` into an interactive Pi that runs inside a
|
|
3
|
+
* Switchyard terminal holder (a pty). It is the control channel the host uses instead of
|
|
4
|
+
* `pi --mode rpc`: it answers the same JSONL commands (`prompt`, `steer`, `follow_up`, `abort`,
|
|
5
|
+
* `get_state`, ...), forwards session events in RPC shape, and lets remote clients answer extension
|
|
6
|
+
* dialogs while they are also shown in the terminal (whichever answer comes first wins).
|
|
7
|
+
*
|
|
8
|
+
* It connects to the holder's socket named by `SWITCHYARD_CONTROL_SOCKET`, imports only Node
|
|
9
|
+
* built-ins, and never throws: without the variable it does nothing.
|
|
10
|
+
*
|
|
11
|
+
* Dialogs: every extension's `ctx.ui` is the same object, so replacing `select`, `confirm`,
|
|
12
|
+
* `input` and `editor` on it routes all dialogs through the race below. When that object cannot be
|
|
13
|
+
* patched, `remoteDialogs` is false and the host tells remote clients to answer in the terminal.
|
|
14
|
+
*/
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { createConnection } from "node:net";
|
|
17
|
+
import { modelInfo } from "./pi-model.js";
|
|
18
|
+
const PATCHED = Symbol.for("switchyard.terminal");
|
|
19
|
+
const WRAPPED = Symbol.for("switchyard.terminal.wrapped");
|
|
20
|
+
function messageText(message) {
|
|
21
|
+
const content = message?.content;
|
|
22
|
+
if (typeof content === "string")
|
|
23
|
+
return content;
|
|
24
|
+
if (!Array.isArray(content))
|
|
25
|
+
return "";
|
|
26
|
+
return content
|
|
27
|
+
.filter((c) => c?.type === "text" && typeof c.text === "string")
|
|
28
|
+
.map((c) => c.text)
|
|
29
|
+
.join("\n");
|
|
30
|
+
}
|
|
31
|
+
/** Pi's `toJsonEvent` for `message_update` (`dist/modes/json-event.js`). */
|
|
32
|
+
export function jsonMessageUpdate(event) {
|
|
33
|
+
const aev = event?.assistantMessageEvent ?? {};
|
|
34
|
+
let out;
|
|
35
|
+
if (aev.type === "toolcall_start") {
|
|
36
|
+
const { partial, ...rest } = aev;
|
|
37
|
+
const call = partial?.content?.[aev.contentIndex];
|
|
38
|
+
out = { ...rest, id: call?.id, toolName: call?.name };
|
|
39
|
+
}
|
|
40
|
+
else if (aev && typeof aev === "object" && "partial" in aev) {
|
|
41
|
+
const { partial: _p, ...rest } = aev;
|
|
42
|
+
out = rest;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
out = aev;
|
|
46
|
+
}
|
|
47
|
+
return { type: "message_update", usage: event?.message?.usage, assistantMessageEvent: out };
|
|
48
|
+
}
|
|
49
|
+
function addUsage(totals, usage) {
|
|
50
|
+
if (!usage || typeof usage !== "object")
|
|
51
|
+
return;
|
|
52
|
+
if (typeof usage.totalTokens === "number")
|
|
53
|
+
totals.tokens += usage.totalTokens;
|
|
54
|
+
if (typeof usage.cost?.total === "number")
|
|
55
|
+
totals.cost += usage.cost.total;
|
|
56
|
+
}
|
|
57
|
+
/** Token and cost totals over session entries, like Pi's `getSessionStats()`. */
|
|
58
|
+
export function sessionTotals(entries) {
|
|
59
|
+
const totals = { tokens: 0, cost: 0 };
|
|
60
|
+
for (const e of entries) {
|
|
61
|
+
if (e?.type === "usage")
|
|
62
|
+
addUsage(totals, e.usage);
|
|
63
|
+
else if ((e?.type === "branch_summary" || e?.type === "compaction") && e.usage) {
|
|
64
|
+
addUsage(totals, e.usage);
|
|
65
|
+
}
|
|
66
|
+
else if (e?.type === "message") {
|
|
67
|
+
const m = e.message;
|
|
68
|
+
if (m?.role === "assistant" || m?.role === "toolResult")
|
|
69
|
+
addUsage(totals, m.usage);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return totals;
|
|
73
|
+
}
|
|
74
|
+
export default function switchyardTerminal(pi) {
|
|
75
|
+
const socketPath = process.env.SWITCHYARD_CONTROL_SOCKET;
|
|
76
|
+
if (!socketPath)
|
|
77
|
+
return;
|
|
78
|
+
let ctx = null;
|
|
79
|
+
let remoteDialogs = false;
|
|
80
|
+
let entryCount = 0;
|
|
81
|
+
const pending = new Map();
|
|
82
|
+
const early = [];
|
|
83
|
+
const queue = { steering: [], followUp: [] };
|
|
84
|
+
let socket = null;
|
|
85
|
+
const send = (obj) => {
|
|
86
|
+
try {
|
|
87
|
+
if (!socket || socket.destroyed)
|
|
88
|
+
return;
|
|
89
|
+
socket.write(`${JSON.stringify(obj)}\n`);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// never throw into Pi
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const sendModelState = () => send({
|
|
96
|
+
type: "model_state",
|
|
97
|
+
model: modelInfo(ctx?.model),
|
|
98
|
+
thinkingLevel: ctx?.thinkingLevel ?? pi.getThinkingLevel?.() ?? null,
|
|
99
|
+
scoped: (ctx?.scopedModels ?? [])
|
|
100
|
+
.map((s) => s?.model?.provider && s?.model?.id ? `${s.model.provider}/${s.model.id}` : null)
|
|
101
|
+
.filter(Boolean),
|
|
102
|
+
});
|
|
103
|
+
const sendQueue = () => send({ type: "queue_update", ...queue });
|
|
104
|
+
const flushEntries = () => {
|
|
105
|
+
try {
|
|
106
|
+
const entries = ctx?.sessionManager?.getEntries?.() ?? [];
|
|
107
|
+
for (const entry of entries.slice(entryCount))
|
|
108
|
+
send({ type: "entry_appended", entry });
|
|
109
|
+
entryCount = entries.length;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// ignore
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
// ---- dialogs ----
|
|
116
|
+
const parseBody = (method, body) => {
|
|
117
|
+
if (method === "confirm")
|
|
118
|
+
return body.cancelled ? false : body.confirmed === true;
|
|
119
|
+
return body.cancelled ? undefined : body.value;
|
|
120
|
+
};
|
|
121
|
+
const race = (ui, method, orig, title, second, opts) => {
|
|
122
|
+
if (opts?.signal?.aborted)
|
|
123
|
+
return orig.call(ui, title, second, opts);
|
|
124
|
+
const id = randomUUID();
|
|
125
|
+
const ac = new AbortController();
|
|
126
|
+
const signals = [ac.signal, opts?.signal].filter(Boolean);
|
|
127
|
+
const signal = signals.length > 1 ? AbortSignal.any(signals) : ac.signal;
|
|
128
|
+
const extra = method === "select"
|
|
129
|
+
? { options: second }
|
|
130
|
+
: method === "confirm"
|
|
131
|
+
? { message: second }
|
|
132
|
+
: { placeholder: second };
|
|
133
|
+
const request = {
|
|
134
|
+
type: "extension_ui_request",
|
|
135
|
+
id,
|
|
136
|
+
method,
|
|
137
|
+
title,
|
|
138
|
+
...extra,
|
|
139
|
+
...(opts?.timeout ? { timeout: opts.timeout } : {}),
|
|
140
|
+
};
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
let done = false;
|
|
143
|
+
const finish = (value) => {
|
|
144
|
+
if (done)
|
|
145
|
+
return;
|
|
146
|
+
done = true;
|
|
147
|
+
pending.delete(id);
|
|
148
|
+
resolve(value);
|
|
149
|
+
};
|
|
150
|
+
pending.set(id, {
|
|
151
|
+
request,
|
|
152
|
+
onRemote: (body) => {
|
|
153
|
+
const value = parseBody(method, body);
|
|
154
|
+
ac.abort();
|
|
155
|
+
finish(value);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
send(request);
|
|
159
|
+
Promise.resolve()
|
|
160
|
+
.then(() => orig.call(ui, title, second, { ...(opts ?? {}), signal }))
|
|
161
|
+
.then((value) => {
|
|
162
|
+
if (ac.signal.aborted)
|
|
163
|
+
return;
|
|
164
|
+
send({
|
|
165
|
+
type: "extension_ui_closed",
|
|
166
|
+
id,
|
|
167
|
+
by: opts?.signal?.aborted ? "pi" : "terminal",
|
|
168
|
+
});
|
|
169
|
+
finish(value);
|
|
170
|
+
}, () => {
|
|
171
|
+
if (ac.signal.aborted)
|
|
172
|
+
return;
|
|
173
|
+
send({ type: "extension_ui_closed", id, by: "terminal" });
|
|
174
|
+
finish(method === "confirm" ? false : undefined);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
const raceEditor = (ui, orig, title, prefill) => {
|
|
179
|
+
const id = randomUUID();
|
|
180
|
+
const request = {
|
|
181
|
+
type: "extension_ui_request",
|
|
182
|
+
id,
|
|
183
|
+
method: "editor",
|
|
184
|
+
title,
|
|
185
|
+
...(prefill !== undefined ? { prefill } : {}),
|
|
186
|
+
};
|
|
187
|
+
return new Promise((resolve) => {
|
|
188
|
+
let done = false;
|
|
189
|
+
let remote = false;
|
|
190
|
+
const finish = (value) => {
|
|
191
|
+
if (done)
|
|
192
|
+
return;
|
|
193
|
+
done = true;
|
|
194
|
+
pending.delete(id);
|
|
195
|
+
resolve(value);
|
|
196
|
+
};
|
|
197
|
+
pending.set(id, {
|
|
198
|
+
request,
|
|
199
|
+
onRemote: (body) => {
|
|
200
|
+
remote = true;
|
|
201
|
+
// The terminal editor takes no abort signal: dismiss it with Esc.
|
|
202
|
+
send({ type: "holder_keys", data: "\u001b" });
|
|
203
|
+
finish(parseBody("editor", body));
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
send(request);
|
|
207
|
+
Promise.resolve()
|
|
208
|
+
.then(() => orig.call(ui, title, prefill))
|
|
209
|
+
.then((value) => {
|
|
210
|
+
if (remote)
|
|
211
|
+
return;
|
|
212
|
+
send({ type: "extension_ui_closed", id, by: "terminal" });
|
|
213
|
+
finish(value);
|
|
214
|
+
}, () => {
|
|
215
|
+
if (remote)
|
|
216
|
+
return;
|
|
217
|
+
send({ type: "extension_ui_closed", id, by: "terminal" });
|
|
218
|
+
finish(undefined);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
const fire = (method, fields) => send({ type: "extension_ui_request", id: randomUUID(), method, ...fields });
|
|
223
|
+
const patchUi = (c) => {
|
|
224
|
+
try {
|
|
225
|
+
const ui = c.ui;
|
|
226
|
+
if (!ui || typeof ui !== "object") {
|
|
227
|
+
remoteDialogs = false;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (ui[PATCHED]) {
|
|
231
|
+
remoteDialogs = c.ui.select === ui.select && ui.select?.[WRAPPED] === true;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const wrap = (fn) => {
|
|
235
|
+
fn[WRAPPED] = true;
|
|
236
|
+
return fn;
|
|
237
|
+
};
|
|
238
|
+
for (const method of ["select", "confirm", "input"]) {
|
|
239
|
+
const orig = ui[method];
|
|
240
|
+
if (typeof orig !== "function")
|
|
241
|
+
continue;
|
|
242
|
+
ui[method] = wrap((title, second, opts) => race(ui, method, orig, title, second, opts));
|
|
243
|
+
}
|
|
244
|
+
const origEditor = ui.editor;
|
|
245
|
+
if (typeof origEditor === "function") {
|
|
246
|
+
ui.editor = wrap((title, prefill) => raceEditor(ui, origEditor, title, prefill));
|
|
247
|
+
}
|
|
248
|
+
const origNotify = ui.notify;
|
|
249
|
+
if (typeof origNotify === "function") {
|
|
250
|
+
ui.notify = (message, type) => {
|
|
251
|
+
const r = origNotify.call(ui, message, type);
|
|
252
|
+
fire("notify", { message, notifyType: type });
|
|
253
|
+
return r;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const origStatus = ui.setStatus;
|
|
257
|
+
if (typeof origStatus === "function") {
|
|
258
|
+
ui.setStatus = (key, text) => {
|
|
259
|
+
const r = origStatus.call(ui, key, text);
|
|
260
|
+
fire("setStatus", { statusKey: key, statusText: text });
|
|
261
|
+
return r;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const origWidget = ui.setWidget;
|
|
265
|
+
if (typeof origWidget === "function") {
|
|
266
|
+
ui.setWidget = (key, content, options) => {
|
|
267
|
+
const r = origWidget.call(ui, key, content, options);
|
|
268
|
+
if (content === undefined || Array.isArray(content)) {
|
|
269
|
+
fire("setWidget", {
|
|
270
|
+
widgetKey: key,
|
|
271
|
+
widgetLines: content,
|
|
272
|
+
widgetPlacement: options?.placement,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return r;
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const origTitle = ui.setTitle;
|
|
279
|
+
if (typeof origTitle === "function") {
|
|
280
|
+
ui.setTitle = (title) => {
|
|
281
|
+
const r = origTitle.call(ui, title);
|
|
282
|
+
fire("setTitle", { title });
|
|
283
|
+
return r;
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
Object.defineProperty(ui, PATCHED, { value: true });
|
|
287
|
+
remoteDialogs = c.ui.select === ui.select && ui.select?.[WRAPPED] === true;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
remoteDialogs = false;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
// ---- commands ----
|
|
294
|
+
const content = (message, images) => {
|
|
295
|
+
if (!Array.isArray(images) || images.length === 0)
|
|
296
|
+
return message;
|
|
297
|
+
return [
|
|
298
|
+
{ type: "text", text: message },
|
|
299
|
+
...images.map((i) => ({ type: "image", data: i.data, mimeType: i.mimeType })),
|
|
300
|
+
];
|
|
301
|
+
};
|
|
302
|
+
const respond = (id, command, data, error) => {
|
|
303
|
+
if (error !== undefined)
|
|
304
|
+
send({ type: "response", id, command, success: false, error });
|
|
305
|
+
else if (data === undefined)
|
|
306
|
+
send({ type: "response", id, command, success: true });
|
|
307
|
+
else
|
|
308
|
+
send({ type: "response", id, command, success: true, data });
|
|
309
|
+
};
|
|
310
|
+
const handleCommand = (cmd) => {
|
|
311
|
+
const id = cmd.id;
|
|
312
|
+
const type = String(cmd.type);
|
|
313
|
+
if (!ctx) {
|
|
314
|
+
// Commands that arrive before the session started run once it has.
|
|
315
|
+
early.push(cmd);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
switch (type) {
|
|
320
|
+
case "get_state":
|
|
321
|
+
respond(id, type, {
|
|
322
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
323
|
+
sessionFile: ctx.sessionManager.getSessionFile() ?? null,
|
|
324
|
+
sessionName: pi.getSessionName?.() ?? null,
|
|
325
|
+
isStreaming: !ctx.isIdle(),
|
|
326
|
+
model: modelInfo(ctx.model),
|
|
327
|
+
thinkingLevel: ctx.thinkingLevel ?? pi.getThinkingLevel?.() ?? null,
|
|
328
|
+
pendingMessageCount: ctx.hasPendingMessages() ? 1 : 0,
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
case "prompt": {
|
|
332
|
+
const message = String(cmd.message ?? "");
|
|
333
|
+
if (message.startsWith("/")) {
|
|
334
|
+
const name = message.slice(1).split(/\s/)[0];
|
|
335
|
+
const known = (pi.getCommands?.() ?? []).find((c) => c.name === name);
|
|
336
|
+
if (known?.source === "extension") {
|
|
337
|
+
pi.sendUserMessage(message, { expandPromptTemplates: true });
|
|
338
|
+
respond(id, type, { disposition: "handled" });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (known) {
|
|
342
|
+
if (!ctx.isIdle()) {
|
|
343
|
+
respond(id, type, undefined, "Session is running: choose Steer or Queue follow-up");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
pi.sendUserMessage(content(message, cmd.images), { expandPromptTemplates: true });
|
|
347
|
+
respond(id, type, { disposition: "started" });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (!ctx.isIdle()) {
|
|
352
|
+
respond(id, type, undefined, "Session is running: choose Steer or Queue follow-up");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
pi.sendUserMessage(content(message, cmd.images));
|
|
356
|
+
respond(id, type, { disposition: "started" });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
case "steer":
|
|
360
|
+
case "follow_up": {
|
|
361
|
+
const message = String(cmd.message ?? "");
|
|
362
|
+
const idle = ctx.isIdle();
|
|
363
|
+
pi.sendUserMessage(content(message, cmd.images), {
|
|
364
|
+
deliverAs: type === "steer" ? "steer" : "followUp",
|
|
365
|
+
});
|
|
366
|
+
if (!idle) {
|
|
367
|
+
(type === "steer" ? queue.steering : queue.followUp).push(message);
|
|
368
|
+
sendQueue();
|
|
369
|
+
}
|
|
370
|
+
respond(id, type, { disposition: idle ? "started" : "queued" });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
case "abort":
|
|
374
|
+
ctx.abort();
|
|
375
|
+
respond(id, type);
|
|
376
|
+
return;
|
|
377
|
+
case "clear_queue":
|
|
378
|
+
respond(id, type, { steering: [], followUp: [] });
|
|
379
|
+
return;
|
|
380
|
+
case "set_session_name": {
|
|
381
|
+
const name = String(cmd.name ?? "").trim();
|
|
382
|
+
if (!name) {
|
|
383
|
+
respond(id, type, undefined, "Session name cannot be empty");
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
pi.setSessionName(name);
|
|
387
|
+
respond(id, type);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
case "get_session_stats": {
|
|
391
|
+
const totals = sessionTotals(ctx.sessionManager.getEntries());
|
|
392
|
+
respond(id, type, {
|
|
393
|
+
tokens: { total: totals.tokens },
|
|
394
|
+
cost: totals.cost,
|
|
395
|
+
contextUsage: ctx.getContextUsage?.() ?? undefined,
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
case "get_available_models":
|
|
400
|
+
respond(id, type, {
|
|
401
|
+
models: (ctx.modelRegistry?.getAvailable?.() ?? []).map(modelInfo).filter(Boolean),
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
case "get_commands":
|
|
405
|
+
respond(id, type, {
|
|
406
|
+
commands: (pi.getCommands?.() ?? []).map((c) => ({
|
|
407
|
+
name: c.name,
|
|
408
|
+
description: c.description ?? null,
|
|
409
|
+
source: c.source,
|
|
410
|
+
})),
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
case "set_model": {
|
|
414
|
+
const provider = String(cmd.provider ?? "");
|
|
415
|
+
const modelId = String(cmd.modelId ?? "");
|
|
416
|
+
const model = ctx.modelRegistry?.find?.(provider, modelId);
|
|
417
|
+
if (!model) {
|
|
418
|
+
respond(id, type, undefined, `Model not found: ${provider}/${modelId}`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
Promise.resolve(pi.setModel(model)).then((ok) => {
|
|
422
|
+
if (ok === false)
|
|
423
|
+
respond(id, type, undefined, `No API key for ${provider}/${modelId}`);
|
|
424
|
+
else
|
|
425
|
+
respond(id, type, modelInfo(ctx.model));
|
|
426
|
+
}, (err) => respond(id, type, undefined, err instanceof Error ? err.message : String(err)));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
case "set_thinking_level":
|
|
430
|
+
pi.setThinkingLevel(String(cmd.level));
|
|
431
|
+
respond(id, type);
|
|
432
|
+
return;
|
|
433
|
+
case "compact":
|
|
434
|
+
if (!ctx.isIdle()) {
|
|
435
|
+
respond(id, type, undefined, "Session is running: wait for it to finish before compacting");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
ctx.compact({
|
|
439
|
+
customInstructions: typeof cmd.customInstructions === "string" && cmd.customInstructions
|
|
440
|
+
? cmd.customInstructions
|
|
441
|
+
: undefined,
|
|
442
|
+
onComplete: () => { },
|
|
443
|
+
onError: () => { },
|
|
444
|
+
});
|
|
445
|
+
respond(id, type, { started: true });
|
|
446
|
+
return;
|
|
447
|
+
case "shutdown":
|
|
448
|
+
respond(id, type);
|
|
449
|
+
setImmediate(() => {
|
|
450
|
+
try {
|
|
451
|
+
ctx.shutdown();
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// ignore
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
return;
|
|
458
|
+
default:
|
|
459
|
+
respond(id, type, undefined, `Unknown command: ${type}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
respond(id, type, undefined, err instanceof Error ? err.message : String(err));
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const onLine = (line) => {
|
|
467
|
+
let msg;
|
|
468
|
+
try {
|
|
469
|
+
msg = JSON.parse(line);
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (!msg || typeof msg !== "object")
|
|
475
|
+
return;
|
|
476
|
+
if (msg.type === "extension_ui_response") {
|
|
477
|
+
pending.get(String(msg.id))?.onRemote(msg);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (msg.type === "control_attached") {
|
|
481
|
+
for (const p of pending.values())
|
|
482
|
+
send(p.request);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (typeof msg.type === "string" && msg.id !== undefined)
|
|
486
|
+
handleCommand(msg);
|
|
487
|
+
};
|
|
488
|
+
// ---- socket ----
|
|
489
|
+
try {
|
|
490
|
+
const s = createConnection(socketPath);
|
|
491
|
+
socket = s;
|
|
492
|
+
let buf = "";
|
|
493
|
+
s.setEncoding("utf8");
|
|
494
|
+
s.on("data", (chunk) => {
|
|
495
|
+
buf += chunk;
|
|
496
|
+
let nl = buf.indexOf("\n");
|
|
497
|
+
while (nl >= 0) {
|
|
498
|
+
const line = buf.slice(0, nl).replace(/\r$/, "");
|
|
499
|
+
buf = buf.slice(nl + 1);
|
|
500
|
+
if (line)
|
|
501
|
+
onLine(line);
|
|
502
|
+
nl = buf.indexOf("\n");
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
s.on("error", () => {
|
|
506
|
+
socket = null;
|
|
507
|
+
});
|
|
508
|
+
s.on("close", () => {
|
|
509
|
+
if (socket === s)
|
|
510
|
+
socket = null;
|
|
511
|
+
});
|
|
512
|
+
s.unref();
|
|
513
|
+
s.write(`${JSON.stringify({ role: "extension" })}\n`);
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
socket = null;
|
|
517
|
+
}
|
|
518
|
+
// ---- events ----
|
|
519
|
+
const on = (name, fn) => {
|
|
520
|
+
try {
|
|
521
|
+
pi.on(name, (event, c) => {
|
|
522
|
+
try {
|
|
523
|
+
fn(event, c);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
// never throw into Pi
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
// ignore
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
on("session_start", (event, c) => {
|
|
535
|
+
ctx = c;
|
|
536
|
+
patchUi(c);
|
|
537
|
+
entryCount = c.sessionManager.getEntries().length;
|
|
538
|
+
const sessionId = c.sessionManager.getSessionId();
|
|
539
|
+
const sessionFile = c.sessionManager.getSessionFile() ?? null;
|
|
540
|
+
send({ type: "extension_hello", pid: process.pid, remoteDialogs, sessionId, sessionFile });
|
|
541
|
+
sendModelState();
|
|
542
|
+
if (event?.reason && event.reason !== "startup") {
|
|
543
|
+
send({ type: "switchyard_native", sessionId, sessionFile });
|
|
544
|
+
}
|
|
545
|
+
for (const cmd of early.splice(0))
|
|
546
|
+
handleCommand(cmd);
|
|
547
|
+
});
|
|
548
|
+
for (const name of [
|
|
549
|
+
"agent_start",
|
|
550
|
+
"message_start",
|
|
551
|
+
"tool_execution_start",
|
|
552
|
+
"tool_execution_update",
|
|
553
|
+
"tool_execution_end",
|
|
554
|
+
"session_info_changed",
|
|
555
|
+
]) {
|
|
556
|
+
on(name, (event) => send(event));
|
|
557
|
+
}
|
|
558
|
+
on("message_start", (event) => {
|
|
559
|
+
if (event?.message?.role !== "user")
|
|
560
|
+
return;
|
|
561
|
+
const text = messageText(event.message);
|
|
562
|
+
for (const list of [queue.steering, queue.followUp]) {
|
|
563
|
+
const i = list.indexOf(text);
|
|
564
|
+
if (i >= 0) {
|
|
565
|
+
list.splice(i, 1);
|
|
566
|
+
sendQueue();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
on("message_update", (event) => send(jsonMessageUpdate(event)));
|
|
572
|
+
on("message_end", (event) => {
|
|
573
|
+
send(event);
|
|
574
|
+
flushEntries();
|
|
575
|
+
});
|
|
576
|
+
on("turn_end", () => flushEntries());
|
|
577
|
+
on("agent_settled", (event) => {
|
|
578
|
+
send({ type: "agent_settled", aborted: event?.aborted === true });
|
|
579
|
+
flushEntries();
|
|
580
|
+
if (queue.steering.length > 0 || queue.followUp.length > 0) {
|
|
581
|
+
queue.steering = [];
|
|
582
|
+
queue.followUp = [];
|
|
583
|
+
sendQueue();
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
on("session_compact", () => {
|
|
587
|
+
send({ type: "compaction_end", result: true });
|
|
588
|
+
flushEntries();
|
|
589
|
+
});
|
|
590
|
+
on("session_compact_failed", (event) => send({
|
|
591
|
+
type: "compaction_end",
|
|
592
|
+
result: null,
|
|
593
|
+
aborted: event?.aborted === true,
|
|
594
|
+
errorMessage: event?.errorMessage ?? "unknown error",
|
|
595
|
+
}));
|
|
596
|
+
on("model_select", () => sendModelState());
|
|
597
|
+
on("thinking_level_select", () => sendModelState());
|
|
598
|
+
on("session_before_compact", (event) => send({ type: "compaction_start", reason: event?.reason ?? "manual" }));
|
|
599
|
+
on("ui_prompt_start", (event) => send({ type: "ui_prompt", event: event.type, kind: event.kind, title: event.title ?? null }));
|
|
600
|
+
on("ui_prompt_end", (event) => send({ type: "ui_prompt", event: event.type, kind: event.kind, title: event.title ?? null }));
|
|
601
|
+
on("input", (event) => {
|
|
602
|
+
if (event?.source !== "interactive" || !event.streamingBehavior)
|
|
603
|
+
return;
|
|
604
|
+
(event.streamingBehavior === "steer" ? queue.steering : queue.followUp).push(String(event.text ?? ""));
|
|
605
|
+
sendQueue();
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
//# sourceMappingURL=switchyard-terminal.js.map
|