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

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 (67) hide show
  1. package/dist/apiContracts.cjs +8 -1
  2. package/dist/apiContracts.cjs.map +1 -1
  3. package/dist/apiContracts.d.mts.map +1 -1
  4. package/dist/apiContracts.mjs +8 -1
  5. package/dist/apiContracts.mjs.map +1 -1
  6. package/dist/{bashRunService-Ci0cGdYO.d.mts → bashRunService-BL4JIUi2.d.mts} +6 -1
  7. package/dist/bashRunService-BL4JIUi2.d.mts.map +1 -0
  8. package/dist/{bashRunService-DjOH5kye.cjs → bashRunService-CJhMNrPz.cjs} +8 -4
  9. package/dist/bashRunService-CJhMNrPz.cjs.map +1 -0
  10. package/dist/{bashRunService-Cl3pYccr.mjs → bashRunService-DbYsj1zc.mjs} +8 -4
  11. package/dist/bashRunService-DbYsj1zc.mjs.map +1 -0
  12. package/dist/bashRunService.cjs +1 -1
  13. package/dist/bashRunService.d.mts +1 -1
  14. package/dist/bashRunService.d.mts.map +1 -1
  15. package/dist/bashRunService.mjs +1 -1
  16. package/dist/bashTool.cjs +1 -1
  17. package/dist/bashTool.d.mts +2 -2
  18. package/dist/bashTool.mjs +1 -1
  19. package/dist/bin/cli.cjs +1 -1
  20. package/dist/bin/cli.mjs +1 -1
  21. package/dist/extensions/mcp.cjs +1 -0
  22. package/dist/extensions/mcp.cjs.map +1 -1
  23. package/dist/extensions/mcp.mjs +1 -0
  24. package/dist/extensions/mcp.mjs.map +1 -1
  25. package/dist/extensions/native.mjs +533 -256
  26. package/dist/extensions/native.mjs.map +1 -1
  27. package/dist/extensions/server.cjs +24 -6
  28. package/dist/extensions/server.cjs.map +1 -1
  29. package/dist/extensions/server.mjs +23 -5
  30. package/dist/extensions/server.mjs.map +1 -1
  31. package/dist/extensions/web.mjs +459 -517
  32. package/dist/extensions/web.mjs.map +1 -1
  33. package/dist/{headless-Dko_yBiy.cjs → headless-BqYKFHwy.cjs} +31 -6
  34. package/dist/headless-BqYKFHwy.cjs.map +1 -0
  35. package/dist/{headless-DOlCixyf.mjs → headless-CV-eb5rz.mjs} +31 -6
  36. package/dist/headless-CV-eb5rz.mjs.map +1 -0
  37. package/dist/{index-BzeXJk6V.d.mts → index-BciAgV5f.d.mts} +11 -4
  38. package/dist/index-BciAgV5f.d.mts.map +1 -0
  39. package/dist/{index--xF7DvsE.d.mts → index-CXbcmf-G.d.mts} +2 -2
  40. package/dist/{index--xF7DvsE.d.mts.map → index-CXbcmf-G.d.mts.map} +1 -1
  41. package/dist/index.cjs +4 -4
  42. package/dist/index.d.mts +2 -2
  43. package/dist/index.mjs +2 -2
  44. package/dist/responseEnvelope.d.mts +1 -1
  45. package/dist/{rmuxBackend-Dh0CYVAL.cjs → rmuxBackend-Cod2H85V.cjs} +2 -2
  46. package/dist/{rmuxBackend-Dh0CYVAL.cjs.map → rmuxBackend-Cod2H85V.cjs.map} +1 -1
  47. package/dist/rmuxBackend.cjs +1 -1
  48. package/dist/{rtkProcessor-DSGbM-Cm.cjs → rtkProcessor-xwARIxhe.cjs} +2 -2
  49. package/dist/{rtkProcessor-DSGbM-Cm.cjs.map → rtkProcessor-xwARIxhe.cjs.map} +1 -1
  50. package/dist/rtkProcessor.cjs +1 -1
  51. package/dist/{runnerDependencies-7h_cxOva.mjs → runnerDependencies-CamKwphq.mjs} +2 -2
  52. package/dist/{runnerDependencies-7h_cxOva.mjs.map → runnerDependencies-CamKwphq.mjs.map} +1 -1
  53. package/dist/{runnerDependencies-DSun3xO-.cjs → runnerDependencies-HIqMQp9b.cjs} +4 -4
  54. package/dist/{runnerDependencies-DSun3xO-.cjs.map → runnerDependencies-HIqMQp9b.cjs.map} +1 -1
  55. package/dist/runnerDependencies.cjs +1 -1
  56. package/dist/runnerDependencies.d.mts +1 -1
  57. package/dist/runnerDependencies.mjs +1 -1
  58. package/dist/{type-Do0YxwVQ.d.mts → type-BQ2shEB5.d.mts} +2 -2
  59. package/dist/{type-Do0YxwVQ.d.mts.map → type-BQ2shEB5.d.mts.map} +1 -1
  60. package/dist/types.d.mts +1 -1
  61. package/package.json +17 -17
  62. package/dist/bashRunService-Ci0cGdYO.d.mts.map +0 -1
  63. package/dist/bashRunService-Cl3pYccr.mjs.map +0 -1
  64. package/dist/bashRunService-DjOH5kye.cjs.map +0 -1
  65. package/dist/headless-DOlCixyf.mjs.map +0 -1
  66. package/dist/headless-Dko_yBiy.cjs.map +0 -1
  67. package/dist/index-BzeXJk6V.d.mts.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 { AlertIcon, AnsiLine, AnsiText, ArrowRightIcon, Button, ChevronDownIcon, Dot, EmptyState, FileIcon, FolderIcon, Input, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, PickerContent, PlayCircleIcon, Popover, PopoverTrigger, 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
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  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
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";
@@ -163,151 +114,233 @@ const runnerRunsChannel = runners.channel({
163
114
  channel: RUNNER_RUNS_TYPE,
164
115
  parse(input) {
165
116
  if (!isRecord$3(input) || !Array.isArray(input.runs)) return null;
166
- return { runs: input.runs.filter(isRecord$3) };
117
+ const failure = input.startFailure;
118
+ return {
119
+ runs: input.runs.filter(isRecord$3),
120
+ ...typeof input.sessionDirectory === "string" ? { sessionDirectory: input.sessionDirectory } : {},
121
+ ...isRecord$3(failure) && typeof failure.id === "string" && typeof failure.message === "string" ? { startFailure: {
122
+ id: failure.id,
123
+ message: failure.message
124
+ } } : {}
125
+ };
167
126
  },
168
- reduce(current, { runs }) {
169
- const stillRunning = new Set(runs.filter((run) => run.state === "running").map((run) => run.id));
127
+ reduce(current, payload) {
128
+ const stillRunning = new Set(payload.runs.filter((run) => run.state === "running").map((run) => run.id));
170
129
  return {
171
- runs,
130
+ ...payload,
172
131
  stopRequested: current.stopRequested.filter((id) => stillRunning.has(id))
173
132
  };
174
133
  }
175
134
  });
176
135
  //#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
- });
136
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/format.ts
137
+ /**
138
+ * Local copy of the runtime's formatUptime (src/services/bashResult/index.ts):
139
+ * the web plugin may reach only src/types, so the one helper it needs lives here.
140
+ */
141
+ function formatRunnerUptime(startedAt, now) {
142
+ const started = Date.parse(startedAt);
143
+ if (Number.isNaN(started)) return "unknown";
144
+ const seconds = Math.max(0, Math.floor((now - started) / 1e3));
145
+ if (seconds < 60) return `${seconds}s`;
146
+ const minutes = Math.floor(seconds / 60);
147
+ if (minutes < 60) return `${minutes}m`;
148
+ const hours = Math.floor(minutes / 60);
149
+ return hours < 24 ? `${hours}h` : `${Math.floor(hours / 24)}d`;
185
150
  }
