@agimon-ai/po-runner 0.0.0-alpha.21 → 0.0.0-alpha.22

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 (49) hide show
  1. package/dist/{bashRunService-Ci0cGdYO.d.mts → bashRunService-BL4JIUi2.d.mts} +6 -1
  2. package/dist/bashRunService-BL4JIUi2.d.mts.map +1 -0
  3. package/dist/{bashRunService-DjOH5kye.cjs → bashRunService-CJhMNrPz.cjs} +8 -4
  4. package/dist/bashRunService-CJhMNrPz.cjs.map +1 -0
  5. package/dist/{bashRunService-Cl3pYccr.mjs → bashRunService-DbYsj1zc.mjs} +8 -4
  6. package/dist/bashRunService-DbYsj1zc.mjs.map +1 -0
  7. package/dist/bashRunService.cjs +1 -1
  8. package/dist/bashRunService.d.mts +1 -1
  9. package/dist/bashRunService.d.mts.map +1 -1
  10. package/dist/bashRunService.mjs +1 -1
  11. package/dist/bashTool.cjs +1 -1
  12. package/dist/bashTool.d.mts +2 -2
  13. package/dist/bashTool.mjs +1 -1
  14. package/dist/bin/cli.cjs +1 -1
  15. package/dist/bin/cli.mjs +1 -1
  16. package/dist/extensions/native.mjs +212 -203
  17. package/dist/extensions/native.mjs.map +1 -1
  18. package/dist/extensions/server.cjs +2 -2
  19. package/dist/extensions/server.mjs +2 -2
  20. package/dist/extensions/web.mjs +334 -500
  21. package/dist/extensions/web.mjs.map +1 -1
  22. package/dist/{headless-Dko_yBiy.cjs → headless-C4Bvl2B-.cjs} +8 -4
  23. package/dist/headless-C4Bvl2B-.cjs.map +1 -0
  24. package/dist/{headless-DOlCixyf.mjs → headless-DidUBSSf.mjs} +8 -4
  25. package/dist/headless-DidUBSSf.mjs.map +1 -0
  26. package/dist/{index--xF7DvsE.d.mts → index-CXbcmf-G.d.mts} +2 -2
  27. package/dist/{index--xF7DvsE.d.mts.map → index-CXbcmf-G.d.mts.map} +1 -1
  28. package/dist/{index-BzeXJk6V.d.mts → index-DJkjYgir.d.mts} +3 -3
  29. package/dist/{index-BzeXJk6V.d.mts.map → index-DJkjYgir.d.mts.map} +1 -1
  30. package/dist/index.cjs +2 -2
  31. package/dist/index.d.mts +2 -2
  32. package/dist/index.mjs +2 -2
  33. package/dist/responseEnvelope.d.mts +1 -1
  34. package/dist/{runnerDependencies-7h_cxOva.mjs → runnerDependencies-CamKwphq.mjs} +2 -2
  35. package/dist/{runnerDependencies-7h_cxOva.mjs.map → runnerDependencies-CamKwphq.mjs.map} +1 -1
  36. package/dist/{runnerDependencies-DSun3xO-.cjs → runnerDependencies-vkpJfxZZ.cjs} +2 -2
  37. package/dist/{runnerDependencies-DSun3xO-.cjs.map → runnerDependencies-vkpJfxZZ.cjs.map} +1 -1
  38. package/dist/runnerDependencies.cjs +1 -1
  39. package/dist/runnerDependencies.d.mts +1 -1
  40. package/dist/runnerDependencies.mjs +1 -1
  41. package/dist/{type-Do0YxwVQ.d.mts → type-BQ2shEB5.d.mts} +2 -2
  42. package/dist/{type-Do0YxwVQ.d.mts.map → type-BQ2shEB5.d.mts.map} +1 -1
  43. package/dist/types.d.mts +1 -1
  44. package/package.json +17 -17
  45. package/dist/bashRunService-Ci0cGdYO.d.mts.map +0 -1
  46. package/dist/bashRunService-Cl3pYccr.mjs.map +0 -1
  47. package/dist/bashRunService-DjOH5kye.cjs.map +0 -1
  48. package/dist/headless-DOlCixyf.mjs.map +0 -1
  49. package/dist/headless-Dko_yBiy.cjs.map +0 -1
@@ -1,70 +1,12 @@
1
1
  import { createApiClient, defineActivityGroup, defineChannelFile, defineFill, defineSessionStore, defineToolRenderer, defineWebPlugin, sessionApiAddress } from "@agimon-ai/po-core/web";
2
- import { AnsiLine, AnsiText, ArrowRightIcon, Button, Checkbox, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Dot, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, EmptyState, FileIcon, Input, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, PlayCircleIcon, PlusIcon, SearchIcon, StatusBadge, SyntaxText, TerminalView, Textarea, ansiSpans, toolTone } from "@agimon-ai/po-web-components";
2
+ import { AnsiLine, AnsiText, ArrowRightIcon, Button, Dot, EmptyState, FileIcon, FolderIcon, Input, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, PlayCircleIcon, SearchIcon, StatusBadge, SyntaxText, TerminalView, ansiSpans, toolTone } from "@agimon-ai/po-web-components";
3
3
  import { useStore } from "@tanstack/react-store";
4
4
  import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
5
- import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
5
  import { sealedTransport } from "@agimon-ai/po-web-security/browser";
