@spencerboucher/switchyard 0.5.0 → 0.7.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 +9 -7
- package/dist/bridge/pi-model.js +44 -0
- package/dist/bridge/switchyard-bridge.js +24 -2
- package/dist/bridge/switchyard-terminal.js +146 -4
- package/dist/switchyard.mjs +1301 -700
- package/package.json +1 -1
- package/web-dist/assets/AutomationView-BIW_pO0M.js +2 -0
- package/web-dist/assets/AutomationView-BIW_pO0M.js.map +1 -0
- package/web-dist/assets/Badge-BiDp7NKI.js +2 -0
- package/web-dist/assets/Badge-BiDp7NKI.js.map +1 -0
- package/web-dist/assets/InteractionCard-CWxyDv81.js +6 -0
- package/web-dist/assets/InteractionCard-CWxyDv81.js.map +1 -0
- package/web-dist/assets/{Menu-C4GUY_ym.js → Menu-BDt1bxwz.js} +2 -2
- package/web-dist/assets/Menu-BDt1bxwz.js.map +1 -0
- package/web-dist/assets/{SegmentedControl-ChdVilXb.js → SegmentedControl-D-2BCZzd.js} +2 -2
- package/web-dist/assets/{SegmentedControl-ChdVilXb.js.map → SegmentedControl-D-2BCZzd.js.map} +1 -1
- package/web-dist/assets/Tabs-ByiPklAw.js +184 -0
- package/web-dist/assets/Tabs-ByiPklAw.js.map +1 -0
- package/web-dist/assets/TextArea-BYr88SnV.js +2 -0
- package/web-dist/assets/TextArea-BYr88SnV.js.map +1 -0
- package/web-dist/assets/attention-Bya8XHKI.js +2 -0
- package/web-dist/assets/attention-Bya8XHKI.js.map +1 -0
- package/web-dist/assets/automations-DiXKRcnE.js +2 -0
- package/web-dist/assets/automations-DiXKRcnE.js.map +1 -0
- package/web-dist/assets/board-BrLaJwLJ.js +10 -0
- package/web-dist/assets/board-BrLaJwLJ.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-DKLO8bzz.js +11 -0
- package/web-dist/assets/code-DKLO8bzz.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-DnvH6_fl.js +5 -0
- package/web-dist/assets/describe-DnvH6_fl.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-BE_V9dwJ.js +56 -0
- package/web-dist/assets/dist-BE_V9dwJ.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-DUDpbJoB.js +2 -0
- package/web-dist/assets/enroll-DUDpbJoB.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-DvRcMGEe.css +2 -0
- package/web-dist/assets/index-vIzH1m8u.js +11 -0
- package/web-dist/assets/index-vIzH1m8u.js.map +1 -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-xb-QF00E.js +2 -0
- package/web-dist/assets/logic-xb-QF00E.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-syTnSpW3.js → more-BP5x45cs.js} +2 -2
- package/web-dist/assets/{more-syTnSpW3.js.map → more-BP5x45cs.js.map} +1 -1
- package/web-dist/assets/now--XjJC37b.js +2 -0
- package/web-dist/assets/now--XjJC37b.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-D8T1esWq.js +7 -0
- package/web-dist/assets/settings-D8T1esWq.js.map +1 -0
- package/web-dist/assets/shared-CZ7fcu_B.js +3 -0
- package/web-dist/assets/shared-CZ7fcu_B.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--geW2Ntl.js +2 -0
- package/web-dist/assets/states--geW2Ntl.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-BJxV78au.js +2 -0
- package/web-dist/assets/terminal-login-BJxV78au.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-jxdoOC_1.js +2 -0
- package/web-dist/assets/{useNow-BQpDYijR.js.map → useNow-jxdoOC_1.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-CFohp0SQ.js +0 -2
- package/web-dist/assets/AutomationView-CFohp0SQ.js.map +0 -1
- package/web-dist/assets/InteractionCard-B-xmpTNG.js +0 -4
- package/web-dist/assets/InteractionCard-B-xmpTNG.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/Menu-C4GUY_ym.js.map +0 -1
- package/web-dist/assets/Overlay--v0ur2lj.js +0 -2
- package/web-dist/assets/Overlay--v0ur2lj.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-q6_xlnT4.js +0 -2
- package/web-dist/assets/attention-q6_xlnT4.js.map +0 -1
- package/web-dist/assets/automations-CyA83OAD.js +0 -2
- package/web-dist/assets/automations-CyA83OAD.js.map +0 -1
- package/web-dist/assets/board-BFvNcFhk.js +0 -10
- package/web-dist/assets/board-BFvNcFhk.js.map +0 -1
- package/web-dist/assets/code-DavnmlQN.js +0 -8
- package/web-dist/assets/code-DavnmlQN.js.map +0 -1
- package/web-dist/assets/describe-BirsjlUl.js +0 -5
- package/web-dist/assets/describe-BirsjlUl.js.map +0 -1
- package/web-dist/assets/dist-C-h8FC-n.js +0 -56
- package/web-dist/assets/dist-C-h8FC-n.js.map +0 -1
- package/web-dist/assets/enroll-C7UBC20s.js +0 -2
- package/web-dist/assets/enroll-C7UBC20s.js.map +0 -1
- package/web-dist/assets/index-ByiuDoG0.css +0 -2
- package/web-dist/assets/index-IdzOP9GE.js +0 -11
- package/web-dist/assets/index-IdzOP9GE.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-BqsLS-tK.js +0 -2
- package/web-dist/assets/now-BqsLS-tK.js.map +0 -1
- package/web-dist/assets/settings-DRj7UPtP.js +0 -8
- package/web-dist/assets/settings-DRj7UPtP.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-BQpDYijR.js +0 -2
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ sequenceDiagram
|
|
|
76
76
|
|
|
77
77
|
| Area | What you get |
|
|
78
78
|
|---|---|
|
|
79
|
-
| **Sessions** | Persistent Pi conversations on any host; the same live session in every client; steer or queue follow-ups; take over and return to automation; resume after crashes with the same conversation (never a silent new one); import existing Pi sessions |
|
|
79
|
+
| **Sessions** | Persistent Pi conversations on any host; the same live session in every client; steer or queue follow-ups; take over and return to automation; resume after crashes with the same conversation (never a silent new one); import existing Pi sessions; switch model and effort, see context use and compact, a slash-command picker |
|
|
80
80
|
| **Workspaces** | A git worktree per task or session inside the repository, discovery of worktrees you made yourself, setup commands, removal that refuses to lose uncommitted or unmerged work, and project folders per host whose repositories are added automatically |
|
|
81
81
|
| **Tasks and board** | Backlog, Ready, In progress, Review, Done; dependencies with cycle detection; priorities; per-project concurrency limits; a Now view explaining why queued work waits |
|
|
82
82
|
| **Review** | Changed files and diffs on desktop and phone, line comments sent as one follow-up, request changes, accept |
|
|
@@ -100,6 +100,8 @@ sequenceDiagram
|
|
|
100
100
|
| **Device** | A signed-in client: a browser, an installed web app or a terminal |
|
|
101
101
|
| **Adapter** | Code that renders a specific Pi plugin's questions and state as structured UI |
|
|
102
102
|
|
|
103
|
+
A seeded **Vanilla** profile runs stock Pi (no extensions, skills or prompt templates).
|
|
104
|
+
|
|
103
105
|
## Quick start
|
|
104
106
|
|
|
105
107
|
**Try it on one machine** (Node 24 or newer, git, and Pi 1.4 or 1.5 on your PATH):
|
|
@@ -109,15 +111,15 @@ npm install -g @spencerboucher/switchyard
|
|
|
109
111
|
switchyard up
|
|
110
112
|
```
|
|
111
113
|
|
|
112
|
-
`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.
|
|
113
115
|
|
|
114
116
|
**Run it as a server** with the container image (it runs only the coordinator):
|
|
115
117
|
|
|
116
118
|
```sh
|
|
117
119
|
docker run -d --name switchyard -p 127.0.0.1:7400:7400 -v switchyard-data:/data \
|
|
118
120
|
-e SWITCHYARD_PUBLIC_URL=https://switchyard.example.com \
|
|
119
|
-
ghcr.io/justmytwospence/switchyard:0.
|
|
120
|
-
|
|
121
|
+
ghcr.io/justmytwospence/switchyard:0.7.0
|
|
122
|
+
# Single sign-on is required off localhost: see docs/auth.md
|
|
121
123
|
```
|
|
122
124
|
|
|
123
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:
|
|
@@ -134,8 +136,8 @@ Settings › Hosts › Add host shows these three commands with your token fille
|
|
|
134
136
|
|
|
135
137
|
## Security model
|
|
136
138
|
|
|
137
|
-
- **A server is owned before it serves anything.**
|
|
138
|
-
- **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.
|
|
139
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.
|
|
140
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.
|
|
141
143
|
|
|
@@ -153,7 +155,7 @@ Switchyard is young and moving quickly. It is tested against Pi `>=1.4.0 <1.6.0`
|
|
|
153
155
|
| Guide | Covers |
|
|
154
156
|
|---|---|
|
|
155
157
|
| [Deployment](docs/deployment.md) | Single machine, server, reverse proxies, VPNs, hosts, environment reference, privilege model |
|
|
156
|
-
| [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 |
|
|
157
159
|
| [CLI](docs/cli.md) | Every `switchyard` command and credentials |
|
|
158
160
|
| [Terminal sessions](docs/terminal-sessions.md) | Pi's own TUI for every session, from any terminal, and the herdr integration |
|
|
159
161
|
| [Operations](docs/operations.md) | Backup, restore, migrations, retention, logs, health, diagnostics |
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model and thinking-level helpers shared by the Switchyard Pi extensions and `ManagedSession`.
|
|
3
|
+
* Imports nothing, so it can be loaded by Pi next to the extensions.
|
|
4
|
+
*/
|
|
5
|
+
/** Pi 1.4's thinking levels (pi-ai EXTENDED_THINKING_LEVELS), in order. */
|
|
6
|
+
export const PI_THINKING_LEVELS = [
|
|
7
|
+
"off",
|
|
8
|
+
"minimal",
|
|
9
|
+
"low",
|
|
10
|
+
"medium",
|
|
11
|
+
"high",
|
|
12
|
+
"xhigh",
|
|
13
|
+
"max",
|
|
14
|
+
];
|
|
15
|
+
/** Pi 1.4 `getSupportedThinkingLevels` (pi-ai dist/models.js:693); passes through an already-mapped `thinkingLevels`. */
|
|
16
|
+
export function supportedThinkingLevels(model) {
|
|
17
|
+
const known = PI_THINKING_LEVELS;
|
|
18
|
+
if (Array.isArray(model?.thinkingLevels)) {
|
|
19
|
+
return model.thinkingLevels.filter((l) => typeof l === "string" && known.includes(l));
|
|
20
|
+
}
|
|
21
|
+
if (!model?.reasoning)
|
|
22
|
+
return ["off"];
|
|
23
|
+
return PI_THINKING_LEVELS.filter((level) => {
|
|
24
|
+
const mapped = model.thinkingLevelMap?.[level];
|
|
25
|
+
if (mapped === null)
|
|
26
|
+
return false;
|
|
27
|
+
if (level === "xhigh" || level === "max")
|
|
28
|
+
return mapped !== undefined;
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function modelInfo(model) {
|
|
33
|
+
if (!model || typeof model.provider !== "string" || typeof model.id !== "string")
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
provider: model.provider,
|
|
37
|
+
id: model.id,
|
|
38
|
+
name: typeof model.name === "string" && model.name ? model.name : null,
|
|
39
|
+
contextWindow: typeof model.contextWindow === "number" ? model.contextWindow : null,
|
|
40
|
+
reasoning: model.reasoning === true,
|
|
41
|
+
thinkingLevels: supportedThinkingLevels(model),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=pi-model.js.map
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* never throws: any socket error disables forwarding for the rest of the process.
|
|
9
9
|
*/
|
|
10
10
|
import { createConnection } from "node:net";
|
|
11
|
+
import { modelInfo } from "./pi-model.js";
|
|
11
12
|
export default function switchyardBridge(pi) {
|
|
12
13
|
const socketPath = process.env.SWITCHYARD_BRIDGE_SOCKET;
|
|
13
14
|
if (!socketPath)
|
|
@@ -46,8 +47,9 @@ export default function switchyardBridge(pi) {
|
|
|
46
47
|
return null;
|
|
47
48
|
}
|
|
48
49
|
};
|
|
49
|
-
const send = (s, msg) => {
|
|
50
|
+
const send = (s, msg, extra = {}) => {
|
|
50
51
|
const full = {
|
|
52
|
+
...extra,
|
|
51
53
|
type: msg.type,
|
|
52
54
|
...(msg.kind !== undefined ? { kind: msg.kind } : {}),
|
|
53
55
|
title: msg.title ?? null,
|
|
@@ -77,15 +79,35 @@ export default function switchyardBridge(pi) {
|
|
|
77
79
|
disabled = true;
|
|
78
80
|
}
|
|
79
81
|
};
|
|
82
|
+
const sendModelState = (ctx) => {
|
|
83
|
+
try {
|
|
84
|
+
const s = ensureSocket();
|
|
85
|
+
if (!s)
|
|
86
|
+
return;
|
|
87
|
+
send(s, { type: "model_state" }, {
|
|
88
|
+
model: modelInfo(ctx?.model),
|
|
89
|
+
thinkingLevel: ctx?.thinkingLevel ?? null,
|
|
90
|
+
scoped: (ctx?.scopedModels ?? [])
|
|
91
|
+
.map((s) => s?.model?.provider && s?.model?.id ? `${s.model.provider}/${s.model.id}` : null)
|
|
92
|
+
.filter(Boolean),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
disabled = true;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
80
99
|
try {
|
|
81
|
-
pi.on("session_start", () => {
|
|
100
|
+
pi.on("session_start", (_event, ctx) => {
|
|
82
101
|
try {
|
|
83
102
|
ensureSocket();
|
|
84
103
|
}
|
|
85
104
|
catch {
|
|
86
105
|
disabled = true;
|
|
87
106
|
}
|
|
107
|
+
sendModelState(ctx);
|
|
88
108
|
});
|
|
109
|
+
pi.on("model_select", (_event, ctx) => sendModelState(ctx));
|
|
110
|
+
pi.on("thinking_level_select", (_event, ctx) => sendModelState(ctx));
|
|
89
111
|
pi.on("ui_prompt_start", forward);
|
|
90
112
|
pi.on("ui_prompt_end", forward);
|
|
91
113
|
pi.on("session_shutdown", () => {
|
|
@@ -11,11 +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";
|
|
20
|
+
import { modelInfo } from "./pi-model.js";
|
|
17
21
|
const PATCHED = Symbol.for("switchyard.terminal");
|
|
18
22
|
const WRAPPED = Symbol.for("switchyard.terminal.wrapped");
|
|
23
|
+
const REMOTE_UI_CHANNEL = "switchyard.remote-ui.v1";
|
|
19
24
|
function messageText(message) {
|
|
20
25
|
const content = message?.content;
|
|
21
26
|
if (typeof content === "string")
|
|
@@ -81,6 +86,7 @@ export default function switchyardTerminal(pi) {
|
|
|
81
86
|
const early = [];
|
|
82
87
|
const queue = { steering: [], followUp: [] };
|
|
83
88
|
let socket = null;
|
|
89
|
+
let remoteBacked = 0;
|
|
84
90
|
const send = (obj) => {
|
|
85
91
|
try {
|
|
86
92
|
if (!socket || socket.destroyed)
|
|
@@ -91,6 +97,14 @@ export default function switchyardTerminal(pi) {
|
|
|
91
97
|
// never throw into Pi
|
|
92
98
|
}
|
|
93
99
|
};
|
|
100
|
+
const sendModelState = () => send({
|
|
101
|
+
type: "model_state",
|
|
102
|
+
model: modelInfo(ctx?.model),
|
|
103
|
+
thinkingLevel: ctx?.thinkingLevel ?? pi.getThinkingLevel?.() ?? null,
|
|
104
|
+
scoped: (ctx?.scopedModels ?? [])
|
|
105
|
+
.map((s) => s?.model?.provider && s?.model?.id ? `${s.model.provider}/${s.model.id}` : null)
|
|
106
|
+
.filter(Boolean),
|
|
107
|
+
});
|
|
94
108
|
const sendQueue = () => send({ type: "queue_update", ...queue });
|
|
95
109
|
const flushEntries = () => {
|
|
96
110
|
try {
|
|
@@ -210,6 +224,44 @@ export default function switchyardTerminal(pi) {
|
|
|
210
224
|
});
|
|
211
225
|
});
|
|
212
226
|
};
|
|
227
|
+
/** A dialog shown only to remote clients (the caller shows its own in the terminal). */
|
|
228
|
+
const remoteOnly = (method, title, fields, signal) => {
|
|
229
|
+
const dflt = method === "confirm" ? false : undefined;
|
|
230
|
+
if (signal?.aborted)
|
|
231
|
+
return Promise.resolve(dflt);
|
|
232
|
+
const id = randomUUID();
|
|
233
|
+
const request = { type: "extension_ui_request", id, method, title, ...fields };
|
|
234
|
+
return new Promise((resolve) => {
|
|
235
|
+
let done = false;
|
|
236
|
+
const onAbort = () => {
|
|
237
|
+
send({
|
|
238
|
+
type: "extension_ui_closed",
|
|
239
|
+
id,
|
|
240
|
+
by: signal?.reason === "pi" ? "pi" : "terminal",
|
|
241
|
+
});
|
|
242
|
+
finish(dflt);
|
|
243
|
+
};
|
|
244
|
+
const finish = (value) => {
|
|
245
|
+
if (done)
|
|
246
|
+
return;
|
|
247
|
+
done = true;
|
|
248
|
+
remoteBacked--;
|
|
249
|
+
pending.delete(id);
|
|
250
|
+
signal?.removeEventListener("abort", onAbort);
|
|
251
|
+
resolve(value);
|
|
252
|
+
};
|
|
253
|
+
remoteBacked++;
|
|
254
|
+
pending.set(id, { request, onRemote: (body) => finish(parseBody(method, body)) });
|
|
255
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
256
|
+
send(request);
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
const remoteUi = {
|
|
260
|
+
select: (title, options, opts) => remoteOnly("select", title, { options }, opts?.signal),
|
|
261
|
+
confirm: (title, message, opts) => remoteOnly("confirm", title, { message }, opts?.signal),
|
|
262
|
+
input: (title, placeholder, opts) => remoteOnly("input", title, placeholder !== undefined ? { placeholder } : {}, opts?.signal),
|
|
263
|
+
editor: (title, prefill, opts) => remoteOnly("editor", title, prefill !== undefined ? { prefill } : {}, opts?.signal),
|
|
264
|
+
};
|
|
213
265
|
const fire = (method, fields) => send({ type: "extension_ui_request", id: randomUUID(), method, ...fields });
|
|
214
266
|
const patchUi = (c) => {
|
|
215
267
|
try {
|
|
@@ -314,14 +366,30 @@ export default function switchyardTerminal(pi) {
|
|
|
314
366
|
sessionFile: ctx.sessionManager.getSessionFile() ?? null,
|
|
315
367
|
sessionName: pi.getSessionName?.() ?? null,
|
|
316
368
|
isStreaming: !ctx.isIdle(),
|
|
369
|
+
model: modelInfo(ctx.model),
|
|
370
|
+
thinkingLevel: ctx.thinkingLevel ?? pi.getThinkingLevel?.() ?? null,
|
|
317
371
|
pendingMessageCount: ctx.hasPendingMessages() ? 1 : 0,
|
|
318
372
|
});
|
|
319
373
|
return;
|
|
320
374
|
case "prompt": {
|
|
321
375
|
const message = String(cmd.message ?? "");
|
|
322
376
|
if (message.startsWith("/")) {
|
|
323
|
-
|
|
324
|
-
|
|
377
|
+
const name = message.slice(1).split(/\s/)[0];
|
|
378
|
+
const known = (pi.getCommands?.() ?? []).find((c) => c.name === name);
|
|
379
|
+
if (known?.source === "extension") {
|
|
380
|
+
pi.sendUserMessage(message, { expandPromptTemplates: true });
|
|
381
|
+
respond(id, type, { disposition: "handled" });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (known) {
|
|
385
|
+
if (!ctx.isIdle()) {
|
|
386
|
+
respond(id, type, undefined, "Session is running: choose Steer or Queue follow-up");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
pi.sendUserMessage(content(message, cmd.images), { expandPromptTemplates: true });
|
|
390
|
+
respond(id, type, { disposition: "started" });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
325
393
|
}
|
|
326
394
|
if (!ctx.isIdle()) {
|
|
327
395
|
respond(id, type, undefined, "Session is running: choose Steer or Queue follow-up");
|
|
@@ -364,9 +432,61 @@ export default function switchyardTerminal(pi) {
|
|
|
364
432
|
}
|
|
365
433
|
case "get_session_stats": {
|
|
366
434
|
const totals = sessionTotals(ctx.sessionManager.getEntries());
|
|
367
|
-
respond(id, type, {
|
|
435
|
+
respond(id, type, {
|
|
436
|
+
tokens: { total: totals.tokens },
|
|
437
|
+
cost: totals.cost,
|
|
438
|
+
contextUsage: ctx.getContextUsage?.() ?? undefined,
|
|
439
|
+
});
|
|
368
440
|
return;
|
|
369
441
|
}
|
|
442
|
+
case "get_available_models":
|
|
443
|
+
respond(id, type, {
|
|
444
|
+
models: (ctx.modelRegistry?.getAvailable?.() ?? []).map(modelInfo).filter(Boolean),
|
|
445
|
+
});
|
|
446
|
+
return;
|
|
447
|
+
case "get_commands":
|
|
448
|
+
respond(id, type, {
|
|
449
|
+
commands: (pi.getCommands?.() ?? []).map((c) => ({
|
|
450
|
+
name: c.name,
|
|
451
|
+
description: c.description ?? null,
|
|
452
|
+
source: c.source,
|
|
453
|
+
})),
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
case "set_model": {
|
|
457
|
+
const provider = String(cmd.provider ?? "");
|
|
458
|
+
const modelId = String(cmd.modelId ?? "");
|
|
459
|
+
const model = ctx.modelRegistry?.find?.(provider, modelId);
|
|
460
|
+
if (!model) {
|
|
461
|
+
respond(id, type, undefined, `Model not found: ${provider}/${modelId}`);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
Promise.resolve(pi.setModel(model)).then((ok) => {
|
|
465
|
+
if (ok === false)
|
|
466
|
+
respond(id, type, undefined, `No API key for ${provider}/${modelId}`);
|
|
467
|
+
else
|
|
468
|
+
respond(id, type, modelInfo(ctx.model));
|
|
469
|
+
}, (err) => respond(id, type, undefined, err instanceof Error ? err.message : String(err)));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
case "set_thinking_level":
|
|
473
|
+
pi.setThinkingLevel(String(cmd.level));
|
|
474
|
+
respond(id, type);
|
|
475
|
+
return;
|
|
476
|
+
case "compact":
|
|
477
|
+
if (!ctx.isIdle()) {
|
|
478
|
+
respond(id, type, undefined, "Session is running: wait for it to finish before compacting");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
ctx.compact({
|
|
482
|
+
customInstructions: typeof cmd.customInstructions === "string" && cmd.customInstructions
|
|
483
|
+
? cmd.customInstructions
|
|
484
|
+
: undefined,
|
|
485
|
+
onComplete: () => { },
|
|
486
|
+
onError: () => { },
|
|
487
|
+
});
|
|
488
|
+
respond(id, type, { started: true });
|
|
489
|
+
return;
|
|
370
490
|
case "shutdown":
|
|
371
491
|
respond(id, type);
|
|
372
492
|
setImmediate(() => {
|
|
@@ -438,6 +558,18 @@ export default function switchyardTerminal(pi) {
|
|
|
438
558
|
catch {
|
|
439
559
|
socket = null;
|
|
440
560
|
}
|
|
561
|
+
try {
|
|
562
|
+
pi.events?.on?.(REMOTE_UI_CHANNEL, (data) => {
|
|
563
|
+
const r = data;
|
|
564
|
+
if (r?.version !== 1 || r.ui || !socket)
|
|
565
|
+
return;
|
|
566
|
+
r.ui = remoteUi;
|
|
567
|
+
r.provider = "switchyard";
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
// optional service
|
|
572
|
+
}
|
|
441
573
|
// ---- events ----
|
|
442
574
|
const on = (name, fn) => {
|
|
443
575
|
try {
|
|
@@ -461,6 +593,7 @@ export default function switchyardTerminal(pi) {
|
|
|
461
593
|
const sessionId = c.sessionManager.getSessionId();
|
|
462
594
|
const sessionFile = c.sessionManager.getSessionFile() ?? null;
|
|
463
595
|
send({ type: "extension_hello", pid: process.pid, remoteDialogs, sessionId, sessionFile });
|
|
596
|
+
sendModelState();
|
|
464
597
|
if (event?.reason && event.reason !== "startup") {
|
|
465
598
|
send({ type: "switchyard_native", sessionId, sessionFile });
|
|
466
599
|
}
|
|
@@ -515,7 +648,16 @@ export default function switchyardTerminal(pi) {
|
|
|
515
648
|
aborted: event?.aborted === true,
|
|
516
649
|
errorMessage: event?.errorMessage ?? "unknown error",
|
|
517
650
|
}));
|
|
518
|
-
on("
|
|
651
|
+
on("model_select", () => sendModelState());
|
|
652
|
+
on("thinking_level_select", () => sendModelState());
|
|
653
|
+
on("session_before_compact", (event) => send({ type: "compaction_start", reason: event?.reason ?? "manual" }));
|
|
654
|
+
on("ui_prompt_start", (event) => send({
|
|
655
|
+
type: "ui_prompt",
|
|
656
|
+
event: event.type,
|
|
657
|
+
kind: event.kind,
|
|
658
|
+
title: event.title ?? null,
|
|
659
|
+
remoteBacked: event.kind === "custom" && remoteBacked > 0,
|
|
660
|
+
}));
|
|
519
661
|
on("ui_prompt_end", (event) => send({ type: "ui_prompt", event: event.type, kind: event.kind, title: event.title ?? null }));
|
|
520
662
|
on("input", (event) => {
|
|
521
663
|
if (event?.source !== "interactive" || !event.streamingBehavior)
|