186
151
  /**
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.
152
+ * The lines a log view shows: the slice's text, then whatever the follow
153
+ * stream appended, bounded to the newest `max`.
190
154
  *
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.
155
+ * A log file ends with a newline, and the reader hands its text back with that
156
+ * newline intact, so a naive split reports one blank line more than the file
157
+ * has. Dropping exactly one trailing empty entry keeps the count the reader
158
+ * sees equal to the count the server reported.
194
159
  */
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
160
+ function logViewLines(text, appended, max, options = {}) {
161
+ const split = text.split("\n");
162
+ if (split.length > 0 && split[split.length - 1] === "") split.pop();
163
+ if (options.dropPartialTail === true && split.length > 0) split.pop();
164
+ const all = [...split, ...appended];
165
+ const lines = all.slice(-max);
166
+ return {
167
+ lines,
168
+ hidden: all.length - lines.length
205
169
  };
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();
170
+ }
171
+ /**
172
+ * Absolute 1-based numbers for a tail, counted back from the last line.
173
+ *
174
+ * A tail is contiguous, so only its end has to be known. Grep results are not
175
+ * contiguous and carry their own numbers from the reader instead.
176
+ */
177
+ function tailLineNumbers(lastLineNumber, count) {
178
+ const first = Math.max(1, lastLineNumber - count + 1);
179
+ return Array.from({ length: count }, (_, index) => first + index);
180
+ }
181
+ /**
182
+ * Whether the view is actually following a growing log.
183
+ *
184
+ * Three things must hold: the reader asked to follow, no query is set (a
185
+ * filtered view is a snapshot of the whole file, which a stream cannot
186
+ * assemble a chunk at a time), and the runner is still writing. Without the
187
+ * last one a finished run would claim to be tailing a file nothing will ever
188
+ * append to.
189
+ */
190
+ function isFollowingLive(following, filtering, running) {
191
+ return following && !filtering && running;
192
+ }
193
+ /**
194
+ * A folder as the command bar names it: the session directory by its last
195
+ * segment, a folder inside it relative to it, anything else as written.
196
+ */
197
+ function runnerFolderLabel(folder, sessionDirectory) {
198
+ if (folder === "" || folder === sessionDirectory) return sessionDirectory === void 0 ? "session directory" : sessionDirectory.split("/").pop() ?? sessionDirectory;
199
+ if (sessionDirectory !== void 0 && folder.startsWith(`${sessionDirectory}/`)) return folder.slice(sessionDirectory.length + 1);
200
+ return folder;
201
+ }
202
+ /** The folders the picker offers past the session directory: those in use, then the one in effect. */
203
+ function runnerFolders(inUse, current, sessionDirectory) {
204
+ const folders = /* @__PURE__ */ new Set([...inUse, current]);
205
+ folders.delete("");
206
+ if (sessionDirectory !== void 0) folders.delete(sessionDirectory);
207
+ return [...folders];
208
+ }
209
+ /** The badge a run wears: its state while running, then how it ended. */
210
+ function runnerBadge(run) {
211
+ if (run === void 0) return {
212
+ tone: "neutral",
213
+ label: "gone"
212
214
  };
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,
215
+ if (run.state === "running") return {
216
+ tone: "running",
217
+ label: "running"
218
+ };
219
+ if (run.exit === void 0 || run.exit.reason === "stopped") return {
220
+ tone: "neutral",
221
+ label: "stopped"
222
+ };
223
+ return run.exit.reason === "completed" && (run.exit.code === 0 || run.exit.code === null) ? {
224
+ tone: "ok",
225
+ label: "done"
226
+ } : {
227
+ tone: "error",
228
+ label: run.exit.reason.replace("_", " ")
229
+ };
230
+ }
231
+ //#endregion
232
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnerCommandBar.tsx
233
+ /**
234
+ * The one place a runner starts: a command, the folder it runs in, and Run.
235
+ * An empty command opens a plain shell in that folder instead.
236
+ */
237
+ function RunnerCommandBar({ command, onCommandChange, cwd, onCwdChange, sessionDirectory, folders, problem, onLaunch, defaultFolderOpen = false }) {
238
+ const [folderOpen, setFolderOpen] = useState(defaultFolderOpen);
239
+ const [path, setPath] = useState("");
240
+ const label = runnerFolderLabel(cwd, sessionDirectory);
241
+ const pick = (next) => {
242
+ onCwdChange(next);
243
+ setPath("");
244
+ setFolderOpen(false);
245
+ };
246
+ const items = [{
247
+ value: "",
248
+ title: "Session directory",
249
+ subtitle: sessionDirectory ?? "Where this session started"
250
+ }, ...runnerFolders(folders, cwd, sessionDirectory).map((folder) => ({
251
+ value: folder,
252
+ title: runnerFolderLabel(folder, sessionDirectory),
253
+ subtitle: folder
254
+ }))];
255
+ return /* @__PURE__ */ jsxs("div", {
256
+ className: "flex shrink-0 flex-col gap-1.5 border-b border-po-border-soft px-3 py-3 sm:px-[26px]",
257
+ children: [/* @__PURE__ */ jsxs("div", {
258
+ className: "flex flex-wrap items-center gap-2 sm:flex-nowrap",
259
+ children: [
260
+ /* @__PURE__ */ jsxs("span", {
261
+ className: "relative flex min-w-0 flex-1 basis-full items-center sm:basis-auto",
262
+ children: [/* @__PURE__ */ jsx("span", {
263
+ "aria-hidden": true,
264
+ className: "pointer-events-none absolute left-3 font-mono text-base text-po-faint",
265
+ children: "$"
266
+ }), /* @__PURE__ */ jsx(Input, {
267
+ size: "lg",
268
+ "data-testid": "runners-panel-command",
269
+ "aria-label": "Command to run",
231
270
  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
271
+ placeholder: "a command, like pnpm dev",
272
+ autoComplete: "off",
273
+ spellCheck: false,
274
+ onChange: (event) => onCommandChange(event.target.value),
275
+ onKeyDown: (event) => {
276
+ if (event.key === "Enter") onLaunch();
277
+ },
278
+ className: "w-full pl-7 font-mono"
277
279
  })]
278
280
  }),
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, {
281
+ /* @__PURE__ */ jsxs(Popover, {
282
+ open: folderOpen,
283
+ onOpenChange: setFolderOpen,
284
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
285
+ asChild: true,
286
+ children: /* @__PURE__ */ jsxs(Button, {
293
287
  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"
288
+ size: "lg",
289
+ "data-testid": "runners-panel-cwd",
290
+ "aria-label": `Runs in ${label}`,
291
+ "aria-invalid": problem === void 0 ? void 0 : true,
292
+ title: cwd === "" ? sessionDirectory : cwd,
293
+ className: `max-w-56 gap-1.5 px-3 font-mono ${problem === void 0 ? "" : "border-po-red"}`,
294
+ children: [
295
+ /* @__PURE__ */ jsx(FolderIcon, { className: "h-3.5 w-3.5 shrink-0 text-po-faint" }),
296
+ /* @__PURE__ */ jsx("span", {
297
+ className: "truncate",
298
+ children: label
299
+ }),
300
+ /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3.5 shrink-0 opacity-60" })
301
+ ]
302
+ })
303
+ }), /* @__PURE__ */ jsx(PickerContent, {
304
+ side: "bottom",
305
+ align: "start",
306
+ title: "Runs in",
307
+ items,
308
+ value: [cwd],
309
+ onSelect: pick,
310
+ footer: /* @__PURE__ */ jsxs("span", {
311
+ className: "relative flex items-center px-3 py-2",
312
+ children: [/* @__PURE__ */ jsx(FolderIcon, { className: "pointer-events-none absolute left-5.5 h-3.5 w-3.5 text-po-faint" }), /* @__PURE__ */ jsx(Input, {
313
+ "data-testid": "runners-panel-cwd-path",
314
+ "aria-label": "Another folder",
315
+ value: path,
316
+ placeholder: "another folder, like packages/web",
317
+ spellCheck: false,
318
+ onChange: (event) => setPath(event.target.value),
319
+ onKeyDown: (event) => {
320
+ if (event.key === "Enter" && path.trim() !== "") pick(path.trim());
321
+ },
322
+ className: "w-full pl-8 font-mono"
323
+ })]
306
324
  })
307
- ]
325
+ })]
326
+ }),
327
+ /* @__PURE__ */ jsx(Button, {
328
+ variant: "primary",
329
+ size: "lg",
330
+ "data-testid": "runners-panel-run",
331
+ onClick: onLaunch,
332
+ children: command.trim() === "" ? "Open shell" : "Run"
308
333
  })
309
- ] })]
310
- })
334
+ ]
335
+ }), problem === void 0 ? /* @__PURE__ */ jsx("span", {
336
+ className: "text-sm text-po-dim",
337
+ children: "Runs in its own terminal and keeps going after Po's turn ends. Leave it empty for a plain shell."
338
+ }) : /* @__PURE__ */ jsxs("span", {
339
+ role: "alert",
340
+ "data-testid": "runners-panel-problem",
341
+ className: "flex items-center gap-1.5 text-sm text-po-red",
342
+ children: [/* @__PURE__ */ jsx(AlertIcon, { className: "size-3.5 shrink-0" }), problem]
343
+ })]
311
344
  });