7
- //#region src/extensions/workspaces/sessions/(frontend)/_lib/format.ts
8
- /**
9
- * Local copy of the runtime's formatUptime (src/services/bashResult/index.ts):
10
- * the web plugin may reach only src/types, so the one helper it needs lives here.
11
- */
12
- function formatRunnerUptime(startedAt, now) {
13
- const started = Date.parse(startedAt);
14
- if (Number.isNaN(started)) return "unknown";
15
- const seconds = Math.max(0, Math.floor((now - started) / 1e3));
16
- if (seconds < 60) return `${seconds}s`;
17
- const minutes = Math.floor(seconds / 60);
18
- if (minutes < 60) return `${minutes}m`;
19
- const hours = Math.floor(minutes / 60);
20
- return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}d`;
21
- }
22
- /**
23
- * The lines a log view shows: the slice's text, then whatever the follow
24
- * stream appended, bounded to the newest `max`.
25
- *
26
- * A log file ends with a newline, and the reader hands its text back with that
27
- * newline intact, so a naive split reports one blank line more than the file
28
- * has. Dropping exactly one trailing empty entry keeps the count the reader
29
- * sees equal to the count the server reported.
30
- */
31
- function logViewLines(text, appended, max, options = {}) {
32
- const split = text.split("\n");
33
- if (split.length > 0 && split[split.length - 1] === "") split.pop();
34
- if (options.dropPartialTail === true && split.length > 0) split.pop();
35
- const all = [...split, ...appended];
36
- const lines = all.slice(-max);
37
- return {
38
- lines,
39
- hidden: all.length - lines.length
40
- };
41
- }
42
- /**
43
- * Absolute 1-based numbers for a tail, counted back from the last line.
44
- *
45
- * A tail is contiguous, so only its end has to be known. Grep results are not
46
- * contiguous and carry their own numbers from the reader instead.
47
- */
48
- function tailLineNumbers(lastLineNumber, count) {
49
- const first = Math.max(1, lastLineNumber - count + 1);
50
- return Array.from({ length: count }, (_, index) => first + index);
51
- }
52
- /**
53
- * Whether the view is actually following a growing log.
54
- *
55
- * Three things must hold: the reader asked to follow, no query is set (a
56
- * filtered view is a snapshot of the whole file, which a stream cannot
57
- * assemble a chunk at a time), and the runner is still writing. Without the
58
- * last one a finished run would claim to be tailing a file nothing will ever
59
- * append to.
60
- */
61
- function isFollowingLive(following, filtering, running) {
62
- return following && !filtering && running;
63
- }
64
- //#endregion
6
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
65
7
  //#region src/extensions/workspaces/sessions/(frontend)/_lib/launchLine.ts
66
8
  /**
67
- * The `/runners start` line the launch form sends.
9
+ * The `/runners start` line the runners panel's command bar sends.
68
10
  *
69
11
  * WHY THE BUILDER LIVES HERE AND THE PARSER DOES NOT:
70
12
  * Cockpit code is compiled into the host bundle and may import only this
@@ -80,6 +22,21 @@ const RUNNER_SHELL_REQUEST = {
80
22
  name: "shell",
81
23
  interactive: true
82
24
  };
25
+ /**
26
+ * What the runners panel's Run sends: the typed command on a terminal when
27
+ * one is available, or a login shell when nothing was typed.
28
+ */
29
+ function runnerRunRequest(command, cwd) {
30
+ const where = cwd.trim() === "" ? {} : { cwd };
31
+ return command.trim() === "" ? {
32
+ ...RUNNER_SHELL_REQUEST,
33
+ ...where
34
+ } : {
35
+ command,
36
+ ...where,
37
+ preferInteractive: true
38
+ };
39
+ }
83
40
  /** Quotes a value that carries whitespace, which is how a value survives tokenizing. */
84
41
  function quote(value) {
85
42
  return /\s/u.test(value) ? `"${value}"` : value;
@@ -90,7 +47,7 @@ function runnerLaunchLine(request) {
90
47
  const pairs = [
91
48
  ...name === "" ? [] : [`name=${quote(name)}`],
92
49
  ...cwd === "" ? [] : [`cwd=${quote(cwd)}`],
93
- ...request.interactive === true ? ["interactive=true"] : []
50
+ ...request.interactive === true ? ["interactive=true"] : request.preferInteractive === true ? ["interactive=prefer"] : []
94
51
  ];
95
52
  return [
96
53
  RUNNERS_START_LINE,
@@ -99,12 +56,6 @@ function runnerLaunchLine(request) {
99
56
  request.command.trim()
100
57
  ].join(" ");
101
58
  }
102
- /** What the form will not let a reader send, worded the way the session would answer. */
103
- function launchProblems(request) {
104
- const problems = [];
105
- if (request.command.trim() === "") problems.push("A runner needs a command to run.");
106
- return problems;
107
- }
108
59
  //#endregion
109
60
  //#region src/constants/webRunners.ts
110
61
  const RUNNER_RUNS_TYPE = "runner_runs";
@@ -174,141 +125,62 @@ const runnerRunsChannel = runners.channel({
174
125
  }
175
126
  });
176
127
  //#endregion
177
- //#region src/extensions/workspaces/sessions/(frontend)/_components/LaunchRunnerDialog.tsx
178
- const COMMAND_ROWS = 3;
179
- const INTERACTIVE_FIELD_ID = "runner-launch-interactive-field";
180
- function FieldLabel({ children }) {
181
- return /* @__PURE__ */ jsx("span", {
182
- className: "text-base font-medium text-po-text",
183
- children
184
- });
128
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/format.ts
129
+ /**
130
+ * Local copy of the runtime's formatUptime (src/services/bashResult/index.ts):
131
+ * the web plugin may reach only src/types, so the one helper it needs lives here.
132
+ */
133
+ function formatRunnerUptime(startedAt, now) {
134
+ const started = Date.parse(startedAt);
135
+ if (Number.isNaN(started)) return "unknown";
136
+ const seconds = Math.max(0, Math.floor((now - started) / 1e3));
137
+ if (seconds < 60) return `${seconds}s`;
138
+ const minutes = Math.floor(seconds / 60);
139
+ if (minutes < 60) return `${minutes}m`;
140
+ const hours = Math.floor(minutes / 60);
141
+ return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}d`;
185
142
  }
186
143
  /**
187
- * Starts one runner by hand: the command, where it runs, what to call it, and
188
- * whether it needs a terminal, then the exact `/runners start` line the
189
- * session will parse.
144
+ * The lines a log view shows: the slice's text, then whatever the follow
145
+ * stream appended, bounded to the newest `max`.
190
146
  *
191
- * The line is shown rather than hidden. This sends a shell command through the
192
- * session, and a reader is entitled to see exactly what they are about to run
193
- * before they run it.
147
+ * A log file ends with a newline, and the reader hands its text back with that
148
+ * newline intact, so a naive split reports one blank line more than the file
149
+ * has. Dropping exactly one trailing empty entry keeps the count the reader
150
+ * sees equal to the count the server reported.
194
151
  */
