@spencerboucher/switchyard 0.5.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.
Files changed (48) hide show
  1. package/README.md +4 -2
  2. package/dist/bridge/pi-model.js +44 -0
  3. package/dist/bridge/switchyard-bridge.js +24 -2
  4. package/dist/bridge/switchyard-terminal.js +84 -3
  5. package/dist/switchyard.mjs +404 -29
  6. package/package.json +1 -1
  7. package/web-dist/assets/{AutomationView-CFohp0SQ.js → AutomationView-DgZn3ltn.js} +2 -2
  8. package/web-dist/assets/{AutomationView-CFohp0SQ.js.map → AutomationView-DgZn3ltn.js.map} +1 -1
  9. package/web-dist/assets/{InteractionCard-B-xmpTNG.js → InteractionCard-cvPWKSu1.js} +2 -2
  10. package/web-dist/assets/{InteractionCard-B-xmpTNG.js.map → InteractionCard-cvPWKSu1.js.map} +1 -1
  11. package/web-dist/assets/{Menu-C4GUY_ym.js → Menu-GymfuHGP.js} +2 -2
  12. package/web-dist/assets/Menu-GymfuHGP.js.map +1 -0
  13. package/web-dist/assets/{Overlay--v0ur2lj.js → Overlay-oYnnwvFM.js} +2 -2
  14. package/web-dist/assets/{Overlay--v0ur2lj.js.map → Overlay-oYnnwvFM.js.map} +1 -1
  15. package/web-dist/assets/{attention-q6_xlnT4.js → attention-Bxg9W15n.js} +2 -2
  16. package/web-dist/assets/{attention-q6_xlnT4.js.map → attention-Bxg9W15n.js.map} +1 -1
  17. package/web-dist/assets/{automations-CyA83OAD.js → automations-DdB97dnL.js} +2 -2
  18. package/web-dist/assets/{automations-CyA83OAD.js.map → automations-DdB97dnL.js.map} +1 -1
  19. package/web-dist/assets/{board-BFvNcFhk.js → board-s4LuDPcg.js} +2 -2
  20. package/web-dist/assets/{board-BFvNcFhk.js.map → board-s4LuDPcg.js.map} +1 -1
  21. package/web-dist/assets/code-B0KXn7lD.js +8 -0
  22. package/web-dist/assets/code-B0KXn7lD.js.map +1 -0
  23. package/web-dist/assets/{describe-BirsjlUl.js → describe-C_3FJuSs.js} +2 -2
  24. package/web-dist/assets/{describe-BirsjlUl.js.map → describe-C_3FJuSs.js.map} +1 -1
  25. package/web-dist/assets/dist-Bgkh2hc5.js +56 -0
  26. package/web-dist/assets/dist-Bgkh2hc5.js.map +1 -0
  27. package/web-dist/assets/{enroll-C7UBC20s.js → enroll-DijmoolY.js} +2 -2
  28. package/web-dist/assets/{enroll-C7UBC20s.js.map → enroll-DijmoolY.js.map} +1 -1
  29. package/web-dist/assets/index-C5n2QB-Y.css +2 -0
  30. package/web-dist/assets/{index-IdzOP9GE.js → index-Cf6ZUCfK.js} +3 -3
  31. package/web-dist/assets/{index-IdzOP9GE.js.map → index-Cf6ZUCfK.js.map} +1 -1
  32. package/web-dist/assets/{more-syTnSpW3.js → more-CRGJsz6E.js} +2 -2
  33. package/web-dist/assets/{more-syTnSpW3.js.map → more-CRGJsz6E.js.map} +1 -1
  34. package/web-dist/assets/{now-BqsLS-tK.js → now-D6lDofYy.js} +2 -2
  35. package/web-dist/assets/{now-BqsLS-tK.js.map → now-D6lDofYy.js.map} +1 -1
  36. package/web-dist/assets/{settings-DRj7UPtP.js → settings-Csal1F-W.js} +6 -6
  37. package/web-dist/assets/settings-Csal1F-W.js.map +1 -0
  38. package/web-dist/assets/{useNow-BQpDYijR.js → useNow-Bc_NlDY3.js} +2 -2
  39. package/web-dist/assets/{useNow-BQpDYijR.js.map → useNow-Bc_NlDY3.js.map} +1 -1
  40. package/web-dist/index.html +2 -2
  41. package/web-dist/sw.js +1 -1
  42. package/web-dist/assets/Menu-C4GUY_ym.js.map +0 -1
  43. package/web-dist/assets/code-DavnmlQN.js +0 -8
  44. package/web-dist/assets/code-DavnmlQN.js.map +0 -1
  45. package/web-dist/assets/dist-C-h8FC-n.js +0 -56
  46. package/web-dist/assets/dist-C-h8FC-n.js.map +0 -1
  47. package/web-dist/assets/index-ByiuDoG0.css +0 -2
  48. package/web-dist/assets/settings-DRj7UPtP.js.map +0 -1
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):
@@ -116,7 +118,7 @@ switchyard up
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.5.0
121
+ ghcr.io/justmytwospence/switchyard:0.6.0
120
122
  docker exec switchyard switchyard init # prints a single-use sign-in link for your first device