312
345
  }
313
346
  //#endregion
@@ -496,9 +529,9 @@ function followRunnerLog(sessionId, runId, from, handlers) {
496
529
  }
497
530
  //#endregion
498
531
  //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnerLogPanel.tsx
499
- const TICK_MS$2 = 1e4;
532
+ const TICK_MS$1 = 1e4;
500
533
  /** The tab id doubles as the URL segment, so it stays plain and unique across plugins. */
501
- const TAB_ID_PREFIX$1 = "runner-log-";
534
+ const TAB_ID_PREFIX = "runner-log-";
502
535
  /** Lines held in the view; a follow that ran all day must not grow without bound. */
503
536
  const MAX_VIEW_LINES = 2e3;
504
537
  /** Context lines each side of a match, the same either-side window grep would use. */
@@ -513,7 +546,7 @@ const BYTES_PER_MB$1 = BYTES_PER_KB$1 * BYTES_PER_KB$1;
513
546
  function runnerLogTab(run) {
514
547
  const runId = run.id;
515
548
  return {
516
- id: `${TAB_ID_PREFIX$1}${runId}`,
549
+ id: `${TAB_ID_PREFIX}${runId}`,
517
550
  label: `log · ${run.name}`,
518
551
  panel: (props) => /* @__PURE__ */ jsx(RunnerLogPanel, {
519
552
  ...props,
@@ -526,45 +559,11 @@ function formatSize$1(bytes) {
526
559
  if (bytes < BYTES_PER_MB$1) return `${(bytes / BYTES_PER_KB$1).toFixed(1)} KB`;
527
560
  return `${(bytes / BYTES_PER_MB$1).toFixed(1)} MB`;
528
561
  }
529
- function badgeOf(run) {
530
- if (run === void 0) return {
531
- tone: "neutral",
532
- label: "gone"
533
- };
534
- if (run.state === "running") return {
535
- tone: "running",
536
- label: "running"
537
- };
538
- if (run.exit === void 0 || run.exit.reason === "stopped") return {
539
- tone: "neutral",
540
- label: "stopped"
541
- };
542
- return run.exit.reason === "completed" && (run.exit.code === 0 || run.exit.code === null) ? {
543
- tone: "ok",
544
- label: "done"
545
- } : {
546
- tone: "error",
547
- label: run.exit.reason.replace("_", " ")
548
- };
549
- }
550
562
  /** Whether one line is a match rather than the context printed around one. */
551
563
  function isMatch(line, needle, ignoreCase) {
552
564
  if (needle === "") return false;
553
565
  return ignoreCase ? line.toLowerCase().includes(needle.toLowerCase()) : line.includes(needle);
554
566
  }
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
567
  /**
569
568
  * One rendered log line: its number, whether it matched, and the text.
570
569
  *
@@ -573,24 +572,24 @@ function MetaRow({ label, value }) {
573
572
  * keyed on the joined string; keyed by line number instead, an append only
574
573
  * costs the line that appended.
575
574
  */
576
- function LogRow({ number, line, matched }) {
575
+ function LogRow({ number, line, matched, dimmed }) {
577
576
  return /* @__PURE__ */ jsxs("div", {
578
577
  "data-testid": "runner-log-row",
579
578
  "data-matched": matched,
580
579
  className: "flex min-w-0 gap-2",
581
580
  children: [
582
581
  number === void 0 ? null : /* @__PURE__ */ jsx("span", {
583
- className: "w-12 shrink-0 select-none text-right text-xs leading-relaxed text-po-faint",
582
+ className: "w-12 shrink-0 select-none text-right font-mono text-sm leading-relaxed text-po-faint",
584
583
  children: number
585
584
  }),
586
585
  /* @__PURE__ */ jsx("span", {
587
586
  "aria-hidden": true,
588
- className: `w-4 shrink-0 select-none text-xs leading-relaxed ${matched ? "text-po-yellow" : "text-po-faint"}`,
587
+ className: `w-4 shrink-0 select-none font-mono text-sm leading-relaxed ${matched ? "text-po-yellow" : "text-po-faint"}`,
589
588
  children: matched ? ">>" : ""
590
589
  }),
591
590
  /* @__PURE__ */ jsx(AnsiLine, {
592
591
  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"}`
592
+ 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
593
  })
595
594
  ]
596
595
  });
@@ -604,7 +603,7 @@ function LogRow({ number, line, matched }) {
604
603
  * rather than moving a cursor through it, and following pauses while one is
605
604
  * set because a filtered view is a snapshot of a file that is still growing.
606
605
  */
607
- function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
606
+ function RunnerLogPanel({ sessionId, runId, sendSessionFrame, onTerminal }) {
608
607
  const { runs, stopRequested } = useStore(runners.store, (state) => runners.select(state, sessionId));
609
608
  const run = runs.find((candidate) => candidate.id === runId);
610
609
  const [now, setNow] = useState(() => Date.now());
@@ -624,7 +623,7 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
624
623
  const filtering = query !== "";
625
624
  const live = isFollowingLive(following, filtering, slice?.running === true);
626
625
  useEffect(() => {
627
- const timer = setInterval(() => setNow(Date.now()), TICK_MS$2);
626
+ const timer = setInterval(() => setNow(Date.now()), TICK_MS$1);
628
627
  return () => clearInterval(timer);
629
628
  }, []);
630
629
  useEffect(() => {
@@ -703,7 +702,7 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
703
702
  if (element === null) return;
704
703
  setPinned(element.scrollHeight - element.scrollTop - element.clientHeight <= NEAR_BOTTOM_PX);
705
704
  };
706
- const badge = badgeOf(run);
705
+ const badge = runnerBadge(run);
707
706
  const stopping = run !== void 0 && stopRequested.includes(run.id);
708
707
  const elapsed = run === void 0 ? "" : formatRunnerUptime(run.startedAt, run.exit ? Date.parse(run.exit.finishedAt) : now);
709
708
  return /* @__PURE__ */ jsxs("div", {
@@ -720,6 +719,7 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
720
719
  }),
721
720
  /* @__PURE__ */ jsx(StatusBadge, {
722
721
  tone: badge.tone,
722
+ size: "md",
723
723
  "data-testid": "runner-log-state",
724
724
  children: badge.label
725
725
  }),
@@ -735,60 +735,71 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
735
735
  children: "no longer listed; its log stays readable"
736
736
  }),
737
737
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
738
+ onTerminal ? /* @__PURE__ */ jsx(Button, {
739
+ variant: "outline",
740
+ size: "sm",
741
+ "data-testid": "runner-log-terminal",
742
+ onClick: onTerminal,
743
+ children: "Back to terminal"
744
+ }) : null,
738
745
  run !== void 0 && run.state === "running" && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
739
746
  variant: stopping ? "outline" : "danger-outline",
740
- size: "xs",
747
+ size: "sm",
741
748
  "data-testid": "runner-log-stop",
742
749
  disabled: stopping,
743
750
  title: stopping ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
744
751
  onClick: () => requestRunnerStop(sendSessionFrame, sessionId, run.id),
745
- className: "px-2 ",
746
- children: stopping ? "stopping…" : "stop"
752
+ children: stopping ? "Stopping…" : "Stop"
747
753
  }) : null
748
754
  ]
749
755
  }),
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
- })]
756
+ run ? /* @__PURE__ */ jsx("div", {
757
+ className: "shrink-0 border-b border-po-border-soft px-3 py-3 sm:px-[26px]",
758
+ children: /* @__PURE__ */ jsxs("div", {
759
+ className: "flex flex-col gap-1 rounded-md bg-po-deep px-3 py-2",
760
+ children: [/* @__PURE__ */ jsxs("span", {
761
+ className: "break-all font-mono text-base text-po-hi",
762
+ children: [/* @__PURE__ */ jsxs("span", {
763
+ "aria-hidden": true,
764
+ className: "select-none text-po-faint",
765
+ children: ["$", " "]
766
+ }), run.command]
767
+ }), /* @__PURE__ */ jsxs("span", {
768
+ className: "break-all font-mono text-sm text-po-dim",
769
+ children: ["in ", run.cwd]
770
+ })]
771
+ })
759
772
  }) : null,
