@spencerboucher/switchyard 0.7.0 → 0.9.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 +3 -3
- package/dist/bridge/lines.js +33 -0
- package/dist/bridge/switchyard-terminal.js +155 -23
- package/dist/switchyard.mjs +3879 -2658
- package/dist/terminal/holder.js +142 -51
- package/package.json +1 -1
- package/web-dist/apple-touch-icon.png +0 -0
- package/web-dist/assets/AutomationView-D7Syv1En.js +2 -0
- package/web-dist/assets/AutomationView-D7Syv1En.js.map +1 -0
- package/web-dist/assets/Checkbox-D6tRfEeI.js +2 -0
- package/web-dist/assets/Checkbox-D6tRfEeI.js.map +1 -0
- package/web-dist/assets/Copy-B6XSAIUd.js +2 -0
- package/web-dist/assets/Copy-B6XSAIUd.js.map +1 -0
- package/web-dist/assets/DangerZone-DfYlxDC1.js +2 -0
- package/web-dist/assets/DangerZone-DfYlxDC1.js.map +1 -0
- package/web-dist/assets/Field-D-Ty7fKT.js +2 -0
- package/web-dist/assets/Field-D-Ty7fKT.js.map +1 -0
- package/web-dist/assets/InteractionCard-DF1EcSpm.js +6 -0
- package/web-dist/assets/InteractionCard-DF1EcSpm.js.map +1 -0
- package/web-dist/assets/Menu-BfOszNC4.js +2 -0
- package/web-dist/assets/Menu-BfOszNC4.js.map +1 -0
- package/web-dist/assets/Overlay-DUQwh6_b.js +2 -0
- package/web-dist/assets/Overlay-DUQwh6_b.js.map +1 -0
- package/web-dist/assets/Priority-CHmzfoHg.js +2 -0
- package/web-dist/assets/Priority-CHmzfoHg.js.map +1 -0
- package/web-dist/assets/{SegmentedControl-D-2BCZzd.js → SegmentedControl-C_hEVQMB.js} +2 -2
- package/web-dist/assets/{SegmentedControl-D-2BCZzd.js.map → SegmentedControl-C_hEVQMB.js.map} +1 -1
- package/web-dist/assets/Select-Cfql4Nwu.js +2 -0
- package/web-dist/assets/Select-Cfql4Nwu.js.map +1 -0
- package/web-dist/assets/Switch-DBfzmqkm.js +2 -0
- package/web-dist/assets/Switch-DBfzmqkm.js.map +1 -0
- package/web-dist/assets/Tabs-BNuL3_Et.js +184 -0
- package/web-dist/assets/Tabs-BNuL3_Et.js.map +1 -0
- package/web-dist/assets/TextArea-CKybrQyn.js +2 -0
- package/web-dist/assets/TextArea-CKybrQyn.js.map +1 -0
- package/web-dist/assets/TextLink-OzzyP2-p.js +2 -0
- package/web-dist/assets/TextLink-OzzyP2-p.js.map +1 -0
- package/web-dist/assets/arrow-down-C-iLrN8I.js +2 -0
- package/web-dist/assets/arrow-down-C-iLrN8I.js.map +1 -0
- package/web-dist/assets/arrow-up-Dj-VTKx4.js +2 -0
- package/web-dist/assets/arrow-up-Dj-VTKx4.js.map +1 -0
- package/web-dist/assets/attention-V86Gye3I.js +2 -0
- package/web-dist/assets/attention-V86Gye3I.js.map +1 -0
- package/web-dist/assets/automations-Cw5Gacgm.js +2 -0
- package/web-dist/assets/automations-Cw5Gacgm.js.map +1 -0
- package/web-dist/assets/board-B8X7TYYR.js +10 -0
- package/web-dist/assets/board-B8X7TYYR.js.map +1 -0
- package/web-dist/assets/code-BKroYJmR.js +12 -0
- package/web-dist/assets/code-BKroYJmR.js.map +1 -0
- package/web-dist/assets/describe-D6KLGKWa.js +5 -0
- package/web-dist/assets/describe-D6KLGKWa.js.map +1 -0
- package/web-dist/assets/enroll-DwT7AcuS.js +2 -0
- package/web-dist/assets/enroll-DwT7AcuS.js.map +1 -0
- package/web-dist/assets/index-B3F7WcYf.css +2 -0
- package/web-dist/assets/index-hxvDvCS7.js +12 -0
- package/web-dist/assets/index-hxvDvCS7.js.map +1 -0
- package/web-dist/assets/logic-B46fwD-8.js +2 -0
- package/web-dist/assets/logic-B46fwD-8.js.map +1 -0
- package/web-dist/assets/more-DQR5uu1c.js +2 -0
- package/web-dist/assets/more-DQR5uu1c.js.map +1 -0
- package/web-dist/assets/queue-DUg1rS8t.js +2 -0
- package/web-dist/assets/queue-DUg1rS8t.js.map +1 -0
- package/web-dist/assets/repo-XzzzEI0d.js +2 -0
- package/web-dist/assets/repo-XzzzEI0d.js.map +1 -0
- package/web-dist/assets/server-BqZ-w6M8.js +2 -0
- package/web-dist/assets/server-BqZ-w6M8.js.map +1 -0
- package/web-dist/assets/settings-DyUu0tJM.js +8 -0
- package/web-dist/assets/settings-DyUu0tJM.js.map +1 -0
- package/web-dist/assets/states-ZhrxQa3K.js +56 -0
- package/web-dist/assets/states-ZhrxQa3K.js.map +1 -0
- package/web-dist/assets/terminal-BOV_MXPZ.js +2 -0
- package/web-dist/assets/terminal-BOV_MXPZ.js.map +1 -0
- package/web-dist/assets/terminal-login-S5CAd5xA.js +2 -0
- package/web-dist/assets/terminal-login-S5CAd5xA.js.map +1 -0
- package/web-dist/assets/useBackTo-BIrCq3W2.js +2 -0
- package/web-dist/assets/useBackTo-BIrCq3W2.js.map +1 -0
- package/web-dist/assets/useNow-Dj7oEiwn.js +2 -0
- package/web-dist/assets/useNow-Dj7oEiwn.js.map +1 -0
- package/web-dist/assets/useResource-CDGSq2At.js +2 -0
- package/web-dist/assets/useResource-CDGSq2At.js.map +1 -0
- package/web-dist/icon-192.png +0 -0
- package/web-dist/icon-512.png +0 -0
- package/web-dist/icon.svg +20 -8
- package/web-dist/index.html +3 -4
- package/web-dist/manifest.webmanifest +1 -1
- package/web-dist/maskable-512.png +0 -0
- package/web-dist/sw.js +1 -1
- package/web-dist/sw.js.map +1 -1
- package/web-dist/assets/AutomationView-BIW_pO0M.js +0 -2
- package/web-dist/assets/AutomationView-BIW_pO0M.js.map +0 -1
- package/web-dist/assets/Badge-BiDp7NKI.js +0 -2
- package/web-dist/assets/Badge-BiDp7NKI.js.map +0 -1
- package/web-dist/assets/InteractionCard-CWxyDv81.js +0 -6
- package/web-dist/assets/InteractionCard-CWxyDv81.js.map +0 -1
- package/web-dist/assets/Menu-BDt1bxwz.js +0 -2
- package/web-dist/assets/Menu-BDt1bxwz.js.map +0 -1
- package/web-dist/assets/Tabs-ByiPklAw.js +0 -184
- package/web-dist/assets/Tabs-ByiPklAw.js.map +0 -1
- package/web-dist/assets/TextArea-BYr88SnV.js +0 -2
- package/web-dist/assets/TextArea-BYr88SnV.js.map +0 -1
- package/web-dist/assets/arrow-left-C6YWM6y6.js +0 -2
- package/web-dist/assets/arrow-left-C6YWM6y6.js.map +0 -1
- package/web-dist/assets/attention-Bya8XHKI.js +0 -2
- package/web-dist/assets/attention-Bya8XHKI.js.map +0 -1
- package/web-dist/assets/automations-DiXKRcnE.js +0 -2
- package/web-dist/assets/automations-DiXKRcnE.js.map +0 -1
- package/web-dist/assets/board-BrLaJwLJ.js +0 -10
- package/web-dist/assets/board-BrLaJwLJ.js.map +0 -1
- package/web-dist/assets/code-DKLO8bzz.js +0 -11
- package/web-dist/assets/code-DKLO8bzz.js.map +0 -1
- package/web-dist/assets/copy-oWgGHmkS.js +0 -2
- package/web-dist/assets/copy-oWgGHmkS.js.map +0 -1
- package/web-dist/assets/describe-DnvH6_fl.js +0 -5
- package/web-dist/assets/describe-DnvH6_fl.js.map +0 -1
- package/web-dist/assets/dist-BE_V9dwJ.js +0 -56
- package/web-dist/assets/dist-BE_V9dwJ.js.map +0 -1
- package/web-dist/assets/enroll-DUDpbJoB.js +0 -2
- package/web-dist/assets/enroll-DUDpbJoB.js.map +0 -1
- package/web-dist/assets/index-DvRcMGEe.css +0 -2
- package/web-dist/assets/index-vIzH1m8u.js +0 -11
- package/web-dist/assets/index-vIzH1m8u.js.map +0 -1
- package/web-dist/assets/logic-xb-QF00E.js +0 -2
- package/web-dist/assets/logic-xb-QF00E.js.map +0 -1
- package/web-dist/assets/more-BP5x45cs.js +0 -2
- package/web-dist/assets/more-BP5x45cs.js.map +0 -1
- package/web-dist/assets/now--XjJC37b.js +0 -2
- package/web-dist/assets/now--XjJC37b.js.map +0 -1
- package/web-dist/assets/plus-BLP-wIYD.js +0 -2
- package/web-dist/assets/plus-BLP-wIYD.js.map +0 -1
- package/web-dist/assets/save-DEvZPWlS.js +0 -2
- package/web-dist/assets/save-DEvZPWlS.js.map +0 -1
- package/web-dist/assets/send-BKjswHxB.js +0 -2
- package/web-dist/assets/send-BKjswHxB.js.map +0 -1
- package/web-dist/assets/server-SxFxIScj.js +0 -2
- package/web-dist/assets/server-SxFxIScj.js.map +0 -1
- package/web-dist/assets/settings-D8T1esWq.js +0 -7
- package/web-dist/assets/settings-D8T1esWq.js.map +0 -1
- package/web-dist/assets/shared-CZ7fcu_B.js +0 -3
- package/web-dist/assets/shared-CZ7fcu_B.js.map +0 -1
- package/web-dist/assets/sliders-horizontal-DAoirila.js +0 -2
- package/web-dist/assets/sliders-horizontal-DAoirila.js.map +0 -1
- package/web-dist/assets/states--geW2Ntl.js +0 -2
- package/web-dist/assets/states--geW2Ntl.js.map +0 -1
- package/web-dist/assets/terminal-login-BJxV78au.js +0 -2
- package/web-dist/assets/terminal-login-BJxV78au.js.map +0 -1
- package/web-dist/assets/trash-C6X2OLrI.js +0 -2
- package/web-dist/assets/trash-C6X2OLrI.js.map +0 -1
- package/web-dist/assets/useNow-jxdoOC_1.js +0 -2
- package/web-dist/assets/useNow-jxdoOC_1.js.map +0 -1
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ sequenceDiagram
|
|
|
68
68
|
1. **You describe the work.** A task has an objective, acceptance criteria, dependencies and a priority. Moving it to Ready lets Switchyard start it.
|
|
69
69
|
2. **Switchyard starts it on a host.** The coordinator picks an eligible host, the host creates a git worktree for the task inside the repository (`<repo>/.worktrees/<branch>` by default), and Pi starts there with your normal extensions. A launch is recorded before it is sent, so a lost connection or a coordinator restart never starts it twice.
|
|
70
70
|
3. **The agent asks, you answer anywhere.** Questions and approvals from Pi and its plugins appear as cards in every client and as a notification on the device you are most likely to see. Answer once; every client shows the result.
|
|
71
|
-
4. **You can step in.** Join from any client to watch and send messages, steer the current turn or queue a follow-up. *Take over* pauses automation (no automatic continuations or retries) until you *return it*. *Stop* aborts the run. Detaching never stops anything.
|
|
71
|
+
4. **You can step in.** Join from any client to watch and send messages, steer the current turn or queue a follow-up. *Take over* pauses automation (no automatic continuations or retries) until you *return it*. *Stop* aborts the run. Detaching never stops anything. Interrupting a turn in Pi does not pause automation.
|
|
72
72
|
5. **It ends in Review, not in Done.** When the agent finishes, your configured checks run (tests, linters). If they pass, the task waits in Review with its diff; you can comment line by line, send all comments as one follow-up, request changes or accept. Accepting never merges or pushes for you.
|
|
73
73
|
6. **Background work uses the same path.** An automation can open a task on a schedule, from a webhook or when another task finishes, then continue it, append to it, or report only when something changed.
|
|
74
74
|
|
|
@@ -78,7 +78,7 @@ sequenceDiagram
|
|
|
78
78
|
|---|---|
|
|
79
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
|
-
| **Tasks and board** | Backlog, Ready, In progress, Review, Done; dependencies with cycle detection; priorities; per-project concurrency limits; a
|
|
81
|
+
| **Tasks and board** | Backlog, Ready, In progress, Review, Done; dependencies with cycle detection; priorities; per-project concurrency limits; a Queue 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 |
|
|
83
83
|
| **Bounded continuation** | Optional: keep the agent working toward an objective, within a maximum number of continuations, a time limit and token or cost caps, ending in your checks and your review |
|
|
84
84
|
| **Automations** | Cron (time-zone aware), one-time, webhook and dependency triggers; fresh, continue or watch modes; skip, queue or append when work overlaps; skip or catch up missed runs; a preview of the resulting task |
|
|
@@ -118,7 +118,7 @@ switchyard up
|
|
|
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.
|
|
121
|
+
ghcr.io/justmytwospence/switchyard:0.9.0
|
|
122
122
|
# Single sign-on is required off localhost: see docs/auth.md
|
|
123
123
|
```
|
|
124
124
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits text into lines on LF only (never on U+2028/U+2029, valid inside JSON strings), strips
|
|
3
|
+
* one trailing CR and skips empty lines. Imports nothing: the terminal extension loads it from
|
|
4
|
+
* next to itself, and the holder and JsonlReader share it.
|
|
5
|
+
*/
|
|
6
|
+
export class LineSplitter {
|
|
7
|
+
onLine;
|
|
8
|
+
buffer = "";
|
|
9
|
+
constructor(onLine) {
|
|
10
|
+
this.onLine = onLine;
|
|
11
|
+
}
|
|
12
|
+
push(text) {
|
|
13
|
+
const buf = this.buffer + text;
|
|
14
|
+
let start = 0;
|
|
15
|
+
for (let nl = buf.indexOf("\n"); nl >= 0; nl = buf.indexOf("\n", start)) {
|
|
16
|
+
this.emit(buf.slice(start, nl));
|
|
17
|
+
start = nl + 1;
|
|
18
|
+
}
|
|
19
|
+
this.buffer = buf.slice(start);
|
|
20
|
+
}
|
|
21
|
+
/** Return and clear the unterminated rest (CR stripped). */
|
|
22
|
+
end() {
|
|
23
|
+
const rest = this.buffer;
|
|
24
|
+
this.buffer = "";
|
|
25
|
+
return rest.endsWith("\r") ? rest.slice(0, -1) : rest;
|
|
26
|
+
}
|
|
27
|
+
emit(raw) {
|
|
28
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
29
|
+
if (line)
|
|
30
|
+
this.onLine(line);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=lines.js.map
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* dialogs while they are also shown in the terminal (whichever answer comes first wins).
|
|
7
7
|
*
|
|
8
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.
|
|
9
|
+
* built-ins and its sibling modules, and never throws: without the variable it does nothing.
|
|
10
10
|
*
|
|
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
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { randomUUID } from "node:crypto";
|
|
19
19
|
import { createConnection } from "node:net";
|
|
20
|
+
import { LineSplitter } from "./lines.js";
|
|
20
21
|
import { modelInfo } from "./pi-model.js";
|
|
21
22
|
const PATCHED = Symbol.for("switchyard.terminal");
|
|
22
23
|
const WRAPPED = Symbol.for("switchyard.terminal.wrapped");
|
|
@@ -80,6 +81,29 @@ export default function switchyardTerminal(pi) {
|
|
|
80
81
|
if (!socketPath)
|
|
81
82
|
return;
|
|
82
83
|
let ctx = null;
|
|
84
|
+
let completer = null;
|
|
85
|
+
// Work that needs a command context (reload, plugin web views) runs inside a hidden command.
|
|
86
|
+
const internal = new Map();
|
|
87
|
+
const runInternal = (fn) => {
|
|
88
|
+
const token = randomUUID();
|
|
89
|
+
internal.set(token, fn);
|
|
90
|
+
// Without expandPromptTemplates Pi sends the text to the model instead of running the command.
|
|
91
|
+
pi.sendUserMessage(`/switchyard-internal ${token}`, { expandPromptTemplates: true });
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
pi.registerCommand?.("switchyard-internal", {
|
|
95
|
+
description: "Used by Switchyard's web app",
|
|
96
|
+
handler: async (args, c) => {
|
|
97
|
+
const key = String(args ?? "").trim();
|
|
98
|
+
const run = internal.get(key);
|
|
99
|
+
internal.delete(key);
|
|
100
|
+
await run?.(c);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// an older Pi: no web views or reload
|
|
106
|
+
}
|
|
83
107
|
let remoteDialogs = false;
|
|
84
108
|
let entryCount = 0;
|
|
85
109
|
const pending = new Map();
|
|
@@ -106,6 +130,23 @@ export default function switchyardTerminal(pi) {
|
|
|
106
130
|
.filter(Boolean),
|
|
107
131
|
});
|
|
108
132
|
const sendQueue = () => send({ type: "queue_update", ...queue });
|
|
133
|
+
/** Announce this session (and whether dialogs are remote-backed) to the host. */
|
|
134
|
+
const hello = () => {
|
|
135
|
+
if (!ctx)
|
|
136
|
+
return;
|
|
137
|
+
try {
|
|
138
|
+
send({
|
|
139
|
+
type: "extension_hello",
|
|
140
|
+
pid: process.pid,
|
|
141
|
+
remoteDialogs,
|
|
142
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
143
|
+
sessionFile: ctx.sessionManager.getSessionFile() ?? null,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// never throw into Pi
|
|
148
|
+
}
|
|
149
|
+
};
|
|
109
150
|
const flushEntries = () => {
|
|
110
151
|
try {
|
|
111
152
|
const entries = ctx?.sessionManager?.getEntries?.() ?? [];
|
|
@@ -377,6 +418,33 @@ export default function switchyardTerminal(pi) {
|
|
|
377
418
|
const name = message.slice(1).split(/\s/)[0];
|
|
378
419
|
const known = (pi.getCommands?.() ?? []).find((c) => c.name === name);
|
|
379
420
|
if (known?.source === "extension") {
|
|
421
|
+
const args = message.slice(1 + name.length).trim();
|
|
422
|
+
const probe = { version: 1, name, args, probe: true };
|
|
423
|
+
try {
|
|
424
|
+
pi.events?.emit?.("switchyard.web-command.v1", probe);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// no web view
|
|
428
|
+
}
|
|
429
|
+
if (probe.accept === true) {
|
|
430
|
+
runInternal(async (c) => {
|
|
431
|
+
const req = { version: 1, name, args, probe: false, ctx: c };
|
|
432
|
+
try {
|
|
433
|
+
pi.events.emit("switchyard.web-command.v1", req);
|
|
434
|
+
const view = await Promise.race([
|
|
435
|
+
Promise.resolve(req.view),
|
|
436
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("timed out")), 15000)),
|
|
437
|
+
]);
|
|
438
|
+
if (!view)
|
|
439
|
+
throw new Error(`/${name} returned no view`);
|
|
440
|
+
respond(id, type, { disposition: "view", view });
|
|
441
|
+
}
|
|
442
|
+
catch (e) {
|
|
443
|
+
respond(id, type, undefined, `/${name} failed: ${e?.message ?? e}`);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
380
448
|
pi.sendUserMessage(message, { expandPromptTemplates: true });
|
|
381
449
|
respond(id, type, { disposition: "handled" });
|
|
382
450
|
return;
|
|
@@ -446,10 +514,13 @@ export default function switchyardTerminal(pi) {
|
|
|
446
514
|
return;
|
|
447
515
|
case "get_commands":
|
|
448
516
|
respond(id, type, {
|
|
449
|
-
commands: (pi.getCommands?.() ?? [])
|
|
517
|
+
commands: (pi.getCommands?.() ?? [])
|
|
518
|
+
.filter((c) => c.name !== "switchyard-internal")
|
|
519
|
+
.map((c) => ({
|
|
450
520
|
name: c.name,
|
|
451
521
|
description: c.description ?? null,
|
|
452
522
|
source: c.source,
|
|
523
|
+
sourceInfo: c.sourceInfo ?? null,
|
|
453
524
|
})),
|
|
454
525
|
});
|
|
455
526
|
return;
|
|
@@ -498,6 +569,38 @@ export default function switchyardTerminal(pi) {
|
|
|
498
569
|
}
|
|
499
570
|
});
|
|
500
571
|
return;
|
|
572
|
+
case "reload":
|
|
573
|
+
if (!ctx) {
|
|
574
|
+
respond(id, type, undefined, "Session is not ready");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
respond(id, type);
|
|
578
|
+
runInternal((c) => c.reload());
|
|
579
|
+
return;
|
|
580
|
+
case "complete": {
|
|
581
|
+
const text = String(cmd.text ?? "");
|
|
582
|
+
if (!completer?.getSuggestions || !/^\/\S+ /.test(text) || text.includes("\n")) {
|
|
583
|
+
respond(id, type, { items: [], prefix: "" });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const ac = new AbortController();
|
|
587
|
+
const timer = setTimeout(() => ac.abort(), 2000);
|
|
588
|
+
Promise.resolve(completer.getSuggestions([text], 0, text.length, { signal: ac.signal }))
|
|
589
|
+
.then((s) => respond(id, type, {
|
|
590
|
+
items: (Array.isArray(s?.items) ? s.items : [])
|
|
591
|
+
.filter((i) => typeof i?.value === "string")
|
|
592
|
+
.slice(0, 50)
|
|
593
|
+
.map((i) => ({
|
|
594
|
+
value: i.value,
|
|
595
|
+
label: typeof i.label === "string" ? i.label : i.value,
|
|
596
|
+
description: typeof i.description === "string" ? i.description : null,
|
|
597
|
+
})),
|
|
598
|
+
prefix: typeof s?.prefix === "string" ? s.prefix : "",
|
|
599
|
+
}))
|
|
600
|
+
.catch(() => respond(id, type, { items: [], prefix: "" }))
|
|
601
|
+
.finally(() => clearTimeout(timer));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
501
604
|
default:
|
|
502
605
|
respond(id, type, undefined, `Unknown command: ${type}`);
|
|
503
606
|
}
|
|
@@ -506,6 +609,16 @@ export default function switchyardTerminal(pi) {
|
|
|
506
609
|
respond(id, type, undefined, err instanceof Error ? err.message : String(err));
|
|
507
610
|
}
|
|
508
611
|
};
|
|
612
|
+
/** Tell a (re)attached controller who we are, then re-send the dialogs it has not answered. */
|
|
613
|
+
const announce = () => {
|
|
614
|
+
if (ctx) {
|
|
615
|
+
hello();
|
|
616
|
+
sendModelState();
|
|
617
|
+
sendQueue();
|
|
618
|
+
}
|
|
619
|
+
for (const p of pending.values())
|
|
620
|
+
send(p.request);
|
|
621
|
+
};
|
|
509
622
|
const onLine = (line) => {
|
|
510
623
|
let msg;
|
|
511
624
|
try {
|
|
@@ -521,43 +634,52 @@ export default function switchyardTerminal(pi) {
|
|
|
521
634
|
return;
|
|
522
635
|
}
|
|
523
636
|
if (msg.type === "control_attached") {
|
|
524
|
-
|
|
525
|
-
|
|
637
|
+
// A controller re-attached (host agent restart): it starts from scratch, so announce the
|
|
638
|
+
// session again before re-sending the dialogs it has not answered.
|
|
639
|
+
announce();
|
|
526
640
|
return;
|
|
527
641
|
}
|
|
528
642
|
if (typeof msg.type === "string" && msg.id !== undefined)
|
|
529
643
|
handleCommand(msg);
|
|
530
644
|
};
|
|
531
645
|
// ---- socket ----
|
|
532
|
-
|
|
533
|
-
|
|
646
|
+
// The holder listens before it starts Pi, but retry a few times anyway (a slow or restarting holder).
|
|
647
|
+
const CONNECT_ATTEMPTS = 5;
|
|
648
|
+
const CONNECT_RETRY_MS = 200;
|
|
649
|
+
const connect = (attempt) => {
|
|
650
|
+
let s;
|
|
651
|
+
try {
|
|
652
|
+
s = createConnection(socketPath);
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
socket = null;
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
534
658
|
socket = s;
|
|
535
|
-
let
|
|
659
|
+
let connected = false;
|
|
660
|
+
const lines = new LineSplitter(onLine);
|
|
536
661
|
s.setEncoding("utf8");
|
|
537
|
-
s.on("data", (chunk) =>
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
buf = buf.slice(nl + 1);
|
|
543
|
-
if (line)
|
|
544
|
-
onLine(line);
|
|
545
|
-
nl = buf.indexOf("\n");
|
|
546
|
-
}
|
|
662
|
+
s.on("data", (chunk) => lines.push(chunk));
|
|
663
|
+
s.once("connect", () => {
|
|
664
|
+
connected = true;
|
|
665
|
+
if (attempt > 1)
|
|
666
|
+
announce();
|
|
547
667
|
});
|
|
548
668
|
s.on("error", () => {
|
|
549
|
-
socket
|
|
669
|
+
if (socket === s)
|
|
670
|
+
socket = null;
|
|
550
671
|
});
|
|
551
672
|
s.on("close", () => {
|
|
552
673
|
if (socket === s)
|
|
553
674
|
socket = null;
|
|
675
|
+
if (!connected && attempt < CONNECT_ATTEMPTS) {
|
|
676
|
+
setTimeout(() => connect(attempt + 1), CONNECT_RETRY_MS).unref();
|
|
677
|
+
}
|
|
554
678
|
});
|
|
555
679
|
s.unref();
|
|
556
680
|
s.write(`${JSON.stringify({ role: "extension" })}\n`);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
socket = null;
|
|
560
|
-
}
|
|
681
|
+
};
|
|
682
|
+
connect(1);
|
|
561
683
|
try {
|
|
562
684
|
pi.events?.on?.(REMOTE_UI_CHANNEL, (data) => {
|
|
563
685
|
const r = data;
|
|
@@ -589,10 +711,20 @@ export default function switchyardTerminal(pi) {
|
|
|
589
711
|
on("session_start", (event, c) => {
|
|
590
712
|
ctx = c;
|
|
591
713
|
patchUi(c);
|
|
714
|
+
completer = null;
|
|
715
|
+
try {
|
|
716
|
+
c.ui?.addAutocompleteProvider?.((current) => {
|
|
717
|
+
completer = current;
|
|
718
|
+
return current;
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
// RPC or an older Pi: no completions
|
|
723
|
+
}
|
|
592
724
|
entryCount = c.sessionManager.getEntries().length;
|
|
593
725
|
const sessionId = c.sessionManager.getSessionId();
|
|
594
726
|
const sessionFile = c.sessionManager.getSessionFile() ?? null;
|
|
595
|
-
|
|
727
|
+
hello();
|
|
596
728
|
sendModelState();
|
|
597
729
|
if (event?.reason && event.reason !== "startup") {
|
|
598
730
|
send({ type: "switchyard_native", sessionId, sessionFile });
|