195
- function LaunchRunnerDialog({ sessionId, sendSessionFrame, defaultCwd, onClose }) {
196
- const [command, setCommand] = useState("");
197
- const [cwd, setCwd] = useState(defaultCwd ?? "");
198
- const [name, setName] = useState("");
199
- const [interactive, setInteractive] = useState(false);
200
- const request = {
201
- command,
202
- cwd,
203
- name,
204
- interactive
205
- };
206
- const problems = launchProblems(request);
207
- const line = runnerLaunchLine(request);
208
- const launch = () => {
209
- if (problems.length > 0) return;
210
- requestRunnerStart(sendSessionFrame, sessionId, request);
211
- onClose();
152
+ function logViewLines(text, appended, max, options = {}) {
153
+ const split = text.split("\n");
154
+ if (split.length > 0 && split[split.length - 1] === "") split.pop();
155
+ if (options.dropPartialTail === true && split.length > 0) split.pop();
156
+ const all = [...split, ...appended];
157
+ const lines = all.slice(-max);
158
+ return {
159
+ lines,
160
+ hidden: all.length - lines.length
212
161
  };
213
- return /* @__PURE__ */ jsx(Dialog, {
214
- open: true,
215
- onOpenChange: (open) => {
216
- if (!open) onClose();
217
- },
218
- children: /* @__PURE__ */ jsxs(DialogContent, {
219
- width: "lg",
220
- "data-testid": "runner-launch-dialog",
221
- "aria-describedby": void 0,
222
- children: [/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, {
223
- "data-testid": "runner-launch-title",
224
- children: "launch a runner"
225
- }) }), /* @__PURE__ */ jsxs(DialogBody, { children: [
226
- /* @__PURE__ */ jsxs("div", {
227
- className: "flex flex-col gap-1",
228
- children: [/* @__PURE__ */ jsx(FieldLabel, { children: "Command" }), /* @__PURE__ */ jsx(Textarea, {
229
- "data-testid": "runner-launch-command",
230
- rows: COMMAND_ROWS,
231
- value: command,
232
- placeholder: "pnpm test",
233
- onChange: (event) => setCommand(event.target.value)
234
- })]
235
- }),
236
- /* @__PURE__ */ jsxs("div", {
237
- className: "flex flex-col gap-2.5 sm:flex-row",
238
- children: [/* @__PURE__ */ jsxs("div", {
239
- className: "flex min-w-0 flex-1 flex-col gap-1",
240
- children: [/* @__PURE__ */ jsx(FieldLabel, { children: "Cwd" }), /* @__PURE__ */ jsx(Input, {
241
- size: "sm",
242
- "data-testid": "runner-launch-cwd",
243
- value: cwd,
244
- placeholder: "the session's own directory",
245
- onChange: (event) => setCwd(event.target.value)
246
- })]
247
- }), /* @__PURE__ */ jsxs("div", {
248
- className: "flex min-w-0 flex-1 flex-col gap-1",
249
- children: [/* @__PURE__ */ jsx(FieldLabel, { children: "Name" }), /* @__PURE__ */ jsx(Input, {
250
- size: "sm",
251
- "data-testid": "runner-launch-name",
252
- value: name,
253
- placeholder: "chosen for you when empty",
254
- onChange: (event) => setName(event.target.value)
255
- })]
256
- })]
257
- }),
258
- /* @__PURE__ */ jsxs("div", {
259
- className: "flex items-center gap-2",
260
- children: [/* @__PURE__ */ jsx(Checkbox, {
261
- id: INTERACTIVE_FIELD_ID,
262
- "data-testid": "runner-launch-interactive",
263
- checked: interactive,
264
- onCheckedChange: (checked) => setInteractive(checked === true)
265
- }), /* @__PURE__ */ jsx("label", {
266
- htmlFor: INTERACTIVE_FIELD_ID,
267
- className: "text-base text-po-dim",
268
- children: "needs a terminal, for a command that prompts (requires rmux or tmux)"
269
- })]
270
- }),
271
- /* @__PURE__ */ jsxs("div", {
272
- className: "flex flex-col gap-1 rounded-md border border-po-border bg-po-deep px-3 py-2",
273
- children: [/* @__PURE__ */ jsx(FieldLabel, { children: "Sent to the session" }), /* @__PURE__ */ jsx("pre", {
274
- "data-testid": "runner-launch-line",
275
- className: "whitespace-pre-wrap break-words font-mono text-sm text-po-green",
276
- children: line
277
- })]
278
- }),
279
- problems.length > 0 ? /* @__PURE__ */ jsx("p", {
280
- "data-testid": "runner-launch-problem",
281
- className: "text-base text-po-red",
282
- children: problems.join(" ")
283
- }) : null,
284
- /* @__PURE__ */ jsxs(DialogFooter, {
285
- className: "flex-wrap sm:flex-nowrap",
286
- children: [
287
- /* @__PURE__ */ jsx("span", {
288
- className: "w-full text-sm text-po-dim sm:w-auto",
289
- children: "it appears above once the runtime starts it"
290
- }),
291
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
292
- /* @__PURE__ */ jsx(Button, {
293
- variant: "outline",
294
- size: "xs",
295
- "data-testid": "runner-launch-cancel",
296
- onClick: onClose,
297
- children: "cancel"
298
- }),
299
- /* @__PURE__ */ jsx(Button, {
300
- variant: "primary",
301
- size: "xs",
302
- "data-testid": "runner-launch-submit",
303
- disabled: problems.length > 0,
304
- onClick: launch,
305
- children: "launch"
306
- })
307
- ]
308
- })
309
- ] })]
310
- })
311
- });
162
+ }
163
+ /**
164
+ * Absolute 1-based numbers for a tail, counted back from the last line.
165
+ *
166
+ * A tail is contiguous, so only its end has to be known. Grep results are not
167
+ * contiguous and carry their own numbers from the reader instead.
168
+ */
169
+ function tailLineNumbers(lastLineNumber, count) {
170
+ const first = Math.max(1, lastLineNumber - count + 1);
171
+ return Array.from({ length: count }, (_, index) => first + index);
172
+ }
173
+ /**
174
+ * Whether the view is actually following a growing log.
175
+ *
176
+ * Three things must hold: the reader asked to follow, no query is set (a
177
+ * filtered view is a snapshot of the whole file, which a stream cannot
178
+ * assemble a chunk at a time), and the runner is still writing. Without the
179
+ * last one a finished run would claim to be tailing a file nothing will ever
180
+ * append to.
181
+ */
182
+ function isFollowingLive(following, filtering, running) {
183
+ return following && !filtering && running;
312
184
  }
313
185
  //#endregion
314
186
  //#region src/constants/webRunnerLog.ts
@@ -496,9 +368,9 @@ function followRunnerLog(sessionId, runId, from, handlers) {
496
368
  }
497
369
  //#endregion
498
370
  //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnerLogPanel.tsx
499
- const TICK_MS$2 = 1e4;
371
+ const TICK_MS$1 = 1e4;
500
372
  /** The tab id doubles as the URL segment, so it stays plain and unique across plugins. */
501
- const TAB_ID_PREFIX$1 = "runner-log-";
373
+ const TAB_ID_PREFIX = "runner-log-";
502
374
  /** Lines held in the view; a follow that ran all day must not grow without bound. */
503
375
  const MAX_VIEW_LINES = 2e3;
504
376
  /** Context lines each side of a match, the same either-side window grep would use. */