760
773
  /* @__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",
774
+ 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
775
  children: [
763
776
  /* @__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",
777
+ className: "relative flex min-w-0 flex-1 items-center sm:w-72 sm:flex-none",
778
+ children: [/* @__PURE__ */ jsx(SearchIcon, { className: "pointer-events-none absolute left-2.5 h-3.5 w-3.5 text-po-faint" }), /* @__PURE__ */ jsx(Input, {
779
+ size: "md",
767
780
  "data-testid": "runner-log-search",
768
781
  value: typed,
769
- placeholder: "search this log",
782
+ placeholder: "Search output",
770
783
  onChange: (event) => setTyped(event.target.value),
771
- className: "pl-7 "
784
+ className: "w-full pl-8"
772
785
  })]
773
786
  }),
774
787
  /* @__PURE__ */ jsx(Button, {
775
788
  variant: ignoreCase ? "subtle" : "outline",
776
- size: "xs",
789
+ size: "sm",
777
790
  "data-testid": "runner-log-ignore-case",
778
791
  "aria-pressed": ignoreCase,
779
792
  title: "match regardless of case",
780
793
  onClick: () => setIgnoreCase((current) => !current),
781
- className: "px-2 ",
782
- children: "aA"
794
+ children: ignoreCase ? "Any case" : "Match case"
783
795
  }),