121
123
  ```
122
124
 
@@ -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", () => {
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { randomUUID } from "node:crypto";
16
16
  import { createConnection } from "node:net";
17
+ import { modelInfo } from "./pi-model.js";
17
18
  const PATCHED = Symbol.for("switchyard.terminal");
18
19
  const WRAPPED = Symbol.for("switchyard.terminal.wrapped");
19
20
  function messageText(message) {
@@ -91,6 +92,14 @@ export default function switchyardTerminal(pi) {
91
92
  // never throw into Pi
92
93
  }
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
+ });
94
103
  const sendQueue = () => send({ type: "queue_update", ...queue });
95
104
  const flushEntries = () => {
96
105
  try {
@@ -314,14 +323,30 @@ export default function switchyardTerminal(pi) {
314
323
  sessionFile: ctx.sessionManager.getSessionFile() ?? null,
315
324
  sessionName: pi.getSessionName?.() ?? null,
316
325
  isStreaming: !ctx.isIdle(),
326
+ model: modelInfo(ctx.model),
327
+ thinkingLevel: ctx.thinkingLevel ?? pi.getThinkingLevel?.() ?? null,
317
328
  pendingMessageCount: ctx.hasPendingMessages() ? 1 : 0,
318
329
  });
319
330
  return;
320
331
  case "prompt": {
321
332
  const message = String(cmd.message ?? "");
322
333
  if (message.startsWith("/")) {
323
- respond(id, type, undefined, "Slash commands from Switchyard need the terminal in terminal sessions: type it there");
324
- return;
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
+ }
325
350
  }
326
351
  if (!ctx.isIdle()) {
327
352
  respond(id, type, undefined, "Session is running: choose Steer or Queue follow-up");
@@ -364,9 +389,61 @@ export default function switchyardTerminal(pi) {
364
389
  }
365
390
  case "get_session_stats": {
366
391
  const totals = sessionTotals(ctx.sessionManager.getEntries());
367
- respond(id, type, { tokens: { total: totals.tokens }, cost: totals.cost });
392
+ respond(id, type, {
393
+ tokens: { total: totals.tokens },
394
+ cost: totals.cost,
395
+ contextUsage: ctx.getContextUsage?.() ?? undefined,
396
+ });
368
397
  return;
369
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;
370
447
  case "shutdown":
371
448
  respond(id, type);
372
449
  setImmediate(() => {
@@ -461,6 +538,7 @@ export default function switchyardTerminal(pi) {
461
538
  const sessionId = c.sessionManager.getSessionId();
462
539
  const sessionFile = c.sessionManager.getSessionFile() ?? null;
463
540
  send({ type: "extension_hello", pid: process.pid, remoteDialogs, sessionId, sessionFile });
541
+ sendModelState();
464
542
  if (event?.reason && event.reason !== "startup") {
465
543
  send({ type: "switchyard_native", sessionId, sessionFile });
466
544
  }
@@ -515,6 +593,9 @@ export default function switchyardTerminal(pi) {
515
593
  aborted: event?.aborted === true,
516
594
  errorMessage: event?.errorMessage ?? "unknown error",
517
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" }));
518
599
  on("ui_prompt_start", (event) => send({ type: "ui_prompt", event: event.type, kind: event.kind, title: event.title ?? null }));
519
600
  on("ui_prompt_end", (event) => send({ type: "ui_prompt", event: event.type, kind: event.kind, title: event.title ?? null }));
520
601
  on("input", (event) => {