@@ -513,7 +385,7 @@ const BYTES_PER_MB$1 = BYTES_PER_KB$1 * BYTES_PER_KB$1;
513
385
  function runnerLogTab(run) {
514
386
  const runId = run.id;
515
387
  return {
516
- id: `${TAB_ID_PREFIX$1}${runId}`,
388
+ id: `${TAB_ID_PREFIX}${runId}`,
517
389
  label: `log · ${run.name}`,
518
390
  panel: (props) => /* @__PURE__ */ jsx(RunnerLogPanel, {
519
391
  ...props,
@@ -552,19 +424,6 @@ function isMatch(line, needle, ignoreCase) {
552
424
  if (needle === "") return false;
553
425
  return ignoreCase ? line.toLowerCase().includes(needle.toLowerCase()) : line.includes(needle);
554
426
  }
555
- /** One fact of the runner, on its own line so a long command is never elided. */
556
- function MetaRow({ label, value }) {
557
- return /* @__PURE__ */ jsxs("span", {
558
- className: "flex min-w-0 gap-2.5",
559
- children: [/* @__PURE__ */ jsx("span", {
560
- className: "w-14 shrink-0 text-base text-po-dim",
561
- children: label
562
- }), /* @__PURE__ */ jsx("span", {
563
- className: "min-w-0 flex-1 break-all font-mono text-sm text-po-dim",
564
- children: value
565
- })]
566
- });
567
- }
568
427
  /**
569
428
  * One rendered log line: its number, whether it matched, and the text.
570
429
  *
@@ -573,24 +432,24 @@ function MetaRow({ label, value }) {
573
432
  * keyed on the joined string; keyed by line number instead, an append only
574
433
  * costs the line that appended.
575
434
  */
576
- function LogRow({ number, line, matched }) {
435
+ function LogRow({ number, line, matched, dimmed }) {
577
436
  return /* @__PURE__ */ jsxs("div", {
578
437
  "data-testid": "runner-log-row",
579
438
  "data-matched": matched,
580
439
  className: "flex min-w-0 gap-2",
581
440
  children: [
582
441
  number === void 0 ? null : /* @__PURE__ */ jsx("span", {
583
- className: "w-12 shrink-0 select-none text-right text-xs leading-relaxed text-po-faint",
442
+ className: "w-12 shrink-0 select-none text-right font-mono text-sm leading-relaxed text-po-faint",
584
443
  children: number
585
444
  }),
586
445
  /* @__PURE__ */ jsx("span", {
587
446
  "aria-hidden": true,
588
- className: `w-4 shrink-0 select-none text-xs leading-relaxed ${matched ? "text-po-yellow" : "text-po-faint"}`,
447
+ className: `w-4 shrink-0 select-none font-mono text-sm leading-relaxed ${matched ? "text-po-yellow" : "text-po-faint"}`,
589
448
  children: matched ? ">>" : ""
590
449
  }),
591
450
  /* @__PURE__ */ jsx(AnsiLine, {
592
451
  line,
593
- className: `min-w-0 flex-1 whitespace-pre-wrap break-words font-mono text-xs leading-relaxed ${matched ? "text-po-text" : "text-po-dim"}`
452
+ className: `min-w-0 flex-1 whitespace-pre-wrap break-words font-mono text-sm leading-relaxed ${dimmed ? "text-po-dim" : "text-po-text"}`
594
453
  })
595
454
  ]
596
455
  });
@@ -604,7 +463,7 @@ function LogRow({ number, line, matched }) {
604
463
  * rather than moving a cursor through it, and following pauses while one is
605
464
  * set because a filtered view is a snapshot of a file that is still growing.
606
465
  */
607
- function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
466
+ function RunnerLogPanel({ sessionId, runId, sendSessionFrame, onTerminal }) {
608
467
  const { runs, stopRequested } = useStore(runners.store, (state) => runners.select(state, sessionId));
609
468
  const run = runs.find((candidate) => candidate.id === runId);
610
469
  const [now, setNow] = useState(() => Date.now());
@@ -624,7 +483,7 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
624
483
  const filtering = query !== "";
625
484
  const live = isFollowingLive(following, filtering, slice?.running === true);
626
485
  useEffect(() => {
627
- const timer = setInterval(() => setNow(Date.now()), TICK_MS$2);
486
+ const timer = setInterval(() => setNow(Date.now()), TICK_MS$1);
628
487
  return () => clearInterval(timer);
629
488
  }, []);
630
489
  useEffect(() => {
@@ -720,6 +579,7 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
720
579
  }),
721
580
  /* @__PURE__ */ jsx(StatusBadge, {
722
581
  tone: badge.tone,
582
+ size: "md",
723
583
  "data-testid": "runner-log-state",
724
584
  children: badge.label
725
585
  }),
@@ -735,60 +595,71 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
735
595
  children: "no longer listed; its log stays readable"
736
596
  }),
737
597
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
598
+ onTerminal ? /* @__PURE__ */ jsx(Button, {
599
+ variant: "outline",
600
+ size: "sm",
601
+ "data-testid": "runner-log-terminal",
602
+ onClick: onTerminal,
603
+ children: "Back to terminal"
604
+ }) : null,
738
605
  run !== void 0 && run.state === "running" && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
739
606
  variant: stopping ? "outline" : "danger-outline",
740
- size: "xs",
607
+ size: "sm",
741
608
  "data-testid": "runner-log-stop",
742
609
  disabled: stopping,
743
610
  title: stopping ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
744
611
  onClick: () => requestRunnerStop(sendSessionFrame, sessionId, run.id),
745
- className: "px-2 ",
746
- children: stopping ? "stopping…" : "stop"
612
+ children: stopping ? "Stopping…" : "Stop"
747
613
  }) : null
748
614
  ]
749
615
  }),
750
- run ? /* @__PURE__ */ jsxs("div", {
751
- className: "flex shrink-0 flex-col gap-1.5 border-b border-po-border-soft px-3 py-2.5 sm:px-[26px]",
752
- children: [/* @__PURE__ */ jsx(MetaRow, {
753
- label: "command",
754
- value: run.command
755
- }), /* @__PURE__ */ jsx(MetaRow, {
756
- label: "cwd",
757
- value: run.cwd
758
- })]
616
+ run ? /* @__PURE__ */ jsx("div", {
617
+ className: "shrink-0 border-b border-po-border-soft px-3 py-3 sm:px-[26px]",
618
+ children: /* @__PURE__ */ jsxs("div", {
619
+ className: "flex flex-col gap-1 rounded-md bg-po-deep px-3 py-2",
620
+ children: [/* @__PURE__ */ jsxs("span", {
621
+ className: "break-all font-mono text-base text-po-hi",
622
+ children: [/* @__PURE__ */ jsxs("span", {
623
+ "aria-hidden": true,
624
+ className: "select-none text-po-faint",
625
+ children: ["$", " "]
626
+ }), run.command]
627
+ }), /* @__PURE__ */ jsxs("span", {
628
+ className: "break-all font-mono text-sm text-po-dim",
629
+ children: ["in ", run.cwd]
630
+ })]
631
+ })
759
632
  }) : null,
760
633
  /* @__PURE__ */ jsxs("div", {
761
- className: "flex min-h-10 shrink-0 flex-wrap items-center gap-2 border-b border-po-border-soft px-3 py-2 sm:h-10 sm:flex-nowrap sm:gap-2.5 sm:px-[26px] sm:py-0",
634
+ className: "flex min-h-11 shrink-0 flex-wrap items-center gap-2 border-b border-po-border-soft px-3 py-2 sm:flex-nowrap sm:gap-2.5 sm:px-[26px]",
762
635
  children: [
763
636
  /* @__PURE__ */ jsxs("span", {
764
- className: "relative flex min-w-0 flex-1 items-center sm:w-[300px] sm:flex-none",
765
- children: [/* @__PURE__ */ jsx(SearchIcon, { className: "pointer-events-none absolute left-2.5 h-3 w-3 text-po-faint" }), /* @__PURE__ */ jsx(Input, {
766
- size: "sm",
637
+ className: "relative flex min-w-0 flex-1 items-center sm:w-72 sm:flex-none",
638
+ children: [/* @__PURE__ */ jsx(SearchIcon, { className: "pointer-events-none absolute left-2.5 h-3.5 w-3.5 text-po-faint" }), /* @__PURE__ */ jsx(Input, {
639
+ size: "md",
767
640
  "data-testid": "runner-log-search",
768
641
  value: typed,
769
- placeholder: "search this log",
642
+ placeholder: "Search output",
770
643
  onChange: (event) => setTyped(event.target.value),
771
- className: "pl-7 "
644
+ className: "w-full pl-8"
772
645
  })]
773
646
  }),
774
647
  /* @__PURE__ */ jsx(Button, {
775
648
  variant: ignoreCase ? "subtle" : "outline",
776
- size: "xs",
649
+ size: "sm",
777
650
  "data-testid": "runner-log-ignore-case",
778
651
  "aria-pressed": ignoreCase,
779
652
  title: "match regardless of case",
780
653
  onClick: () => setIgnoreCase((current) => !current),
781
- className: "px-2 ",
782
- children: "aA"
654
+ children: ignoreCase ? "Any case" : "Match case"
783
655
  }),
784
656
  /* @__PURE__ */ jsx(Button, {
785
657
  variant: context > 0 && filtering ? "subtle" : "outline",
786
- size: "xs",
658
+ size: "sm",
787
659
  "data-testid": "runner-log-context",
788
660
  disabled: !filtering,
789
661
  title: "lines of context kept either side of a match",
790
662
  onClick: () => setContext((current) => current === 0 ? CONTEXT_LINES : 0),
791
- className: "px-2 ",
792
663
  children: `± ${String(context)} lines`
793
664
  }),
794
665
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
@@ -799,14 +670,13 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
799
670
  }),
800
671
  /* @__PURE__ */ jsx(Button, {
801
672
  variant: live ? "subtle" : "outline",
802
- size: "xs",
673
+ size: "sm",
803
674
  "data-testid": "runner-log-follow",
804
675
  "aria-pressed": live,
805
676
  disabled: filtering || slice?.running !== true,
806
677
  title: filtering ? "a filtered view is a snapshot; clear the query to follow" : "follow the log as it grows",
807
678
  onClick: () => setFollowing((current) => !current),
808
- className: "px-2 ",
809
- children: filtering ? "follow · paused" : "follow"
679
+ children: filtering ? "Paused while searching" : live ? "Following" : "Follow"
810
680
  })
811
681
  ]
812
682
  }),
@@ -819,7 +689,7 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
819
689
  className: "h-full overflow-auto bg-po-deep px-3 py-3 sm:px-[26px]",
820
690
  children: [error !== void 0 ? /* @__PURE__ */ jsx("p", {
821
691
  "data-testid": "runner-log-error",
822
- className: "text-base text-po-red",
692
+ className: "text-sm text-po-red",
823
693
  children: error
824
694
  }) : slice === void 0 ? /* @__PURE__ */ jsx("p", {
825
695
  "data-testid": "runner-log-reading",
@@ -827,10 +697,10 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
827
697
  children: "reading…"
828
698
  }) : slice.exists === false ? /* @__PURE__ */ jsx("p", {
829
699
  className: "text-sm text-po-dim",
830
- children: "this runner has not written a log yet"
700
+ children: "This runner has not printed anything yet."
831
701
  }) : view.lines.length === 0 ? /* @__PURE__ */ jsx("p", {
832
702
  className: "text-sm text-po-dim",
833
- children: filtering ? "nothing matched" : "the log is empty"
703
+ children: filtering ? "Nothing matched." : "The log is empty."
834
704
  }) : view.lines.map((line, index) => {
835
705
  const number = numbers[index];
836
706
  const previous = numbers[index - 1];
@@ -838,25 +708,26 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
838
708
  return /* @__PURE__ */ jsxs(Fragment, { children: [gap ? /* @__PURE__ */ jsx("div", {
839
709
  "data-testid": "runner-log-gap",
840
710
  "aria-hidden": true,
841
- className: "select-none py-0.5 pl-12 text-xs leading-relaxed text-po-faint",
711
+ className: "select-none py-0.5 pl-14 text-sm leading-relaxed text-po-faint",
842
712
  children: "⋯"
843
713
  }) : null, /* @__PURE__ */ jsx(LogRow, {
844
714
  number,
845
715
  line,
846
- matched: filtering && isMatch(line, query, ignoreCase)
716
+ matched: filtering && isMatch(line, query, ignoreCase),
717
+ dimmed: filtering && !isMatch(line, query, ignoreCase)
847
718
  })] }, number ?? `row-${String(index)}`);
848
719
  }), /* @__PURE__ */ jsx("div", { ref: bottom })]
849
720
  }), live && !pinned ? /* @__PURE__ */ jsx(Button, {
850
721
  variant: "subtle",
851
- size: "xs",
722
+ size: "sm",
852
723
  "data-testid": "runner-log-jump",
853
724
  title: "scroll back to the newest lines and keep following",
854
725
  onClick: () => {
855
726
  setPinned(true);
856
727
  bottom.current?.scrollIntoView({ block: "end" });
857
728
  },
858
- className: "absolute bottom-3 right-3 px-2 ",
859
- children: "jump to latest"
729
+ className: "absolute bottom-3 right-3",
730
+ children: "Jump to latest"
860
731
  }) : null]
861
732
  }),