784
796
  /* @__PURE__ */ jsx(Button, {
785
797
  variant: context > 0 && filtering ? "subtle" : "outline",
786
- size: "xs",
798
+ size: "sm",
787
799
  "data-testid": "runner-log-context",
788
800
  disabled: !filtering,
789
801
  title: "lines of context kept either side of a match",
790
802
  onClick: () => setContext((current) => current === 0 ? CONTEXT_LINES : 0),
791
- className: "px-2 ",
792
803
  children: `± ${String(context)} lines`
793
804
  }),
794
805
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
@@ -799,14 +810,13 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
799
810
  }),
800
811
  /* @__PURE__ */ jsx(Button, {
801
812
  variant: live ? "subtle" : "outline",
802
- size: "xs",
813
+ size: "sm",
803
814
  "data-testid": "runner-log-follow",
804
815
  "aria-pressed": live,
805
816
  disabled: filtering || slice?.running !== true,
806
817
  title: filtering ? "a filtered view is a snapshot; clear the query to follow" : "follow the log as it grows",
807
818
  onClick: () => setFollowing((current) => !current),
808
- className: "px-2 ",
809
- children: filtering ? "follow · paused" : "follow"
819
+ children: filtering ? "Paused while searching" : live ? "Following" : "Follow"
810
820
  })
