@spencerboucher/switchyard 0.6.0 → 0.8.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 +6 -6
- package/dist/bridge/switchyard-terminal.js +158 -2
- package/dist/switchyard.mjs +1332 -769
- package/package.json +1 -1
- package/web-dist/assets/AutomationView-GCthHkym.js +2 -0
- package/web-dist/assets/AutomationView-GCthHkym.js.map +1 -0
- package/web-dist/assets/Badge-CEMI3YyI.js +2 -0
- package/web-dist/assets/Badge-CEMI3YyI.js.map +1 -0
- package/web-dist/assets/InteractionCard-DaLqafmA.js +6 -0
- package/web-dist/assets/InteractionCard-DaLqafmA.js.map +1 -0
- package/web-dist/assets/{Menu-GymfuHGP.js → Menu-BcKiEc6X.js} +2 -2
- package/web-dist/assets/{Menu-GymfuHGP.js.map → Menu-BcKiEc6X.js.map} +1 -1
- package/web-dist/assets/{SegmentedControl-ChdVilXb.js → SegmentedControl-BZPzNU08.js} +2 -2
- package/web-dist/assets/{SegmentedControl-ChdVilXb.js.map → SegmentedControl-BZPzNU08.js.map} +1 -1
- package/web-dist/assets/Tabs-COH5EMql.js +184 -0
- package/web-dist/assets/Tabs-COH5EMql.js.map +1 -0
- package/web-dist/assets/TextArea-CeMEPZn4.js +2 -0
- package/web-dist/assets/TextArea-CeMEPZn4.js.map +1 -0
- package/web-dist/assets/attention-BAdea2kA.js +2 -0
- package/web-dist/assets/attention-BAdea2kA.js.map +1 -0
- package/web-dist/assets/automations-DSFUzB0w.js +2 -0
- package/web-dist/assets/automations-DSFUzB0w.js.map +1 -0
- package/web-dist/assets/board-eQ3H8yCp.js +10 -0
- package/web-dist/assets/board-eQ3H8yCp.js.map +1 -0
- package/web-dist/assets/c-Cz4nF6n_.js +1 -0
- package/web-dist/assets/c-SSSlmE39.js +2 -0
- package/web-dist/assets/c-SSSlmE39.js.map +1 -0
- package/web-dist/assets/code-D7JmYMKe.js +13 -0
- package/web-dist/assets/code-D7JmYMKe.js.map +1 -0
- package/web-dist/assets/cpp-Cts1PyIS.js +2 -0
- package/web-dist/assets/cpp-Cts1PyIS.js.map +1 -0
- package/web-dist/assets/cpp-DzjeI0uH.js +1 -0
- package/web-dist/assets/css-BGIxQoYs.js +2 -0
- package/web-dist/assets/css-BGIxQoYs.js.map +1 -0
- package/web-dist/assets/css-DgaLhXQH.js +1 -0
- package/web-dist/assets/describe-C1YimBXJ.js +5 -0
- package/web-dist/assets/describe-C1YimBXJ.js.map +1 -0
- package/web-dist/assets/diff-woXpYk--.js +2 -0
- package/web-dist/assets/diff-woXpYk--.js.map +1 -0
- package/web-dist/assets/dist-B_RpNOcK.js +56 -0
- package/web-dist/assets/dist-B_RpNOcK.js.map +1 -0
- package/web-dist/assets/dockerfile-IyjqRm3v.js +2 -0
- package/web-dist/assets/dockerfile-IyjqRm3v.js.map +1 -0
- package/web-dist/assets/enroll-1OCy5r50.js +2 -0
- package/web-dist/assets/enroll-1OCy5r50.js.map +1 -0
- package/web-dist/assets/github-dark-C-LZuMrd.js +2 -0
- package/web-dist/assets/github-dark-C-LZuMrd.js.map +1 -0
- package/web-dist/assets/github-light-EUqPIrTm.js +2 -0
- package/web-dist/assets/github-light-EUqPIrTm.js.map +1 -0
- package/web-dist/assets/go-rLFTqkRN.js +2 -0
- package/web-dist/assets/go-rLFTqkRN.js.map +1 -0
- package/web-dist/assets/html-BMn1ogTm.js +2 -0
- package/web-dist/assets/html-BMn1ogTm.js.map +1 -0
- package/web-dist/assets/html-BWDvXlHg.js +1 -0
- package/web-dist/assets/index-BhpEXLav.js +11 -0
- package/web-dist/assets/index-BhpEXLav.js.map +1 -0
- package/web-dist/assets/index-DUeOwDZ6.css +2 -0
- package/web-dist/assets/java-BtIQNnN0.js +2 -0
- package/web-dist/assets/java-BtIQNnN0.js.map +1 -0
- package/web-dist/assets/java-_ATy9zCa.js +1 -0
- package/web-dist/assets/javascript-BzbQS41l.js +2 -0
- package/web-dist/assets/javascript-BzbQS41l.js.map +1 -0
- package/web-dist/assets/javascript-CpIGwZ_Y.js +1 -0
- package/web-dist/assets/json-qhed-kSA.js +2 -0
- package/web-dist/assets/json-qhed-kSA.js.map +1 -0
- package/web-dist/assets/jsonc-CYpm1nAK.js +2 -0
- package/web-dist/assets/jsonc-CYpm1nAK.js.map +1 -0
- package/web-dist/assets/jsx-C4yDKi0A.js +1 -0
- package/web-dist/assets/jsx-D3KKoC5L.js +2 -0
- package/web-dist/assets/jsx-D3KKoC5L.js.map +1 -0
- package/web-dist/assets/logic-CBvFOyIo.js +2 -0
- package/web-dist/assets/logic-CBvFOyIo.js.map +1 -0
- package/web-dist/assets/lua-DjuecEsk.js +2 -0
- package/web-dist/assets/lua-DjuecEsk.js.map +1 -0
- package/web-dist/assets/lua-fPRBWD-V.js +1 -0
- package/web-dist/assets/{more-CRGJsz6E.js → more-RjVyxPaz.js} +2 -2
- package/web-dist/assets/{more-CRGJsz6E.js.map → more-RjVyxPaz.js.map} +1 -1
- package/web-dist/assets/now-CDjeFSLD.js +2 -0
- package/web-dist/assets/now-CDjeFSLD.js.map +1 -0
- package/web-dist/assets/python-gzcpVVnB.js +2 -0
- package/web-dist/assets/python-gzcpVVnB.js.map +1 -0
- package/web-dist/assets/ruby-Bx-U2qjP.js +2 -0
- package/web-dist/assets/ruby-Bx-U2qjP.js.map +1 -0
- package/web-dist/assets/rust-DpiIf5JO.js +2 -0
- package/web-dist/assets/rust-DpiIf5JO.js.map +1 -0
- package/web-dist/assets/scss-8bKmll0B.js +2 -0
- package/web-dist/assets/scss-8bKmll0B.js.map +1 -0
- package/web-dist/assets/send-BKjswHxB.js +2 -0
- package/web-dist/assets/send-BKjswHxB.js.map +1 -0
- package/web-dist/assets/settings-tH4UuIfJ.js +7 -0
- package/web-dist/assets/settings-tH4UuIfJ.js.map +1 -0
- package/web-dist/assets/shared-CRX6KSBm.js +3 -0
- package/web-dist/assets/shared-CRX6KSBm.js.map +1 -0
- package/web-dist/assets/shellscript-CVTB3cr6.js +1 -0
- package/web-dist/assets/shellscript-CqHdAlCp.js +2 -0
- package/web-dist/assets/shellscript-CqHdAlCp.js.map +1 -0
- package/web-dist/assets/sliders-horizontal-DAoirila.js +2 -0
- package/web-dist/assets/sliders-horizontal-DAoirila.js.map +1 -0
- package/web-dist/assets/sql-CNu3Or2W.js +2 -0
- package/web-dist/assets/sql-CNu3Or2W.js.map +1 -0
- package/web-dist/assets/sql-CfM2IJm3.js +1 -0
- package/web-dist/assets/states-DXozulGZ.js +2 -0
- package/web-dist/assets/states-DXozulGZ.js.map +1 -0
- package/web-dist/assets/swift-CyEgAFGc.js +2 -0
- package/web-dist/assets/swift-CyEgAFGc.js.map +1 -0
- package/web-dist/assets/terminal-login-CW2rE50W.js +2 -0
- package/web-dist/assets/terminal-login-CW2rE50W.js.map +1 -0
- package/web-dist/assets/toml-CcmNWLt0.js +2 -0
- package/web-dist/assets/toml-CcmNWLt0.js.map +1 -0
- package/web-dist/assets/tsx-BOvsFOI6.js +2 -0
- package/web-dist/assets/tsx-BOvsFOI6.js.map +1 -0
- package/web-dist/assets/tsx-CGosfAUg.js +1 -0
- package/web-dist/assets/typescript-Bb1J8qyy.js +1 -0
- package/web-dist/assets/typescript-j_1H8WHN.js +2 -0
- package/web-dist/assets/typescript-j_1H8WHN.js.map +1 -0
- package/web-dist/assets/useNow-C_B7JgMM.js +2 -0
- package/web-dist/assets/{useNow-Bc_NlDY3.js.map → useNow-C_B7JgMM.js.map} +1 -1
- package/web-dist/assets/xml-DuKfbPI6.js +1 -0
- package/web-dist/assets/xml-WY0zc4PG.js +2 -0
- package/web-dist/assets/xml-WY0zc4PG.js.map +1 -0
- package/web-dist/assets/yaml-DDA_XMNu.js +1 -0
- package/web-dist/assets/yaml-DijMHRDA.js +2 -0
- package/web-dist/assets/yaml-DijMHRDA.js.map +1 -0
- package/web-dist/index.html +4 -3
- package/web-dist/sw.js +1 -1
- package/web-dist/sw.js.map +1 -1
- package/web-dist/assets/AutomationView-DgZn3ltn.js +0 -2
- package/web-dist/assets/AutomationView-DgZn3ltn.js.map +0 -1
- package/web-dist/assets/InteractionCard-cvPWKSu1.js +0 -4
- package/web-dist/assets/InteractionCard-cvPWKSu1.js.map +0 -1
- package/web-dist/assets/Markdown-jXkvZeRp.js +0 -31
- package/web-dist/assets/Markdown-jXkvZeRp.js.map +0 -1
- package/web-dist/assets/Overlay-oYnnwvFM.js +0 -2
- package/web-dist/assets/Overlay-oYnnwvFM.js.map +0 -1
- package/web-dist/assets/TextArea-BsQHOQMe.js +0 -2
- package/web-dist/assets/TextArea-BsQHOQMe.js.map +0 -1
- package/web-dist/assets/attention-Bxg9W15n.js +0 -2
- package/web-dist/assets/attention-Bxg9W15n.js.map +0 -1
- package/web-dist/assets/automations-DdB97dnL.js +0 -2
- package/web-dist/assets/automations-DdB97dnL.js.map +0 -1
- package/web-dist/assets/board-s4LuDPcg.js +0 -10
- package/web-dist/assets/board-s4LuDPcg.js.map +0 -1
- package/web-dist/assets/code-B0KXn7lD.js +0 -8
- package/web-dist/assets/code-B0KXn7lD.js.map +0 -1
- package/web-dist/assets/describe-C_3FJuSs.js +0 -5
- package/web-dist/assets/describe-C_3FJuSs.js.map +0 -1
- package/web-dist/assets/dist-Bgkh2hc5.js +0 -56
- package/web-dist/assets/dist-Bgkh2hc5.js.map +0 -1
- package/web-dist/assets/enroll-DijmoolY.js +0 -2
- package/web-dist/assets/enroll-DijmoolY.js.map +0 -1
- package/web-dist/assets/index-C5n2QB-Y.css +0 -2
- package/web-dist/assets/index-Cf6ZUCfK.js +0 -11
- package/web-dist/assets/index-Cf6ZUCfK.js.map +0 -1
- package/web-dist/assets/logic-D_Eq9DMW.js +0 -2
- package/web-dist/assets/logic-D_Eq9DMW.js.map +0 -1
- package/web-dist/assets/now-D6lDofYy.js +0 -2
- package/web-dist/assets/now-D6lDofYy.js.map +0 -1
- package/web-dist/assets/settings-Csal1F-W.js +0 -8
- package/web-dist/assets/settings-Csal1F-W.js.map +0 -1
- package/web-dist/assets/sliders-horizontal-S3tmwxsP.js +0 -2
- package/web-dist/assets/sliders-horizontal-S3tmwxsP.js.map +0 -1
- package/web-dist/assets/states-DrTQuYXw.js +0 -2
- package/web-dist/assets/states-DrTQuYXw.js.map +0 -1
- package/web-dist/assets/useNow-Bc_NlDY3.js +0 -2
package/README.md
CHANGED
|
@@ -111,15 +111,15 @@ npm install -g @spencerboucher/switchyard
|
|
|
111
111
|
switchyard up
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
`up` runs the coordinator and a host in one process at `http://localhost:7400`,
|
|
114
|
+
`up` runs the coordinator and a host in one process at `http://localhost:7400`, prints and opens a single-use sign-in link in your browser on first start. Add a project in Settings › Projects (a path to a git repository), then start a session or create a task.
|
|
115
115
|
|
|
116
116
|
**Run it as a server** with the container image (it runs only the coordinator):
|
|
117
117
|
|
|
118
118
|
```sh
|
|
119
119
|
docker run -d --name switchyard -p 127.0.0.1:7400:7400 -v switchyard-data:/data \
|
|
120
120
|
-e SWITCHYARD_PUBLIC_URL=https://switchyard.example.com \
|
|
121
|
-
ghcr.io/justmytwospence/switchyard:0.
|
|
122
|
-
|
|
121
|
+
ghcr.io/justmytwospence/switchyard:0.8.0
|
|
122
|
+
# Single sign-on is required off localhost: see docs/auth.md
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
Put HTTPS in front of it (a reverse proxy, or a VPN or tailnet; the installed phone app, notifications and passkeys need HTTPS). Then, on each machine that should run agents, create a host token in Settings › Hosts › Add host and run:
|
|
@@ -136,8 +136,8 @@ Settings › Hosts › Add host shows these three commands with your token fille
|
|
|
136
136
|
|
|
137
137
|
## Security model
|
|
138
138
|
|
|
139
|
-
- **A server is owned before it serves anything.**
|
|
140
|
-
- **Only you add devices.** A
|
|
139
|
+
- **A server is owned before it serves anything.** Off localhost, the owner is declared in an OpenID Connect configuration (required). There is never an "unclaimed" web page.
|
|
140
|
+
- **Only you add devices.** A browser gets in by single sign-on that matches the configured owner or by a passkey; a terminal is approved from a browser that is already signed in. Each device has its own credential that you can revoke; signing out of an identity provider never breaks an installed app.
|
|
141
141
|
- **Hosts dial out** with their own revocable credentials; the coordinator holds no provider credentials and needs no access to your home directory or Docker socket.
|
|
142
142
|
- **Agents run with the host user's privileges.** Running the coordinator in a container does not sandbox the agents; run hosts as a user whose access you are comfortable giving an agent.
|
|
143
143
|
|
|
@@ -155,7 +155,7 @@ Switchyard is young and moving quickly. It is tested against Pi `>=1.4.0 <1.6.0`
|
|
|
155
155
|
| Guide | Covers |
|
|
156
156
|
|---|---|
|
|
157
157
|
| [Deployment](docs/deployment.md) | Single machine, server, reverse proxies, VPNs, hosts, environment reference, privilege model |
|
|
158
|
-
| [Sign-in and devices](docs/auth.md) | The owner model,
|
|
158
|
+
| [Sign-in and devices](docs/auth.md) | The owner model, single sign-on, passkeys, terminal approval, single sign-on, host enrollment, revocation |
|
|
159
159
|
| [CLI](docs/cli.md) | Every `switchyard` command and credentials |
|
|
160
160
|
| [Terminal sessions](docs/terminal-sessions.md) | Pi's own TUI for every session, from any terminal, and the herdr integration |
|
|
161
161
|
| [Operations](docs/operations.md) | Backup, restore, migrations, retention, logs, health, diagnostics |
|
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
* Dialogs: every extension's `ctx.ui` is the same object, so replacing `select`, `confirm`,
|
|
12
12
|
* `input` and `editor` on it routes all dialogs through the race below. When that object cannot be
|
|
13
13
|
* patched, `remoteDialogs` is false and the host tells remote clients to answer in the terminal.
|
|
14
|
+
*
|
|
15
|
+
* Other extensions that draw a custom terminal dialog can also ask the same question as a
|
|
16
|
+
* web-only dialog through `switchyard.remote-ui.v1` on `pi.events`; the first answer wins.
|
|
14
17
|
*/
|
|
15
18
|
import { randomUUID } from "node:crypto";
|
|
16
19
|
import { createConnection } from "node:net";
|
|
17
20
|
import { modelInfo } from "./pi-model.js";
|
|
18
21
|
const PATCHED = Symbol.for("switchyard.terminal");
|
|
19
22
|
const WRAPPED = Symbol.for("switchyard.terminal.wrapped");
|
|
23
|
+
const REMOTE_UI_CHANNEL = "switchyard.remote-ui.v1";
|
|
20
24
|
function messageText(message) {
|
|
21
25
|
const content = message?.content;
|
|
22
26
|
if (typeof content === "string")
|
|
@@ -76,12 +80,36 @@ export default function switchyardTerminal(pi) {
|
|
|
76
80
|
if (!socketPath)
|
|
77
81
|
return;
|
|
78
82
|
let ctx = null;
|
|
83
|
+
let completer = null;
|
|
84
|
+
// Work that needs a command context (reload, plugin web views) runs inside a hidden command.
|
|
85
|
+
const internal = new Map();
|
|
86
|
+
const runInternal = (fn) => {
|
|
87
|
+
const token = randomUUID();
|
|
88
|
+
internal.set(token, fn);
|
|
89
|
+
// Without expandPromptTemplates Pi sends the text to the model instead of running the command.
|
|
90
|
+
pi.sendUserMessage(`/switchyard-internal ${token}`, { expandPromptTemplates: true });
|
|
91
|
+
};
|
|
92
|
+
try {
|
|
93
|
+
pi.registerCommand?.("switchyard-internal", {
|
|
94
|
+
description: "Used by Switchyard's web app",
|
|
95
|
+
handler: async (args, c) => {
|
|
96
|
+
const key = String(args ?? "").trim();
|
|
97
|
+
const run = internal.get(key);
|
|
98
|
+
internal.delete(key);
|
|
99
|
+
await run?.(c);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// an older Pi: no web views or reload
|
|
105
|
+
}
|
|
79
106
|
let remoteDialogs = false;
|
|
80
107
|
let entryCount = 0;
|
|
81
108
|
const pending = new Map();
|
|
82
109
|
const early = [];
|
|
83
110
|
const queue = { steering: [], followUp: [] };
|
|
84
111
|
let socket = null;
|
|
112
|
+
let remoteBacked = 0;
|
|
85
113
|
const send = (obj) => {
|
|
86
114
|
try {
|
|
87
115
|
if (!socket || socket.destroyed)
|
|
@@ -219,6 +247,44 @@ export default function switchyardTerminal(pi) {
|
|
|
219
247
|
});
|
|
220
248
|
});
|
|
221
249
|
};
|
|
250
|
+
/** A dialog shown only to remote clients (the caller shows its own in the terminal). */
|
|
251
|
+
const remoteOnly = (method, title, fields, signal) => {
|
|
252
|
+
const dflt = method === "confirm" ? false : undefined;
|
|
253
|
+
if (signal?.aborted)
|
|
254
|
+
return Promise.resolve(dflt);
|
|
255
|
+
const id = randomUUID();
|
|
256
|
+
const request = { type: "extension_ui_request", id, method, title, ...fields };
|
|
257
|
+
return new Promise((resolve) => {
|
|
258
|
+
let done = false;
|
|
259
|
+
const onAbort = () => {
|
|
260
|
+
send({
|
|
261
|
+
type: "extension_ui_closed",
|
|
262
|
+
id,
|
|
263
|
+
by: signal?.reason === "pi" ? "pi" : "terminal",
|
|
264
|
+
});
|
|
265
|
+
finish(dflt);
|
|
266
|
+
};
|
|
267
|
+
const finish = (value) => {
|
|
268
|
+
if (done)
|
|
269
|
+
return;
|
|
270
|
+
done = true;
|
|
271
|
+
remoteBacked--;
|
|
272
|
+
pending.delete(id);
|
|
273
|
+
signal?.removeEventListener("abort", onAbort);
|
|
274
|
+
resolve(value);
|
|
275
|
+
};
|
|
276
|
+
remoteBacked++;
|
|
277
|
+
pending.set(id, { request, onRemote: (body) => finish(parseBody(method, body)) });
|
|
278
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
279
|
+
send(request);
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
const remoteUi = {
|
|
283
|
+
select: (title, options, opts) => remoteOnly("select", title, { options }, opts?.signal),
|
|
284
|
+
confirm: (title, message, opts) => remoteOnly("confirm", title, { message }, opts?.signal),
|
|
285
|
+
input: (title, placeholder, opts) => remoteOnly("input", title, placeholder !== undefined ? { placeholder } : {}, opts?.signal),
|
|
286
|
+
editor: (title, prefill, opts) => remoteOnly("editor", title, prefill !== undefined ? { prefill } : {}, opts?.signal),
|
|
287
|
+
};
|
|
222
288
|
const fire = (method, fields) => send({ type: "extension_ui_request", id: randomUUID(), method, ...fields });
|
|
223
289
|
const patchUi = (c) => {
|
|
224
290
|
try {
|
|
@@ -334,6 +400,33 @@ export default function switchyardTerminal(pi) {
|
|
|
334
400
|
const name = message.slice(1).split(/\s/)[0];
|
|
335
401
|
const known = (pi.getCommands?.() ?? []).find((c) => c.name === name);
|
|
336
402
|
if (known?.source === "extension") {
|
|
403
|
+
const args = message.slice(1 + name.length).trim();
|
|
404
|
+
const probe = { version: 1, name, args, probe: true };
|
|
405
|
+
try {
|
|
406
|
+
pi.events?.emit?.("switchyard.web-command.v1", probe);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// no web view
|
|
410
|
+
}
|
|
411
|
+
if (probe.accept === true) {
|
|
412
|
+
runInternal(async (c) => {
|
|
413
|
+
const req = { version: 1, name, args, probe: false, ctx: c };
|
|
414
|
+
try {
|
|
415
|
+
pi.events.emit("switchyard.web-command.v1", req);
|
|
416
|
+
const view = await Promise.race([
|
|
417
|
+
Promise.resolve(req.view),
|
|
418
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("timed out")), 15000)),
|
|
419
|
+
]);
|
|
420
|
+
if (!view)
|
|
421
|
+
throw new Error(`/${name} returned no view`);
|
|
422
|
+
respond(id, type, { disposition: "view", view });
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
respond(id, type, undefined, `/${name} failed: ${e?.message ?? e}`);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
337
430
|
pi.sendUserMessage(message, { expandPromptTemplates: true });
|
|
338
431
|
respond(id, type, { disposition: "handled" });
|
|
339
432
|
return;
|
|
@@ -403,10 +496,13 @@ export default function switchyardTerminal(pi) {
|
|
|
403
496
|
return;
|
|
404
497
|
case "get_commands":
|
|
405
498
|
respond(id, type, {
|
|
406
|
-
commands: (pi.getCommands?.() ?? [])
|
|
499
|
+
commands: (pi.getCommands?.() ?? [])
|
|
500
|
+
.filter((c) => c.name !== "switchyard-internal")
|
|
501
|
+
.map((c) => ({
|
|
407
502
|
name: c.name,
|
|
408
503
|
description: c.description ?? null,
|
|
409
504
|
source: c.source,
|
|
505
|
+
sourceInfo: c.sourceInfo ?? null,
|
|
410
506
|
})),
|
|
411
507
|
});
|
|
412
508
|
return;
|
|
@@ -455,6 +551,38 @@ export default function switchyardTerminal(pi) {
|
|
|
455
551
|
}
|
|
456
552
|
});
|
|
457
553
|
return;
|
|
554
|
+
case "reload":
|
|
555
|
+
if (!ctx) {
|
|
556
|
+
respond(id, type, undefined, "Session is not ready");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
respond(id, type);
|
|
560
|
+
runInternal((c) => c.reload());
|
|
561
|
+
return;
|
|
562
|
+
case "complete": {
|
|
563
|
+
const text = String(cmd.text ?? "");
|
|
564
|
+
if (!completer?.getSuggestions || !/^\/\S+ /.test(text) || text.includes("\n")) {
|
|
565
|
+
respond(id, type, { items: [], prefix: "" });
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const ac = new AbortController();
|
|
569
|
+
const timer = setTimeout(() => ac.abort(), 2000);
|
|
570
|
+
Promise.resolve(completer.getSuggestions([text], 0, text.length, { signal: ac.signal }))
|
|
571
|
+
.then((s) => respond(id, type, {
|
|
572
|
+
items: (Array.isArray(s?.items) ? s.items : [])
|
|
573
|
+
.filter((i) => typeof i?.value === "string")
|
|
574
|
+
.slice(0, 50)
|
|
575
|
+
.map((i) => ({
|
|
576
|
+
value: i.value,
|
|
577
|
+
label: typeof i.label === "string" ? i.label : i.value,
|
|
578
|
+
description: typeof i.description === "string" ? i.description : null,
|
|
579
|
+
})),
|
|
580
|
+
prefix: typeof s?.prefix === "string" ? s.prefix : "",
|
|
581
|
+
}))
|
|
582
|
+
.catch(() => respond(id, type, { items: [], prefix: "" }))
|
|
583
|
+
.finally(() => clearTimeout(timer));
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
458
586
|
default:
|
|
459
587
|
respond(id, type, undefined, `Unknown command: ${type}`);
|
|
460
588
|
}
|
|
@@ -515,6 +643,18 @@ export default function switchyardTerminal(pi) {
|
|
|
515
643
|
catch {
|
|
516
644
|
socket = null;
|
|
517
645
|
}
|
|
646
|
+
try {
|
|
647
|
+
pi.events?.on?.(REMOTE_UI_CHANNEL, (data) => {
|
|
648
|
+
const r = data;
|
|
649
|
+
if (r?.version !== 1 || r.ui || !socket)
|
|
650
|
+
return;
|
|
651
|
+
r.ui = remoteUi;
|
|
652
|
+
r.provider = "switchyard";
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// optional service
|
|
657
|
+
}
|
|
518
658
|
// ---- events ----
|
|
519
659
|
const on = (name, fn) => {
|
|
520
660
|
try {
|
|
@@ -534,6 +674,16 @@ export default function switchyardTerminal(pi) {
|
|
|
534
674
|
on("session_start", (event, c) => {
|
|
535
675
|
ctx = c;
|
|
536
676
|
patchUi(c);
|
|
677
|
+
completer = null;
|
|
678
|
+
try {
|
|
679
|
+
c.ui?.addAutocompleteProvider?.((current) => {
|
|
680
|
+
completer = current;
|
|
681
|
+
return current;
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
// RPC or an older Pi: no completions
|
|
686
|
+
}
|
|
537
687
|
entryCount = c.sessionManager.getEntries().length;
|
|
538
688
|
const sessionId = c.sessionManager.getSessionId();
|
|
539
689
|
const sessionFile = c.sessionManager.getSessionFile() ?? null;
|
|
@@ -596,7 +746,13 @@ export default function switchyardTerminal(pi) {
|
|
|
596
746
|
on("model_select", () => sendModelState());
|
|
597
747
|
on("thinking_level_select", () => sendModelState());
|
|
598
748
|
on("session_before_compact", (event) => send({ type: "compaction_start", reason: event?.reason ?? "manual" }));
|
|
599
|
-
on("ui_prompt_start", (event) => send({
|
|
749
|
+
on("ui_prompt_start", (event) => send({
|
|
750
|
+
type: "ui_prompt",
|
|
751
|
+
event: event.type,
|
|
752
|
+
kind: event.kind,
|
|
753
|
+
title: event.title ?? null,
|
|
754
|
+
remoteBacked: event.kind === "custom" && remoteBacked > 0,
|
|
755
|
+
}));
|
|
600
756
|
on("ui_prompt_end", (event) => send({ type: "ui_prompt", event: event.type, kind: event.kind, title: event.title ?? null }));
|
|
601
757
|
on("input", (event) => {
|
|
602
758
|
if (event?.source !== "interactive" || !event.streamingBehavior)
|