862
733
  /* @__PURE__ */ jsxs("div", {
@@ -955,21 +826,8 @@ async function sendRunnerInput(sessionId, runId, text) {
955
826
  }
956
827
  //#endregion
957
828
  //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnerShellPanel.tsx
958
- const TAB_ID_PREFIX = "runner-shell-";
959
829
  /** Keystrokes are gathered for this long and sent as one request. */
960
830
  const FLUSH_MS = 30;
961
- /** The temporary tab holding one interactive runner's pane. */
962
- function runnerShellTab(run) {
963
- const runId = run.id;
964
- return {
965
- id: `${TAB_ID_PREFIX}${runId}`,
966
- label: `shell · ${run.name}`,
967
- panel: (props) => /* @__PURE__ */ jsx(RunnerShellPanel, {
968
- ...props,
969
- runId
970
- })
971
- };
972
- }
973
831
  /**
974
832
  * An interactive runner's pane, attached.
975
833
  *
@@ -981,9 +839,13 @@ function runnerShellTab(run) {
981
839
  *
982
840
  * xterm is loaded when the tab opens rather than with the plugin. Most sessions
983
841
  * never attach to anything, and an emulator is not a small thing to carry.
842
+ *
843
+ * Search reads the scrubbed log the sink writes beside the raw bytes, so it
844
+ * hands over to the log view: `onSearch` when the panel is embedded, the
845
+ * runner's log tab otherwise.
984
846
  */
985
- function RunnerShellPanel({ sessionId, runId }) {
986
- const { runs } = useStore(runners.store, (state) => runners.select(state, sessionId));
847
+ function RunnerShellPanel({ sessionId, runId, sendSessionFrame, openTransientTab, onSearch }) {
848
+ const { runs, stopRequested } = useStore(runners.store, (state) => runners.select(state, sessionId));
987
849
  const run = runs.find((candidate) => candidate.id === runId);
988
850
  const [ended, setEnded] = useState(false);
989
851
  const [lost, setLost] = useState(false);
@@ -1021,12 +883,13 @@ function RunnerShellPanel({ sessionId, runId }) {
1021
883
  }, FLUSH_MS);
1022
884
  };
1023
885
  const live = run?.state === "running" && !ended;
886
+ const stopping = stopRequested.includes(runId);
1024
887
  return /* @__PURE__ */ jsxs("div", {
1025
888
  "data-testid": "runner-shell-panel",
1026
889
  className: "flex min-h-0 flex-1 flex-col",
1027
890
  children: [
1028
891
  /* @__PURE__ */ jsxs("div", {
1029
- className: "flex min-h-11 shrink-0 items-center gap-2.5 border-b border-po-border-soft px-3 sm:h-11 sm:px-[26px]",
892
+ className: "flex min-h-11 shrink-0 flex-wrap items-center gap-2 border-b border-po-border-soft px-3 py-2 sm:flex-nowrap sm:gap-2.5 sm:px-[26px] sm:py-1.5",
1030
893
  children: [
1031
894
  /* @__PURE__ */ jsx("span", {
1032
895
  "data-testid": "runner-shell-name",
@@ -1035,23 +898,45 @@ function RunnerShellPanel({ sessionId, runId }) {
1035
898
  }),
1036
899
  /* @__PURE__ */ jsx(StatusBadge, {
1037
900
  tone: live ? "running" : "neutral",
901
+ size: "md",
1038
902
  "data-testid": "runner-shell-state",
1039
- children: live ? "attached" : "detached"
903
+ children: live ? "Running" : "Ended"
1040
904
  }),
905
+ run ? /* @__PURE__ */ jsx("span", {
906
+ className: "min-w-0 truncate font-mono text-sm text-po-dim",
907
+ children: run.command
908
+ }) : null,
1041
909
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
1042
910
  lost ? /* @__PURE__ */ jsx("span", {
1043
911
  "data-testid": "runner-shell-lost",
1044
- className: "shrink-0 text-base text-po-red",
1045
- children: "the connection to this pane dropped"
912
+ className: "shrink-0 text-sm text-po-red",
913
+ children: "The connection to this terminal dropped. Reopen the runner to attach again."
914
+ }) : null,
915
+ run ? /* @__PURE__ */ jsxs(Button, {
916
+ variant: "outline",
917
+ size: "sm",
918
+ "data-testid": "runner-shell-search",
919
+ title: "search everything this runner has printed",
920
+ onClick: () => onSearch ? onSearch() : openTransientTab(runnerLogTab(run)),
921
+ className: "gap-1.5",
922
+ children: [/* @__PURE__ */ jsx(SearchIcon, { className: "h-3.5 w-3.5" }), "Search output"]
1046
923
  }) : null,
1047
924
  live ? /* @__PURE__ */ jsx(Button, {
1048
925
  variant: "outline",
1049
- size: "xs",
926
+ size: "sm",
1050
927
  "data-testid": "runner-shell-interrupt",
1051
928
  title: "send ctrl-c",
1052
929
  onClick: () => send(""),
1053
- className: "px-2 ",
1054
- children: "ctrl-c"
930
+ children: "Ctrl-C"
931
+ }) : null,
932
+ live && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
933
+ variant: stopping ? "outline" : "danger-outline",
934
+ size: "sm",
935
+ "data-testid": "runner-shell-stop",
936
+ disabled: stopping,
937
+ title: stopping ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
938
+ onClick: () => requestRunnerStop(sendSessionFrame, sessionId, runId),
939
+ children: stopping ? "Stopping…" : "Stop"
1055
940
  }) : null
1056
941
  ]
1057
942
  }),
@@ -1072,13 +957,185 @@ function RunnerShellPanel({ sessionId, runId }) {
1072
957
  className: "flex h-8 shrink-0 items-center border-t border-po-border-soft px-3 sm:px-[26px]",
1073
958
  children: /* @__PURE__ */ jsx("span", {
1074
959
  className: "min-w-0 flex-1 truncate text-sm text-po-dim",
1075
- children: live ? "typing goes straight to the runner" : "this runner is no longer accepting input"
960
+ children: live ? `Typing goes straight to ${run?.name ?? "the runner"}. Ctrl-C stops what is running.` : "This runner has ended. Its output stays searchable."
1076
961
  })
1077
962
  })
1078
963
  ]
1079
964
  });