811
821
  ]
812
822
  }),
@@ -819,7 +829,7 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
819
829
  className: "h-full overflow-auto bg-po-deep px-3 py-3 sm:px-[26px]",
820
830
  children: [error !== void 0 ? /* @__PURE__ */ jsx("p", {
821
831
  "data-testid": "runner-log-error",
822
- className: "text-base text-po-red",
832
+ className: "text-sm text-po-red",
823
833
  children: error
824
834
  }) : slice === void 0 ? /* @__PURE__ */ jsx("p", {
825
835
  "data-testid": "runner-log-reading",
@@ -827,10 +837,10 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
827
837
  children: "reading…"
828
838
  }) : slice.exists === false ? /* @__PURE__ */ jsx("p", {
829
839
  className: "text-sm text-po-dim",
830
- children: "this runner has not written a log yet"
840
+ children: "This runner has not printed anything yet."
831
841
  }) : view.lines.length === 0 ? /* @__PURE__ */ jsx("p", {
832
842
  className: "text-sm text-po-dim",
833
- children: filtering ? "nothing matched" : "the log is empty"
843
+ children: filtering ? "Nothing matched." : "The log is empty."
834
844
  }) : view.lines.map((line, index) => {
835
845
  const number = numbers[index];
836
846
  const previous = numbers[index - 1];
@@ -838,25 +848,26 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
838
848
  return /* @__PURE__ */ jsxs(Fragment, { children: [gap ? /* @__PURE__ */ jsx("div", {
839
849
  "data-testid": "runner-log-gap",
840
850
  "aria-hidden": true,
841
- className: "select-none py-0.5 pl-12 text-xs leading-relaxed text-po-faint",
851
+ className: "select-none py-0.5 pl-14 text-sm leading-relaxed text-po-faint",
842
852
  children: "⋯"
843
853
  }) : null, /* @__PURE__ */ jsx(LogRow, {
844
854
  number,
845
855
  line,
846
- matched: filtering && isMatch(line, query, ignoreCase)
856
+ matched: filtering && isMatch(line, query, ignoreCase),
857
+ dimmed: filtering && !isMatch(line, query, ignoreCase)
847
858
  })] }, number ?? `row-${String(index)}`);
848
859
  }), /* @__PURE__ */ jsx("div", { ref: bottom })]
849
860
  }), live && !pinned ? /* @__PURE__ */ jsx(Button, {
850
861
  variant: "subtle",
851
- size: "xs",
862
+ size: "sm",
852
863
  "data-testid": "runner-log-jump",
853
864
  title: "scroll back to the newest lines and keep following",
854
865
  onClick: () => {
855
866
  setPinned(true);
856
867
  bottom.current?.scrollIntoView({ block: "end" });
857
868
  },
858
- className: "absolute bottom-3 right-3 px-2 ",
859
- children: "jump to latest"
869
+ className: "absolute bottom-3 right-3",
870
+ children: "Jump to latest"
860
871
  }) : null]
861
872
  }),
