@osolmaz/pi-workflows 0.8.2 → 0.9.1

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 CHANGED
@@ -71,6 +71,25 @@ The npm package also includes the simpler `pi-workflows` snapshot viewer. To
71
71
  link that command from a clone, run `npm install && npm run build && npm link`,
72
72
  or run it in place with `npx tsx src/viewer/cli.ts`.
73
73
 
74
+ ## Herdr integration
75
+
76
+ Pi Workflows also ships as a [Herdr](https://herdr.dev) plugin. After installing
77
+ `piw` and Pi Workflows, link the bundled plugin once:
78
+
79
+ ```bash
80
+ pi-workflows herdr setup
81
+ ```
82
+
83
+ When Pi runs inside Herdr, a workflow widget shows `Ctrl+Shift+R piw`. When the widget has hidden rows, this call to action shares the existing scroll-controls line instead of taking another line.
84
+ The shortcut opens the exact run bundle and lets you choose a split, tab, or new
85
+ workspace. `/piw` opens the same menu, and `/piw right`, `/piw below`, `/piw
86
+ left`, `/piw above`, `/piw tab`, or `/piw workspace` selects a placement
87
+ directly. If a viewer for that run already exists, Pi Workflows focuses it
88
+ instead of opening a duplicate.
89
+
90
+ The plugin uses Herdr's public pane APIs and runs no service or polling loop. It
91
+ is also available through the [Herdr plugin marketplace](https://herdr.dev/plugins/).
92
+
74
93
  ## Quick start
75
94
 
76
95
  Put a workflow file in `.pi/workflows/` (project) or `~/.pi/agent/workflows/`
@@ -0,0 +1,50 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export declare const PIW_SHORTCUT = "ctrl+shift+r";
3
+ export declare const PIW_SHORTCUT_HINT = "Ctrl+Shift+R piw";
4
+ export declare const VIEWER_PLACEMENTS: readonly ["right", "below", "left", "above", "tab", "workspace"];
5
+ export type ViewerPlacement = (typeof VIEWER_PLACEMENTS)[number];
6
+ export type WorkflowViewTarget = {
7
+ runId: string;
8
+ workflowName: string;
9
+ runDir: string;
10
+ };
11
+ export type HerdrCapability = {
12
+ available: true;
13
+ } | {
14
+ available: false;
15
+ reason: string;
16
+ };
17
+ export type ViewerOpenResult = {
18
+ paneId: string;
19
+ reused: boolean;
20
+ warning?: string;
21
+ };
22
+ type Exec = ExtensionAPI["exec"];
23
+ type HerdrPane = {
24
+ paneId: string;
25
+ tabId: string;
26
+ workspaceId: string;
27
+ label?: string;
28
+ };
29
+ export declare class HerdrWorkflowViewer {
30
+ private readonly exec;
31
+ private readonly env;
32
+ private readonly knownPanes;
33
+ private readonly opening;
34
+ constructor(exec: Exec, env?: NodeJS.ProcessEnv);
35
+ probe(): Promise<HerdrCapability>;
36
+ focusExisting(target: WorkflowViewTarget): Promise<boolean>;
37
+ open(target: WorkflowViewTarget, placement: ViewerPlacement, cwd: string): Promise<ViewerOpenResult>;
38
+ private openOnce;
39
+ private findAndFocus;
40
+ find(target: WorkflowViewTarget): Promise<HerdrPane | undefined>;
41
+ private currentPane;
42
+ private openPluginPane;
43
+ private openWorkspace;
44
+ private closePane;
45
+ private run;
46
+ private runJson;
47
+ }
48
+ export declare function parseViewerPlacement(value: string): ViewerPlacement | undefined;
49
+ export declare function viewerPaneLabel(runId: string): string;
50
+ export {};
@@ -0,0 +1,310 @@
1
+ import { HERDR_PLUGIN_ENTRYPOINT, HERDR_PLUGIN_ID } from "../herdr/constants.js";
2
+ export const PIW_SHORTCUT = "ctrl+shift+r";
3
+ export const PIW_SHORTCUT_HINT = "Ctrl+Shift+R piw";
4
+ const COMMAND_TIMEOUT_MS = 5_000;
5
+ const MAX_JSON_CHARS = 1_000_000;
6
+ const VIEWER_LABEL_PREFIX = "piw · ";
7
+ export const VIEWER_PLACEMENTS = ["right", "below", "left", "above", "tab", "workspace"];
8
+ export class HerdrWorkflowViewer {
9
+ exec;
10
+ env;
11
+ knownPanes = new Map();
12
+ opening = new Map();
13
+ constructor(exec, env = process.env) {
14
+ this.exec = exec;
15
+ this.env = env;
16
+ }
17
+ async probe() {
18
+ if (this.env.HERDR_ENV !== "1") {
19
+ return { available: false, reason: "Pi is not running in Herdr." };
20
+ }
21
+ try {
22
+ await this.currentPane();
23
+ const plugin = await this.runJson("herdr", [
24
+ "plugin",
25
+ "list",
26
+ "--plugin",
27
+ HERDR_PLUGIN_ID,
28
+ "--json",
29
+ ]);
30
+ if (!pluginIsEnabled(plugin)) {
31
+ return {
32
+ available: false,
33
+ reason: `Herdr plugin ${HERDR_PLUGIN_ID} is not linked and enabled.`,
34
+ };
35
+ }
36
+ await this.run("piw", ["--version"]);
37
+ return { available: true };
38
+ }
39
+ catch (error) {
40
+ return { available: false, reason: errorMessage(error) };
41
+ }
42
+ }
43
+ async focusExisting(target) {
44
+ return (await this.findAndFocus(target)) !== undefined;
45
+ }
46
+ async open(target, placement, cwd) {
47
+ const pending = this.opening.get(target.runId);
48
+ if (pending !== undefined)
49
+ return await pending;
50
+ const opening = this.openOnce(target, placement, cwd).finally(() => {
51
+ if (this.opening.get(target.runId) === opening)
52
+ this.opening.delete(target.runId);
53
+ });
54
+ this.opening.set(target.runId, opening);
55
+ return await opening;
56
+ }
57
+ async openOnce(target, placement, cwd) {
58
+ const existingPaneId = await this.findAndFocus(target);
59
+ if (existingPaneId !== undefined) {
60
+ return { paneId: existingPaneId, reused: true };
61
+ }
62
+ const caller = await this.currentPane();
63
+ if (placement === "workspace") {
64
+ const opened = await this.openWorkspace(target, cwd);
65
+ this.knownPanes.set(target.runId, opened.paneId);
66
+ return { ...opened, reused: false };
67
+ }
68
+ const opened = await this.openPluginPane(target, placement, caller);
69
+ if (placement === "left" || placement === "above") {
70
+ try {
71
+ await this.run("herdr", [
72
+ "pane",
73
+ "swap",
74
+ "--source-pane",
75
+ opened.paneId,
76
+ "--target-pane",
77
+ caller.paneId,
78
+ ]);
79
+ }
80
+ catch (error) {
81
+ await this.closePane(opened.paneId);
82
+ throw error;
83
+ }
84
+ }
85
+ this.knownPanes.set(target.runId, opened.paneId);
86
+ return { paneId: opened.paneId, reused: false };
87
+ }
88
+ async findAndFocus(target) {
89
+ const knownPaneId = this.knownPanes.get(target.runId);
90
+ if (knownPaneId !== undefined) {
91
+ try {
92
+ await this.run("herdr", ["plugin", "pane", "focus", knownPaneId]);
93
+ return knownPaneId;
94
+ }
95
+ catch {
96
+ this.knownPanes.delete(target.runId);
97
+ }
98
+ }
99
+ const existing = await this.find(target);
100
+ if (existing === undefined)
101
+ return undefined;
102
+ try {
103
+ await this.run("herdr", ["plugin", "pane", "focus", existing.paneId]);
104
+ this.knownPanes.set(target.runId, existing.paneId);
105
+ return existing.paneId;
106
+ }
107
+ catch {
108
+ // The pane can close between the snapshot and focus request.
109
+ return undefined;
110
+ }
111
+ }
112
+ async find(target) {
113
+ const snapshot = await this.runJson("herdr", ["api", "snapshot"]);
114
+ return snapshotPanes(snapshot).find((pane) => pane.label === viewerPaneLabel(target.runId));
115
+ }
116
+ async currentPane() {
117
+ return parseCurrentPane(await this.runJson("herdr", ["pane", "current", "--current"]));
118
+ }
119
+ async openPluginPane(target, placement, caller) {
120
+ const args = [
121
+ "plugin",
122
+ "pane",
123
+ "open",
124
+ "--plugin",
125
+ HERDR_PLUGIN_ID,
126
+ "--entrypoint",
127
+ HERDR_PLUGIN_ENTRYPOINT,
128
+ "--env",
129
+ `PI_WORKFLOWS_RUN_ID=${target.runId}`,
130
+ "--env",
131
+ `PI_WORKFLOWS_RUN_DIR=${target.runDir}`,
132
+ "--focus",
133
+ ];
134
+ if (placement === "tab") {
135
+ args.push("--placement", "tab", "--workspace", caller.workspaceId);
136
+ }
137
+ else {
138
+ args.push("--placement", "split", "--target-pane", caller.paneId, "--direction", placement === "below" || placement === "above" ? "down" : "right");
139
+ }
140
+ return parseOpenedPane(await this.runJson("herdr", args));
141
+ }
142
+ async openWorkspace(target, cwd) {
143
+ const created = parseCreatedWorkspace(await this.runJson("herdr", [
144
+ "workspace",
145
+ "create",
146
+ "--cwd",
147
+ cwd,
148
+ "--label",
149
+ workspaceLabel(target.workflowName),
150
+ "--no-focus",
151
+ ]));
152
+ let opened;
153
+ try {
154
+ opened = parseOpenedPane(await this.runJson("herdr", [
155
+ "plugin",
156
+ "pane",
157
+ "open",
158
+ "--plugin",
159
+ HERDR_PLUGIN_ID,
160
+ "--entrypoint",
161
+ HERDR_PLUGIN_ENTRYPOINT,
162
+ "--placement",
163
+ "tab",
164
+ "--workspace",
165
+ created.workspaceId,
166
+ "--env",
167
+ `PI_WORKFLOWS_RUN_ID=${target.runId}`,
168
+ "--env",
169
+ `PI_WORKFLOWS_RUN_DIR=${target.runDir}`,
170
+ "--focus",
171
+ ]));
172
+ }
173
+ catch (error) {
174
+ await this.run("herdr", ["workspace", "close", created.workspaceId]).catch(() => undefined);
175
+ throw error;
176
+ }
177
+ try {
178
+ await this.run("herdr", ["tab", "close", created.bootstrapTabId]);
179
+ return { paneId: opened.paneId };
180
+ }
181
+ catch (error) {
182
+ return {
183
+ paneId: opened.paneId,
184
+ warning: `The piw viewer opened, but its empty bootstrap tab could not be closed: ${errorMessage(error)}`,
185
+ };
186
+ }
187
+ }
188
+ async closePane(paneId) {
189
+ await this.run("herdr", ["plugin", "pane", "close", paneId]).catch(() => undefined);
190
+ }
191
+ async run(command, args) {
192
+ const result = await this.exec(command, args, { timeout: COMMAND_TIMEOUT_MS });
193
+ if (result.killed) {
194
+ throw new Error(`${command} timed out.`);
195
+ }
196
+ if (result.code !== 0) {
197
+ const message = result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`;
198
+ throw new Error(`${command} failed: ${boundedText(message)}`);
199
+ }
200
+ return result.stdout;
201
+ }
202
+ async runJson(command, args) {
203
+ const stdout = await this.run(command, args);
204
+ if (stdout.length > MAX_JSON_CHARS) {
205
+ throw new Error(`${command} returned too much data.`);
206
+ }
207
+ try {
208
+ return JSON.parse(stdout);
209
+ }
210
+ catch {
211
+ throw new Error(`${command} returned invalid JSON.`);
212
+ }
213
+ }
214
+ }
215
+ export function parseViewerPlacement(value) {
216
+ return VIEWER_PLACEMENTS.find((placement) => placement === value);
217
+ }
218
+ export function viewerPaneLabel(runId) {
219
+ return `${VIEWER_LABEL_PREFIX}${runId}`;
220
+ }
221
+ function pluginIsEnabled(value) {
222
+ const result = recordValue(value, "result");
223
+ const plugins = result === undefined ? undefined : arrayValue(result, "plugins");
224
+ return (plugins?.some((plugin) => recordValue(plugin, "plugin_id") === HERDR_PLUGIN_ID &&
225
+ recordValue(plugin, "enabled") === true) === true);
226
+ }
227
+ function parseCurrentPane(value) {
228
+ const result = requiredRecord(value, "result", "Herdr pane response");
229
+ return paneFromValue(requiredRecord(result, "pane", "Herdr pane response"));
230
+ }
231
+ function parseOpenedPane(value) {
232
+ const result = requiredRecord(value, "result", "Herdr plugin pane response");
233
+ const pluginPane = requiredRecord(result, "plugin_pane", "Herdr plugin pane response");
234
+ return paneFromValue(requiredRecord(pluginPane, "pane", "Herdr plugin pane response"));
235
+ }
236
+ function parseCreatedWorkspace(value) {
237
+ const result = requiredRecord(value, "result", "Herdr workspace response");
238
+ const workspace = requiredRecord(result, "workspace", "Herdr workspace response");
239
+ const rootPane = requiredRecord(result, "root_pane", "Herdr workspace response");
240
+ return {
241
+ workspaceId: requiredString(workspace, "workspace_id", "Herdr workspace response"),
242
+ bootstrapTabId: requiredString(rootPane, "tab_id", "Herdr workspace response"),
243
+ };
244
+ }
245
+ function snapshotPanes(value) {
246
+ const result = requiredRecord(value, "result", "Herdr snapshot");
247
+ const snapshot = requiredRecord(result, "snapshot", "Herdr snapshot");
248
+ const panes = arrayValue(snapshot, "panes");
249
+ if (panes === undefined)
250
+ throw new Error("Herdr snapshot has no panes.");
251
+ return panes.flatMap((pane) => {
252
+ try {
253
+ return [paneFromValue(pane)];
254
+ }
255
+ catch {
256
+ return [];
257
+ }
258
+ });
259
+ }
260
+ function paneFromValue(value) {
261
+ if (!isRecord(value))
262
+ throw new Error("Herdr returned an invalid pane.");
263
+ const label = recordValue(value, "label");
264
+ return {
265
+ paneId: requiredString(value, "pane_id", "Herdr pane"),
266
+ tabId: requiredString(value, "tab_id", "Herdr pane"),
267
+ workspaceId: requiredString(value, "workspace_id", "Herdr pane"),
268
+ ...(typeof label === "string" ? { label } : {}),
269
+ };
270
+ }
271
+ function workspaceLabel(workflowName) {
272
+ const compact = workflowName.replace(/[\r\n\t]+/gu, " ").trim();
273
+ return `piw · ${(compact || "workflow").slice(0, 60)}`;
274
+ }
275
+ function requiredRecord(value, key, label) {
276
+ if (!isRecord(value))
277
+ throw new Error(`${label} is invalid.`);
278
+ const nested = value[key];
279
+ if (!isRecord(nested))
280
+ throw new Error(`${label} has no ${key}.`);
281
+ return nested;
282
+ }
283
+ function requiredString(value, key, label) {
284
+ const nested = value[key];
285
+ if (typeof nested !== "string" || nested.length === 0) {
286
+ throw new Error(`${label} has no ${key}.`);
287
+ }
288
+ return nested;
289
+ }
290
+ function recordValue(value, key) {
291
+ return isRecord(value) ? value[key] : undefined;
292
+ }
293
+ function arrayValue(value, key) {
294
+ const nested = recordValue(value, key);
295
+ return Array.isArray(nested) ? nested : undefined;
296
+ }
297
+ function isRecord(value) {
298
+ return typeof value === "object" && value !== null && !Array.isArray(value);
299
+ }
300
+ function boundedText(value) {
301
+ const compact = value
302
+ .replace(/[\r\n\t]+/gu, " ")
303
+ .replace(/ +/gu, " ")
304
+ .trim();
305
+ return compact.length <= 300 ? compact : `${compact.slice(0, 299)}…`;
306
+ }
307
+ function errorMessage(error) {
308
+ return error instanceof Error ? error.message : String(error);
309
+ }
310
+ //# sourceMappingURL=herdr-viewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"herdr-viewer.js","sourceRoot":"","sources":["../../src/extension/herdr-viewer.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
2
3
  import { isDeepStrictEqual } from "node:util";
3
4
  import { builtinWorkflowCatalog } from "../builtins/catalog.js";
4
5
  import { projectControllerStorePath, SqliteControllerStore } from "../controllers/index.js";
@@ -10,6 +11,7 @@ import { appendProgressHistory, progressRecordsFromTrace } from "../workflows/pr
10
11
  import { createRunId, listRunBundles, readLastTraceEvent, readRunBundle, WorkflowRunStore, createDefinitionSnapshot, } from "../workflows/store.js";
11
12
  import { PiControllerHost, parseControllerArgs, } from "./controller-host.js";
12
13
  import { ConversationStepExecutor } from "./executor.js";
14
+ import { HerdrWorkflowViewer, parseViewerPlacement, PIW_SHORTCUT, PIW_SHORTCUT_HINT, VIEWER_PLACEMENTS, } from "./herdr-viewer.js";
13
15
  import { SessionRecorder } from "./recorder.js";
14
16
  import { registerWorkflowAgentStepMessageRenderer, WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA, WORKFLOW_AGENT_STEP_MESSAGE_TYPE, } from "./step-message.js";
15
17
  import { buildWidgetView } from "./widget.js";
@@ -27,6 +29,15 @@ const MAX_STATUS_ERROR_CHARS = 4_000;
27
29
  const MAX_WORKFLOW_LIST_ITEMS = 50;
28
30
  const MAX_WORKFLOW_LIST_NAME_CHARS = 3_500;
29
31
  const PRESENTATION_TIMEOUT_MS = 30_000;
32
+ const PIW_PLACEMENT_LABELS = {
33
+ right: "Split right",
34
+ below: "Split below",
35
+ left: "Split left",
36
+ above: "Split above",
37
+ tab: "New tab",
38
+ workspace: "New workspace",
39
+ };
40
+ const PIW_PLACEMENT_BY_LABEL = new Map(VIEWER_PLACEMENTS.map((placement) => [PIW_PLACEMENT_LABELS[placement], placement]));
30
41
  class PresentationSupersededError extends Error {
31
42
  }
32
43
  class PresentationTimeoutError extends Error {
@@ -123,6 +134,14 @@ function workflowStateSummary(state) {
123
134
  }
124
135
  export default function piWorkflows(pi) {
125
136
  registerWorkflowAgentStepMessageRenderer(pi);
137
+ const herdrEnabled = process.env.HERDR_ENV === "1";
138
+ const herdrViewer = new HerdrWorkflowViewer((command, args, options) => pi.exec(command, args, options));
139
+ let herdrCapability = {
140
+ available: false,
141
+ reason: "Herdr integration has not been checked.",
142
+ };
143
+ let workflowViewTarget = null;
144
+ let herdrProbeGeneration = 0;
126
145
  // One runner identity per session; it names this session in run claims.
127
146
  const runnerId = randomUUID();
128
147
  let runQueueStore = null;
@@ -277,7 +296,12 @@ export default function piWorkflows(pi) {
277
296
  const render = (width = Number.POSITIVE_INFINITY, theme) => {
278
297
  if (!widgetSource)
279
298
  return [];
280
- const view = buildWidgetView(widgetSource.state, widgetSource.snapshot, new Date(), widgetScroll, runHeld(), width, theme, widgetSource.updateHistory);
299
+ const view = buildWidgetView(widgetSource.state, widgetSource.snapshot, new Date(), widgetScroll, runHeld(), width, theme, widgetSource.updateHistory, ctx.mode === "tui" &&
300
+ herdrEnabled &&
301
+ herdrCapability.available &&
302
+ workflowViewTarget?.runId === widgetSource.state.runId
303
+ ? PIW_SHORTCUT_HINT
304
+ : undefined);
281
305
  widgetShownScroll = view.scroll;
282
306
  widgetMaxScroll = view.maxScroll;
283
307
  if (widgetScroll !== null) {
@@ -308,14 +332,66 @@ export default function piWorkflows(pi) {
308
332
  snapshot,
309
333
  ...(updateHistory !== undefined ? { updateHistory: [...updateHistory] } : {}),
310
334
  };
335
+ workflowViewTarget = {
336
+ runId: state.runId,
337
+ workflowName: state.workflowName,
338
+ runDir: path.resolve(new WorkflowRunStore().runDirFor(state.runId)),
339
+ };
311
340
  renderWidget(ctx);
312
341
  };
313
342
  const clearWidget = (ctx) => {
314
343
  widgetSource = null;
344
+ workflowViewTarget = null;
315
345
  widgetScroll = null;
316
346
  setWidget(ctx, undefined);
317
347
  setStatus(ctx, undefined);
318
348
  };
349
+ const refreshHerdrCapability = async (ctx) => {
350
+ const generation = ++herdrProbeGeneration;
351
+ const capability = await herdrViewer.probe();
352
+ if (!sessionClosed && generation === herdrProbeGeneration) {
353
+ herdrCapability = capability;
354
+ renderWidget(ctx);
355
+ }
356
+ return capability;
357
+ };
358
+ const selectPiwPlacement = async (ctx) => {
359
+ if (!ctx.hasUI || ctx.mode !== "tui")
360
+ return undefined;
361
+ const label = await ctx.ui.select("Open workflow in piw", VIEWER_PLACEMENTS.map((placement) => PIW_PLACEMENT_LABELS[placement]));
362
+ return label === undefined ? undefined : PIW_PLACEMENT_BY_LABEL.get(label);
363
+ };
364
+ const openPiw = async (ctx, requestedPlacement) => {
365
+ const target = workflowViewTarget;
366
+ if (target === null) {
367
+ notify(ctx, "No workflow run is available to open in piw.", "warning");
368
+ return;
369
+ }
370
+ const capability = await refreshHerdrCapability(ctx);
371
+ if (!capability.available) {
372
+ notify(ctx, capability.reason, "warning");
373
+ return;
374
+ }
375
+ try {
376
+ if (await herdrViewer.focusExisting(target)) {
377
+ notify(ctx, `Focused the piw viewer for ${target.workflowName}.`);
378
+ return;
379
+ }
380
+ const placement = requestedPlacement ?? (await selectPiwPlacement(ctx));
381
+ if (placement === undefined) {
382
+ if (!ctx.hasUI || ctx.mode !== "tui") {
383
+ notify(ctx, "Specify a piw placement: right, below, left, above, tab, or workspace.");
384
+ }
385
+ return;
386
+ }
387
+ const opened = await herdrViewer.open(target, placement, ctx.cwd);
388
+ if (opened.warning !== undefined)
389
+ notify(ctx, opened.warning, "warning");
390
+ }
391
+ catch (error) {
392
+ notify(ctx, `Could not open piw: ${errorMessage(error)}`, "error");
393
+ }
394
+ };
319
395
  const scrollWidget = (ctx, delta) => {
320
396
  if (!widgetSource || widgetMaxScroll === 0) {
321
397
  return;
@@ -696,6 +772,7 @@ export default function piWorkflows(pi) {
696
772
  run.renewTimer.unref?.();
697
773
  }
698
774
  clearWidgetTimer();
775
+ workflowViewTarget = null;
699
776
  startWidgetTicker(ctx, run);
700
777
  if (!options.quiet) {
701
778
  notify(ctx, `Workflow ${workflow.name} started. Follow it live with: pi-workflows view`);
@@ -1191,6 +1268,22 @@ export default function piWorkflows(pi) {
1191
1268
  throw error;
1192
1269
  }
1193
1270
  };
1271
+ pi.registerCommand("piw", {
1272
+ description: "Open the current workflow run in piw through Herdr",
1273
+ getArgumentCompletions: async (prefix) => {
1274
+ const items = VIEWER_PLACEMENTS.filter((placement) => placement.startsWith(prefix)).map((placement) => ({ value: placement, label: placement }));
1275
+ return items.length > 0 ? items : null;
1276
+ },
1277
+ handler: async (args, ctx) => {
1278
+ const value = args.trim();
1279
+ const placement = value.length === 0 ? undefined : parseViewerPlacement(value);
1280
+ if (value.length > 0 && placement === undefined) {
1281
+ notify(ctx, "piw placement must be right, below, left, above, tab, or workspace.", "error");
1282
+ return;
1283
+ }
1284
+ await openPiw(ctx, placement);
1285
+ },
1286
+ });
1194
1287
  pi.registerCommand("workflow", {
1195
1288
  description: "Run or manage a workflow: /workflow <name-or-path> [task | --input-json {…}]; also: status, pause, resume, cancel, answer",
1196
1289
  getArgumentCompletions: async (prefix) => {
@@ -1406,6 +1499,12 @@ export default function piWorkflows(pi) {
1406
1499
  };
1407
1500
  },
1408
1501
  });
1502
+ if (herdrEnabled) {
1503
+ pi.registerShortcut(PIW_SHORTCUT, {
1504
+ description: "Open the current workflow run in piw",
1505
+ handler: async (ctx) => await openPiw(ctx),
1506
+ });
1507
+ }
1409
1508
  pi.registerShortcut("shift+up", {
1410
1509
  description: "Scroll the workflow widget up",
1411
1510
  handler: (ctx) => scrollWidget(ctx, -WIDGET_SCROLL_STEP),
@@ -1417,6 +1516,8 @@ export default function piWorkflows(pi) {
1417
1516
  pi.on("session_start", async (_event, ctx) => {
1418
1517
  sessionClosed = false;
1419
1518
  controllerContext = ctx;
1519
+ if (herdrEnabled)
1520
+ void refreshHerdrCapability(ctx);
1420
1521
  try {
1421
1522
  const queue = ensureRunQueueStore(ctx.cwd);
1422
1523
  const migration = await migrateLegacyWorkflowSources({
@@ -1550,6 +1651,7 @@ export default function piWorkflows(pi) {
1550
1651
  });
1551
1652
  pi.on("session_shutdown", async () => {
1552
1653
  sessionClosed = true;
1654
+ herdrProbeGeneration += 1;
1553
1655
  systemTurnAbort = null;
1554
1656
  suppressWorkflowAssistantTail = false;
1555
1657
  supersedePresentation();
@@ -1581,6 +1683,7 @@ export default function piWorkflows(pi) {
1581
1683
  clearWidgetTimer();
1582
1684
  stopWidgetTicker();
1583
1685
  widgetSource = null;
1686
+ workflowViewTarget = null;
1584
1687
  widgetScroll = null;
1585
1688
  });
1586
1689
  }