1080
965
  }
1081
966
  //#endregion
967
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnersPanel.tsx
968
+ const LAUNCH_TIMEOUT_MS = 3e4;
969
+ const RUNNERS_TAB_ID = "runner-runs";
970
+ /** The temporary tab the dock's runners group opens from its own name. */
971
+ function runnersTab() {
972
+ return {
973
+ id: RUNNERS_TAB_ID,
974
+ label: "runners",
975
+ panel: RunnersPanel
976
+ };
977
+ }
978
+ /**
979
+ * Everything this session has up, and the one place to start another.
980
+ *
981
+ * A runner is a shell: the bar takes a command and runs it on its own
982
+ * terminal, or opens a plain shell when nothing was typed. The selected
983
+ * runner is shown in place, as its terminal when it has one and as its log
984
+ * when it does not (a command the agent backgrounded has no pane).
985
+ *
986
+ * Running runners only, the same rule the rail follows. A finished run leaves
987
+ * the moment it exits; its log stays readable from the tool message that
988
+ * started it, and `po-runner list` is where history lives.
989
+ */
990
+ function RunnersPanel(props) {
991
+ const { sessionId, sendSessionFrame } = props;
992
+ const session = useStore(runners.store, (state) => runners.select(state, sessionId));
993
+ const running = session.runs.filter((run) => run.state === "running");
994
+ const [command, setCommand] = useState("");
995
+ const [cwd, setCwd] = useState("");
996
+ const [selectedId, setSelectedId] = useState(void 0);
997
+ /** The terminal runner whose log is being searched instead of shown live. */
998
+ const [searchingId, setSearchingId] = useState(void 0);
999
+ const launchBaseline = useRef(null);
1000
+ const launchTimeout = useRef(void 0);
1001
+ const selected = running.find((run) => run.id === selectedId) ?? running[0];
1002
+ const launch = () => {
1003
+ if (sessionId === null) return;
1004
+ launchBaseline.current = new Set(session.runs.map((run) => run.id));
1005
+ clearTimeout(launchTimeout.current);
1006
+ launchTimeout.current = setTimeout(() => {
1007
+ launchBaseline.current = null;
1008
+ }, LAUNCH_TIMEOUT_MS);
1009
+ requestRunnerStart(sendSessionFrame, sessionId, runnerRunRequest(command, cwd));
1010
+ setCommand("");
1011
+ };
1012
+ useEffect(() => {
1013
+ const baseline = launchBaseline.current;
1014
+ if (baseline === null) return;
1015
+ const started = session.runs.find((run) => run.state === "running" && !baseline.has(run.id));
1016
+ if (!started) return;
1017
+ launchBaseline.current = null;
1018
+ clearTimeout(launchTimeout.current);
1019
+ setSelectedId(started.id);
1020
+ }, [session.runs]);
1021
+ useEffect(() => () => clearTimeout(launchTimeout.current), []);
1022
+ return /* @__PURE__ */ jsxs("div", {
1023
+ "data-testid": "runners-panel",
1024
+ className: "flex min-h-0 flex-1 flex-col",
1025
+ children: [
1026
+ /* @__PURE__ */ jsxs("div", {
1027
+ className: "flex h-11 shrink-0 items-center gap-2.5 border-b border-po-border-soft px-3 sm:px-[26px]",
1028
+ children: [/* @__PURE__ */ jsx("span", {
1029
+ className: "text-lg font-semibold text-po-hi",
1030
+ children: "Runners"
1031
+ }), /* @__PURE__ */ jsx("span", {
1032
+ "data-testid": "runners-panel-count",
1033
+ className: "text-sm text-po-dim",
1034
+ children: running.length === 0 ? "Nothing running" : `${String(running.length)} running`
1035
+ })]
1036
+ }),
1037
+ sessionId === null ? null : /* @__PURE__ */ jsxs("div", {
1038
+ className: "flex shrink-0 flex-col gap-1.5 border-b border-po-border-soft px-3 py-3 sm:px-[26px]",
1039
+ children: [/* @__PURE__ */ jsxs("div", {
1040
+ className: "flex flex-wrap items-center gap-2 sm:flex-nowrap",
1041
+ children: [
1042
+ /* @__PURE__ */ jsxs("span", {
1043
+ className: "relative flex min-w-0 flex-1 basis-full items-center sm:basis-auto",
1044
+ children: [/* @__PURE__ */ jsx("span", {
1045
+ "aria-hidden": true,
1046
+ className: "pointer-events-none absolute left-3 font-mono text-base text-po-faint",
1047
+ children: "$"
1048
+ }), /* @__PURE__ */ jsx(Input, {
1049
+ size: "lg",
1050
+ "data-testid": "runners-panel-command",
1051
+ "aria-label": "Command to run",
1052
+ value: command,
1053
+ placeholder: "a command, like pnpm dev",
1054
+ autoComplete: "off",
1055
+ spellCheck: false,
1056
+ onChange: (event) => setCommand(event.target.value),
1057
+ onKeyDown: (event) => {
1058
+ if (event.key === "Enter") launch();
1059
+ },
1060
+ className: "w-full pl-7 font-mono"
1061
+ })]
1062
+ }),
1063
+ /* @__PURE__ */ jsxs("span", {
1064
+ className: "relative flex min-w-0 flex-1 items-center sm:w-56 sm:flex-none",
1065
+ children: [/* @__PURE__ */ jsx(FolderIcon, { className: "pointer-events-none absolute left-2.5 h-3.5 w-3.5 text-po-faint" }), /* @__PURE__ */ jsx(Input, {
1066
+ size: "lg",
1067
+ "data-testid": "runners-panel-cwd",
1068
+ "aria-label": "Runs in",
1069
+ value: cwd,
1070
+ placeholder: "session directory",
1071
+ spellCheck: false,
1072
+ onChange: (event) => setCwd(event.target.value),
1073
+ className: "w-full pl-8 font-mono"
1074
+ })]
1075
+ }),
1076
+ /* @__PURE__ */ jsx(Button, {
1077
+ variant: "primary",
1078
+ size: "lg",
1079
+ "data-testid": "runners-panel-run",
1080
+ onClick: launch,
1081
+ children: command.trim() === "" ? "Open shell" : "Run"
1082
+ })
1083
+ ]
1084
+ }), /* @__PURE__ */ jsx("span", {
1085
+ className: "text-sm text-po-dim",
1086
+ children: "Runs in its own terminal and keeps going after Po's turn ends. Leave it empty for a plain shell."
1087
+ })]
1088
+ }),
1089
+ selected === void 0 ? /* @__PURE__ */ jsx(EmptyState, {
1090
+ "data-testid": "runners-panel-empty",
1091
+ className: "px-4 py-8",
1092
+ title: "Nothing running yet",
1093
+ description: sessionId === null ? "Open a session to start a runner." : "Type a command above, like pnpm dev, and it opens here as a live terminal."
1094
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
1095
+ "data-testid": "runners-panel-picker",
1096
+ className: "flex shrink-0 flex-wrap gap-1.5 px-3 pt-3 sm:px-[26px]",
1097
+ children: running.map((run) => /* @__PURE__ */ jsxs(Button, {
1098
+ variant: run === selected ? "subtle" : "ghost",
1099
+ size: "sm",
1100
+ "data-testid": `runners-panel-pick-${run.id}`,
1101
+ "aria-pressed": run === selected,
1102
+ title: run.command,
1103
+ onClick: () => setSelectedId(run.id),
1104
+ className: `gap-1.5 ${run === selected ? "text-po-hi" : ""}`,
1105
+ children: [/* @__PURE__ */ jsx(Dot, {
1106
+ tone: "yellow",
1107
+ pulse: true
1108
+ }), run.name]
1109
+ }, run.id))
1110
+ }), /* @__PURE__ */ jsx("div", {
1111
+ className: "mt-3 flex min-h-0 flex-1 flex-col border-t border-po-border-soft",
1112
+ children: selected.interactive && searchingId !== selected.id ? /* @__PURE__ */ jsx(RunnerShellPanel, {
1113
+ ...props,
1114
+ runId: selected.id,
1115
+ onSearch: () => setSearchingId(selected.id)
1116
+ }, selected.id) : /* @__PURE__ */ jsx(RunnerLogPanel, {
1117
+ ...props,
1118
+ runId: selected.id,
1119
+ ...selected.interactive ? { onTerminal: () => setSearchingId(void 0) } : {}
1120
+ }, selected.id)
1121
+ })] })
1122
+ ]
1123
+ });
1124
+ }
1125
+ //#endregion
1126
+ //#region src/extensions/workspaces/sessions/(frontend)/activity-group/runners.web.ts
1127
+ var runners_web_default = defineActivityGroup({
1128
+ keys: "r l",
1129
+ icon: PlayCircleIcon,
1130
+ statusKey: "po-runner-runners",
1131
+ activeSource: runnerActivitySource,
1132
+ transientTab: runnersTab,
1133
+ order: 20
1134
+ });
1135
+ //#endregion
1136
+ //#region src/extensions/workspaces/sessions/(frontend)/channel/runner-runs.web.ts
1137
+ var runner_runs_web_default = defineChannelFile(runnerRunsChannel);
1138
+ //#endregion
1082
1139
  //#region src/extensions/workspaces/sessions/(frontend)/_components/runnerTail.ts