862
873
  /* @__PURE__ */ jsxs("div", {
@@ -955,21 +966,8 @@ async function sendRunnerInput(sessionId, runId, text) {
955
966
  }
956
967
  //#endregion
957
968
  //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnerShellPanel.tsx
958
- const TAB_ID_PREFIX = "runner-shell-";
959
969
  /** Keystrokes are gathered for this long and sent as one request. */
960
970
  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
971
  /**
974
972
  * An interactive runner's pane, attached.
975
973
  *
@@ -981,9 +979,13 @@ function runnerShellTab(run) {
981
979
  *
982
980
  * xterm is loaded when the tab opens rather than with the plugin. Most sessions
983
981
  * never attach to anything, and an emulator is not a small thing to carry.
982
+ *
983
+ * Search reads the scrubbed log the sink writes beside the raw bytes, so it
984
+ * hands over to the log view: `onSearch` when the panel is embedded, the
985
+ * runner's log tab otherwise.
984
986
  */
985
- function RunnerShellPanel({ sessionId, runId }) {
986
- const { runs } = useStore(runners.store, (state) => runners.select(state, sessionId));
987
+ function RunnerShellPanel({ sessionId, runId, sendSessionFrame, openTransientTab, onSearch }) {
988
+ const { runs, stopRequested } = useStore(runners.store, (state) => runners.select(state, sessionId));
987
989
  const run = runs.find((candidate) => candidate.id === runId);
988
990
  const [ended, setEnded] = useState(false);
989
991
  const [lost, setLost] = useState(false);
@@ -1021,12 +1023,13 @@ function RunnerShellPanel({ sessionId, runId }) {
1021
1023
  }, FLUSH_MS);
1022
1024
  };
1023
1025
  const live = run?.state === "running" && !ended;
1026
+ const stopping = stopRequested.includes(runId);
1024
1027
  return /* @__PURE__ */ jsxs("div", {
1025
1028
  "data-testid": "runner-shell-panel",
1026
1029
  className: "flex min-h-0 flex-1 flex-col",
1027
1030
  children: [
1028
1031
  /* @__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]",
1032
+ 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
1033
  children: [
1031
1034
  /* @__PURE__ */ jsx("span", {
1032
1035
  "data-testid": "runner-shell-name",
@@ -1035,23 +1038,45 @@ function RunnerShellPanel({ sessionId, runId }) {
1035
1038
  }),
1036
1039
  /* @__PURE__ */ jsx(StatusBadge, {
1037
1040
  tone: live ? "running" : "neutral",
1041
+ size: "md",
1038
1042
  "data-testid": "runner-shell-state",
1039
- children: live ? "attached" : "detached"
1043
+ children: live ? "Running" : "Ended"
1040
1044
  }),
1045
+ run ? /* @__PURE__ */ jsx("span", {
1046
+ className: "min-w-0 truncate font-mono text-sm text-po-dim",
1047
+ children: run.command
1048
+ }) : null,
1041
1049
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
1042
1050
  lost ? /* @__PURE__ */ jsx("span", {
1043
1051
  "data-testid": "runner-shell-lost",
1044
- className: "shrink-0 text-base text-po-red",
1045
- children: "the connection to this pane dropped"
1052
+ className: "shrink-0 text-sm text-po-red",
1053
+ children: "The connection to this terminal dropped. Reopen the runner to attach again."
1054
+ }) : null,
1055
+ run ? /* @__PURE__ */ jsxs(Button, {
1056
+ variant: "outline",
1057
+ size: "sm",
1058
+ "data-testid": "runner-shell-search",
1059
+ title: "search everything this runner has printed",
1060
+ onClick: () => onSearch ? onSearch() : openTransientTab(runnerLogTab(run)),
1061
+ className: "gap-1.5",
1062
+ children: [/* @__PURE__ */ jsx(SearchIcon, { className: "h-3.5 w-3.5" }), "Search output"]
1046
1063
  }) : null,
1047
1064
  live ? /* @__PURE__ */ jsx(Button, {
1048
1065
  variant: "outline",
1049
- size: "xs",
1066
+ size: "sm",
1050
1067
  "data-testid": "runner-shell-interrupt",
1051
1068
  title: "send ctrl-c",
1052
1069
  onClick: () => send(""),
1053
- className: "px-2 ",
1054
- children: "ctrl-c"
1070
+ children: "Ctrl-C"
1071
+ }) : null,
1072
+ live && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
1073
+ variant: stopping ? "outline" : "danger-outline",
1074
+ size: "sm",
1075
+ "data-testid": "runner-shell-stop",
1076
+ disabled: stopping,
1077
+ title: stopping ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
1078
+ onClick: () => requestRunnerStop(sendSessionFrame, sessionId, runId),
1079
+ children: stopping ? "Stopping…" : "Stop"
1055
1080
  }) : null
1056
1081
  ]
1057
1082
  }),
@@ -1072,13 +1097,153 @@ function RunnerShellPanel({ sessionId, runId }) {
1072
1097
  className: "flex h-8 shrink-0 items-center border-t border-po-border-soft px-3 sm:px-[26px]",
1073
1098
  children: /* @__PURE__ */ jsx("span", {
1074
1099
  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"
1100
+ 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
1101
  })
1077
1102
  })
1078
1103
  ]
1079
1104
  });
1080
1105
  }
1081
1106
  //#endregion