1083
1140
  /**
1084
1141
  * The newest line a running runner has written, for a row that has one line to
@@ -1154,229 +1211,6 @@ function useRunnerTail(sessionId, runId, running) {
1154
1211
  return running && tail?.runId === runId ? tail.text : void 0;
1155
1212
  }
1156
1213
  //#endregion
1157
- //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnersPanel.tsx
1158
- const TICK_MS$1 = 1e4;
1159
- const SHELL_LAUNCH_TIMEOUT_MS = 3e4;
1160
- const RUNNERS_TAB_ID = "runner-runs";
1161
- /** The temporary tab the dock's runners group opens from its own name. */
1162
- function runnersTab() {
1163
- return {
1164
- id: RUNNERS_TAB_ID,
1165
- label: "runners",
1166
- panel: RunnersPanel
1167
- };
1168
- }
1169
- /**
1170
- * Everything this session has up, one card each.
1171
- *
1172
- * The dock's rail gives a runner one line, which is enough to notice it and
1173
- * not enough to work with it. This is the same set with room: the command in
1174
- * full, where it runs, and the last thing it said.
1175
- *
1176
- * Running runners only, the same rule the rail follows. A finished run leaves
1177
- * the moment it exits, because this answers "what is happening"; its log stays
1178
- * readable from its own tab, and `po-runner list` is where history lives.
1179
- */
1180
- function RunnersPanel({ sessionId, sendSessionFrame, openTransientTab }) {
1181
- const session = useStore(runners.store, (state) => runners.select(state, sessionId));
1182
- const running = session.runs.filter((run) => run.state === "running");
1183
- const [now, setNow] = useState(() => Date.now());
1184
- const [launching, setLaunching] = useState(false);
1185
- const shellBaseline = useRef(null);
1186
- const shellLaunchTimeout = useRef(void 0);
1187
- const defaultCwd = running[0]?.cwd;
1188
- const startShell = () => {
1189
- if (sessionId === null) return;
1190
- shellBaseline.current = new Set(session.runs.map((run) => run.id));
1191
- clearTimeout(shellLaunchTimeout.current);
1192
- shellLaunchTimeout.current = setTimeout(() => {
1193
- shellBaseline.current = null;
1194
- }, SHELL_LAUNCH_TIMEOUT_MS);
1195
- requestRunnerStart(sendSessionFrame, sessionId, RUNNER_SHELL_REQUEST);
1196
- };
1197
- useEffect(() => {
1198
- const timer = setInterval(() => setNow(Date.now()), TICK_MS$1);
1199
- return () => clearInterval(timer);
1200
- }, []);
1201
- useEffect(() => {
1202
- const baseline = shellBaseline.current;
1203
- if (baseline === null) return;
1204
- const shell = session.runs.find((run) => run.state === "running" && run.interactive && !baseline.has(run.id));
1205
- if (!shell) return;
1206
- shellBaseline.current = null;
1207
- clearTimeout(shellLaunchTimeout.current);
1208
- openTransientTab(runnerShellTab(shell));
1209
- }, [openTransientTab, session.runs]);
1210
- useEffect(() => () => clearTimeout(shellLaunchTimeout.current), []);
1211
- return /* @__PURE__ */ jsxs("div", {
1212
- "data-testid": "runners-panel",
1213
- className: "flex min-h-0 flex-1 flex-col overflow-auto",
1214
- children: [
1215
- /* @__PURE__ */ jsxs("div", {
1216
- className: "flex min-h-11 shrink-0 items-center gap-2.5 border-b border-po-border-soft px-3 sm:h-11 sm:px-[26px]",
1217
- children: [
1218
- /* @__PURE__ */ jsx("span", {
1219
- className: "text-lg font-semibold text-po-hi",
1220
- children: "runners"
1221
- }),
1222
- /* @__PURE__ */ jsx("span", {
1223
- "data-testid": "runners-panel-count",
1224
- className: "text-sm text-po-dim",
1225
- children: running.length === 0 ? "nothing running" : `${String(running.length)} running`
1226
- }),
1227
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
1228
- sessionId === null ? null : /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
1229
- asChild: true,
1230
- children: /* @__PURE__ */ jsx(Button, {
1231
- variant: "ghost",
1232
- size: "icon",
1233
- "data-testid": "runners-panel-launch",
1234
- title: "launch a runner",
1235
- "aria-label": "launch a runner",
1236
- className: "text-po-faint hover:text-po-hi",
1237
- children: /* @__PURE__ */ jsx(PlusIcon, { className: "h-3 w-3" })
1238
- })
1239
- }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
1240
- align: "end",
1241
- "data-testid": "runners-panel-launch-menu",
1242
- children: [/* @__PURE__ */ jsx(DropdownMenuItem, {
1243
- "data-testid": "runners-panel-start-shell",
1244
- onSelect: startShell,
1245
- children: "start shell"
1246
- }), /* @__PURE__ */ jsx(DropdownMenuItem, {
1247
- "data-testid": "runners-panel-run-command",
1248
- onSelect: () => setLaunching(true),
1249
- children: "run command"
1250
- })]
1251
- })] })
1252
- ]
1253
- }),
1254
- running.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
1255
- "data-testid": "runners-panel-empty",
1256
- className: "px-4 py-8",
1257
- title: "nothing running",
1258
- description: "a command that outlives its turn moves to a runner and appears here while it works.",
1259
- children: sessionId === null ? null : /* @__PURE__ */ jsx(Button, {
1260
- variant: "outline",
1261
- size: "xs",
1262
- "data-testid": "runners-panel-launch-empty",
1263
- onClick: () => setLaunching(true),
1264
- className: "px-2 ",
1265
- children: "launch a runner"
1266
- })
1267
- }) : /* @__PURE__ */ jsx("div", {
1268
- "data-testid": "runners-panel-grid",
1269
- className: "grid grid-cols-1 gap-2.5 px-3 py-3 sm:grid-cols-2 sm:px-[26px] xl:grid-cols-3",
1270
- children: running.map((run) => /* @__PURE__ */ jsx(RunnerCard, {
1271
- run,
1272
- now,
1273
- stopRequested: session.stopRequested.includes(run.id),
1274
- sessionId,
1275
- sendSessionFrame,
1276
- openTransientTab
1277
- }, run.id))
1278
- }),
1279
- launching && sessionId !== null ? /* @__PURE__ */ jsx(LaunchRunnerDialog, {
1280
- sessionId,
1281
- sendSessionFrame,
1282
- ...defaultCwd === void 0 ? {} : { defaultCwd },
1283
- onClose: () => setLaunching(false)
1284
- }) : null
1285
- ]
1286
- });
1287
- }
1288
- /**
1289
- * One running runner. Its own component because the tail line is a
1290
- * subscription per card, and a card that leaves the grid closes it.
1291
- */
1292
- function RunnerCard({ run, now, stopRequested, sessionId, sendSessionFrame, openTransientTab }) {
1293
- const tail = useRunnerTail(sessionId, run.id, run.state === "running");
1294
- return /* @__PURE__ */ jsxs("div", {
1295
- "data-testid": `runners-card-${run.id}`,
1296
- className: "flex min-w-0 flex-col gap-1.5 rounded-md border border-po-border-soft bg-po-panel p-2.5",
1297
- children: [
1298
- /* @__PURE__ */ jsxs("div", {
1299
- className: "flex min-w-0 items-center gap-1.5",
1300
- children: [
1301
- /* @__PURE__ */ jsx(Dot, {
1302
- tone: "yellow",
1303
- pulse: true
1304
- }),
1305
- /* @__PURE__ */ jsx("span", {
1306
- className: "min-w-0 flex-1 truncate text-base font-semibold text-po-hi",
1307
- children: run.name
1308
- }),
1309
- /* @__PURE__ */ jsxs("span", {
1310
- className: "shrink-0 text-sm text-po-dim",
1311
- children: [formatRunnerUptime(run.startedAt, now), run.interactive ? " · tty" : ""]
1312
- })
1313
- ]
1314
- }),
1315
- /* @__PURE__ */ jsx("span", {
1316
- className: "min-w-0 break-all font-mono text-sm leading-normal text-po-dim",
1317
- children: run.command
1318
- }),
1319
- /* @__PURE__ */ jsx("span", {
1320
- className: "min-w-0 truncate font-mono text-sm text-po-dim",
1321
- children: run.cwd
1322
- }),
1323
- /* @__PURE__ */ jsx("span", {
1324
- "data-testid": `runners-card-detail-${run.id}`,
1325
- "data-detail": tail === void 0 ? "command" : "tail",
1326
- className: "min-w-0 truncate text-sm text-po-dim",
1327
- children: tail ?? "…"
1328
- }),
1329
- /* @__PURE__ */ jsxs("div", {
1330
- className: "flex items-center gap-1.5 pt-0.5",
1331
- children: [
1332
- /* @__PURE__ */ jsx(Button, {
1333
- variant: "outline",
1334
- size: "xs",
1335
- "data-testid": `runners-card-log-${run.id}`,
1336
- title: "open this runner's log",
1337
- onClick: () => openTransientTab(runnerLogTab(run)),
1338
- className: "px-2 ",
1339
- children: "log"
1340
- }),
1341
- run.interactive && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
1342
- variant: "outline",
1343
- size: "xs",
1344
- "data-testid": `runners-card-shell-${run.id}`,
1345
- title: "attach to this runner's terminal",
1346
- onClick: () => openTransientTab(runnerShellTab(run)),
1347
- className: "px-2 ",
1348
- children: "shell"
1349
- }) : null,
1350
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
1351
- sessionId !== null ? /* @__PURE__ */ jsx(Button, {
1352
- variant: stopRequested ? "outline" : "danger-outline",
1353
- size: "xs",
1354
- "data-testid": `runners-card-stop-${run.id}`,
1355
- disabled: stopRequested,
1356
- title: stopRequested ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
1357
- onClick: () => requestRunnerStop(sendSessionFrame, sessionId, run.id),
1358
- className: "px-2 ",
1359
- children: stopRequested ? "stopping…" : "stop"
1360
- }) : null
1361
- ]
1362
- })
1363
- ]
1364
- });
1365
- }
1366
- //#endregion
1367
- //#region src/extensions/workspaces/sessions/(frontend)/activity-group/runners.web.ts
1368
- var runners_web_default = defineActivityGroup({
1369
- keys: "r l",
1370
- icon: PlayCircleIcon,
1371
- statusKey: "po-runner-runners",
1372
- activeSource: runnerActivitySource,
1373
- transientTab: runnersTab,
1374
- order: 20
1375
- });
1376
- //#endregion
1377
- //#region src/extensions/workspaces/sessions/(frontend)/channel/runner-runs.web.ts
1378
- var runner_runs_web_default = defineChannelFile(runnerRunsChannel);
1379
- //#endregion
1380
1214
  //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/RunnersActivitySection.tsx
1381
1215
  const TICK_MS = 1e4;
1382
1216
  /**