1107
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnersPanel.tsx
1108
+ const LAUNCH_TIMEOUT_MS = 3e4;
1109
+ const RUNNERS_TAB_ID = "runner-runs";
1110
+ /** The temporary tab the dock's runners group opens from its own name. */
1111
+ function runnersTab() {
1112
+ return {
1113
+ id: RUNNERS_TAB_ID,
1114
+ label: "runners",
1115
+ panel: RunnersPanel
1116
+ };
1117
+ }
1118
+ /**
1119
+ * Everything this session has up, and the one place to start another.
1120
+ *
1121
+ * A runner is a shell: the bar takes a command and runs it on its own
1122
+ * terminal, or opens a plain shell when nothing was typed. The selected
1123
+ * runner is shown in place, as its terminal when it has one and as its log
1124
+ * when it does not (a command the agent backgrounded has no pane).
1125
+ *
1126
+ * Running runners only, the same rule the rail follows. A finished run leaves
1127
+ * the moment it exits; its log stays readable from the tool message that
1128
+ * started it, and `po-runner list` is where history lives.
1129
+ */
1130
+ function RunnersPanel(props) {
1131
+ const { sessionId, sendSessionFrame } = props;
1132
+ const session = useStore(runners.store, (state) => runners.select(state, sessionId));
1133
+ const running = session.runs.filter((run) => run.state === "running");
1134
+ const [command, setCommand] = useState("");
1135
+ const [cwd, setCwd] = useState("");
1136
+ const [selectedId, setSelectedId] = useState(void 0);
1137
+ /** The terminal runner whose log is being searched instead of shown live. */
1138
+ const [searchingId, setSearchingId] = useState(void 0);
1139
+ const launchBaseline = useRef(null);
1140
+ /** What the pending launch ran, put back if the start fails. */
1141
+ const launchedCommand = useRef("");
1142
+ const launchTimeout = useRef(void 0);
1143
+ const selected = running.find((run) => run.id === selectedId) ?? running[0];
1144
+ const launch = () => {
1145
+ if (sessionId === null) return;
1146
+ launchBaseline.current = new Set(session.runs.map((run) => run.id));
1147
+ clearTimeout(launchTimeout.current);
1148
+ launchTimeout.current = setTimeout(() => {
1149
+ launchBaseline.current = null;
1150
+ }, LAUNCH_TIMEOUT_MS);
1151
+ requestRunnerStart(sendSessionFrame, sessionId, runnerRunRequest(command, cwd));
1152
+ launchedCommand.current = command;
1153
+ setCommand("");
1154
+ };
1155
+ useEffect(() => {
1156
+ const baseline = launchBaseline.current;
1157
+ if (baseline === null) return;
1158
+ const started = session.runs.find((run) => run.state === "running" && !baseline.has(run.id));
1159
+ if (!started) return;
1160
+ launchBaseline.current = null;
1161
+ clearTimeout(launchTimeout.current);
1162
+ setSelectedId(started.id);
1163
+ }, [session.runs]);
1164
+ const failureId = session.startFailure?.id;
1165
+ useEffect(() => {
1166
+ if (failureId === void 0 || launchBaseline.current === null) return;
1167
+ launchBaseline.current = null;
1168
+ clearTimeout(launchTimeout.current);
1169
+ setCommand(launchedCommand.current);
1170
+ }, [failureId]);
1171
+ useEffect(() => () => clearTimeout(launchTimeout.current), []);
1172
+ return /* @__PURE__ */ jsxs("div", {
1173
+ "data-testid": "runners-panel",
1174
+ className: "flex min-h-0 flex-1 flex-col",
1175
+ children: [
1176
+ /* @__PURE__ */ jsxs("div", {
1177
+ className: "flex h-11 shrink-0 items-center gap-2.5 border-b border-po-border-soft px-3 sm:px-[26px]",
1178
+ children: [/* @__PURE__ */ jsx("span", {
1179
+ className: "text-lg font-semibold text-po-hi",
1180
+ children: "Runners"
1181
+ }), /* @__PURE__ */ jsx("span", {
1182
+ "data-testid": "runners-panel-count",
1183
+ className: "text-sm text-po-dim",
1184
+ children: running.length === 0 ? "Nothing running" : `${String(running.length)} running`
1185
+ })]
1186
+ }),
1187
+ sessionId === null ? null : /* @__PURE__ */ jsx(RunnerCommandBar, {
1188
+ command,
1189
+ onCommandChange: setCommand,
1190
+ cwd,
1191
+ onCwdChange: setCwd,
1192
+ folders: running.map((run) => run.cwd),
1193
+ ...session.sessionDirectory === void 0 ? {} : { sessionDirectory: session.sessionDirectory },
1194
+ ...session.startFailure === void 0 ? {} : { problem: session.startFailure.message },
1195
+ onLaunch: launch
1196
+ }),
1197
+ selected === void 0 ? /* @__PURE__ */ jsx(EmptyState, {
1198
+ "data-testid": "runners-panel-empty",
1199
+ className: "px-4 py-8",
1200
+ title: "Nothing running yet",
1201
+ 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."
1202
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
1203
+ "data-testid": "runners-panel-picker",
1204
+ className: "flex shrink-0 flex-wrap gap-1.5 px-3 pt-3 sm:px-[26px]",
1205
+ children: running.map((run) => /* @__PURE__ */ jsxs(Button, {
1206
+ variant: run === selected ? "subtle" : "ghost",
1207
+ size: "sm",
1208
+ "data-testid": `runners-panel-pick-${run.id}`,
1209
+ "aria-pressed": run === selected,
1210
+ title: run.command,
1211
+ onClick: () => setSelectedId(run.id),
1212
+ className: `gap-1.5 ${run === selected ? "text-po-hi" : ""}`,
1213
+ children: [/* @__PURE__ */ jsx(Dot, {
1214
+ tone: "yellow",
1215
+ pulse: true
1216
+ }), run.name]
1217
+ }, run.id))
1218
+ }), /* @__PURE__ */ jsx("div", {
1219
+ className: "mt-3 flex min-h-0 flex-1 flex-col border-t border-po-border-soft",
1220
+ children: selected.interactive && searchingId !== selected.id ? /* @__PURE__ */ jsx(RunnerShellPanel, {
1221
+ ...props,
1222
+ runId: selected.id,
1223
+ onSearch: () => setSearchingId(selected.id)
1224
+ }, selected.id) : /* @__PURE__ */ jsx(RunnerLogPanel, {
1225
+ ...props,
1226
+ runId: selected.id,
1227
+ ...selected.interactive ? { onTerminal: () => setSearchingId(void 0) } : {}
1228
+ }, selected.id)
1229
+ })] })
1230
+ ]
1231
+ });
1232
+ }
1233
+ //#endregion
1234
+ //#region src/extensions/workspaces/sessions/(frontend)/activity-group/runners.web.ts
1235
+ var runners_web_default = defineActivityGroup({
1236
+ keys: "r l",
1237
+ icon: PlayCircleIcon,
1238
+ statusKey: "po-runner-runners",
1239
+ activeSource: runnerActivitySource,
1240
+ transientTab: runnersTab,
1241
+ order: 20
1242
+ });
1243
+ //#endregion
1244
+ //#region src/extensions/workspaces/sessions/(frontend)/channel/runner-runs.web.ts
1245
+ var runner_runs_web_default = defineChannelFile(runnerRunsChannel);
1246
+ //#endregion
1082
1247
  //#region src/extensions/workspaces/sessions/(frontend)/_components/runnerTail.ts
1083
1248
  /**
1084
1249
  * The newest line a running runner has written, for a row that has one line to
@@ -1154,229 +1319,6 @@ function useRunnerTail(sessionId, runId, running) {
1154
1319
  return running && tail?.runId === runId ? tail.text : void 0;
1155
1320
  }
1156
1321
  //#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
1322
  //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/RunnersActivitySection.tsx
1381
1323
  const TICK_MS = 1e4;
1382
1324
  /**