@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,9 +1,10 @@
1
1
  import { createApiClient, defineActivityGroup, defineChannelFile, defineFill, defineNativePlugin, defineSessionStore, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
2
+ import { ArrowRightIcon, Button, Dot, FolderIcon, Input, ListRow, PlayCircleIcon, SearchIcon, Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle, StatusBadge, fontOf } from "@agimon-ai/po-native-components";
3
+ import { createElement, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
4
  import { View } from "react-native-unistyles/components/native/View";
3
5
  import { Text } from "react-native-unistyles/components/native/Text";
4
6
  import { ScrollView } from "react-native-unistyles/components/native/ScrollView";
5
- import { Button, Dot, Input, Label, ListRow, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, Textarea, fontOf } from "@agimon-ai/po-native-components";
6
- import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
7
+ import { Pressable } from "react-native-unistyles/components/native/Pressable";
7
8
  import { StyleSheet } from "react-native-unistyles";
8
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
10
  //#region src/extensions/workspaces/sessions/(frontend)/_lib/format.ts
@@ -51,10 +52,55 @@ function tailLineNumbers(lastLineNumber, count) {
51
52
  const first = Math.max(1, lastLineNumber - count + 1);
52
53
  return Array.from({ length: count }, (_, index) => first + index);
53
54
  }
55
+ /**
56
+ * A folder as the command bar names it: the session directory by its last
57
+ * segment, a folder inside it relative to it, anything else as written.
58
+ */
59
+ function runnerFolderLabel(folder, sessionDirectory) {
60
+ if (folder === "" || folder === sessionDirectory) return sessionDirectory === void 0 ? "session directory" : sessionDirectory.split("/").pop() ?? sessionDirectory;
61
+ if (sessionDirectory !== void 0 && folder.startsWith(`${sessionDirectory}/`)) return folder.slice(sessionDirectory.length + 1);
62
+ return folder;
63
+ }
64
+ /** The folders the picker offers past the session directory: those in use, then the one in effect. */
65
+ function runnerFolders(inUse, current, sessionDirectory) {
66
+ const folders = /* @__PURE__ */ new Set([...inUse, current]);
67
+ folders.delete("");
68
+ if (sessionDirectory !== void 0) folders.delete(sessionDirectory);
69
+ return [...folders];
70
+ }
71
+ /**
72
+ * Undoes iOS smart punctuation, which React Native cannot switch off: a shell
73
+ * reads ‘hi’ as literal quotes and a long dash before port as no flag at all.
74
+ */
75
+ function plainPunctuation(text) {
76
+ return text.replace(/[‘’]/g, "'").replace(/[“”]/g, "\"").replace(/\u2014/g, "--");
77
+ }
78
+ /** The badge a run wears: its state while running, then how it ended. */
79
+ function runnerBadge(run) {
80
+ if (run === void 0) return {
81
+ tone: "neutral",
82
+ label: "gone"
83
+ };
84
+ if (run.state === "running") return {
85
+ tone: "running",
86
+ label: "running"
87
+ };
88
+ if (run.exit === void 0 || run.exit.reason === "stopped") return {
89
+ tone: "neutral",
90
+ label: "stopped"
91
+ };
92
+ return run.exit.reason === "completed" && (run.exit.code === 0 || run.exit.code === null) ? {
93
+ tone: "ok",
94
+ label: "done"
95
+ } : {
96
+ tone: "error",
97
+ label: run.exit.reason.replace("_", " ")
98
+ };
99
+ }
54
100
  //#endregion
55
101
  //#region src/extensions/workspaces/sessions/(frontend)/_lib/launchLine.ts
56
102
  /**
57
- * The `/runners start` line the launch form sends.
103
+ * The `/runners start` line the runners panel's command bar sends.
58
104
  *
59
105
  * WHY THE BUILDER LIVES HERE AND THE PARSER DOES NOT:
60
106
  * Cockpit code is compiled into the host bundle and may import only this
@@ -70,6 +116,21 @@ const RUNNER_SHELL_REQUEST = {
70
116
  name: "shell",
71
117
  interactive: true
72
118
  };
119
+ /**
120
+ * What the runners panel's Run sends: the typed command on a terminal when
121
+ * one is available, or a login shell when nothing was typed.
122
+ */
123
+ function runnerRunRequest(command, cwd) {
124
+ const where = cwd.trim() === "" ? {} : { cwd };
125
+ return command.trim() === "" ? {
126
+ ...RUNNER_SHELL_REQUEST,
127
+ ...where
128
+ } : {
129
+ command,
130
+ ...where,
131
+ preferInteractive: true
132
+ };
133
+ }
73
134
  /** Quotes a value that carries whitespace, which is how a value survives tokenizing. */
74
135
  function quote(value) {
75
136
  return /\s/u.test(value) ? `"${value}"` : value;
@@ -80,7 +141,7 @@ function runnerLaunchLine(request) {
80
141
  const pairs = [
81
142
  ...name === "" ? [] : [`name=${quote(name)}`],
82
143
  ...cwd === "" ? [] : [`cwd=${quote(cwd)}`],
83
- ...request.interactive === true ? ["interactive=true"] : []
144
+ ...request.interactive === true ? ["interactive=true"] : request.preferInteractive === true ? ["interactive=prefer"] : []
84
145
  ];
85
146
  return [
86
147
  RUNNERS_START_LINE,
@@ -89,12 +150,6 @@ function runnerLaunchLine(request) {
89
150
  request.command.trim()
90
151
  ].join(" ");
91
152
  }
92
- /** What the form will not let a reader send, worded the way the session would answer. */
93
- function launchProblems(request) {
94
- const problems = [];
95
- if (request.command.trim() === "") problems.push("A runner needs a command to run.");
96
- return problems;
97
- }
98
153
  //#endregion
99
154
  //#region src/constants/webRunners.ts
100
155
  const RUNNER_RUNS_TYPE = "runner_runs";
@@ -153,122 +208,142 @@ const runnerRunsChannel = runners.channel({
153
208
  channel: RUNNER_RUNS_TYPE,
154
209
  parse(input) {
155
210
  if (!isRecord$1(input) || !Array.isArray(input.runs)) return null;
156
- return { runs: input.runs.filter(isRecord$1) };
211
+ const failure = input.startFailure;
212
+ return {
213
+ runs: input.runs.filter(isRecord$1),
214
+ ...typeof input.sessionDirectory === "string" ? { sessionDirectory: input.sessionDirectory } : {},
215
+ ...isRecord$1(failure) && typeof failure.id === "string" && typeof failure.message === "string" ? { startFailure: {
216
+ id: failure.id,
217
+ message: failure.message
218
+ } } : {}
219
+ };
157
220
  },
158
- reduce(current, { runs }) {
159
- const stillRunning = new Set(runs.filter((run) => run.state === "running").map((run) => run.id));
221
+ reduce(current, payload) {
222
+ const stillRunning = new Set(payload.runs.filter((run) => run.state === "running").map((run) => run.id));
160
223
  return {
161
- runs,
224
+ ...payload,
162
225
  stopRequested: current.stopRequested.filter((id) => stillRunning.has(id))
163
226
  };
164
227
  }
165
228
  });
166
229
  //#endregion
167
- //#region src/extensions/workspaces/sessions/(frontend)/_components/LaunchRunnerSheet.native.tsx
168
- function LaunchRunnerSheet({ sessionId, send, defaultCwd, onClose }) {
169
- const [command, setCommand] = useState("");
170
- const [cwd, setCwd] = useState(defaultCwd ?? "");
171
- const [name, setName] = useState("");
172
- const [interactive, setInteractive] = useState(false);
173
- const request = {
174
- command,
175
- cwd,
176
- name,
177
- interactive
230
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnerCommandBar.native.tsx
231
+ /**
232
+ * The phone's command bar: the command and Run on one line, the folder it runs
233
+ * in on the row under it, chosen from a bottom sheet.
234
+ */
235
+ function RunnerCommandBar({ command, onCommandChange, cwd, onCwdChange, sessionDirectory, folders, problem, onLaunch, defaultFolderOpen = false }) {
236
+ const [folderOpen, setFolderOpen] = useState(defaultFolderOpen);
237
+ const [path, setPath] = useState("");
238
+ const pick = (next) => {
239
+ onCwdChange(next);
240
+ setPath("");
241
+ setFolderOpen(false);
178
242
  };
179
- const problems = launchProblems(request);
180
- return /* @__PURE__ */ jsx(Sheet, {
181
- open: true,
182
- onOpenChange: (open) => {
183
- if (!open) onClose();
184
- },
185
- children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "Launch a runner" }) }), /* @__PURE__ */ jsx(SheetBody, { children: /* @__PURE__ */ jsxs(View, {
186
- style: styles$3.form,
187
- children: [
188
- /* @__PURE__ */ jsx(Label, { children: "Command" }),
189
- /* @__PURE__ */ jsx(Textarea, {
190
- testID: "runner-launch-command",
191
- value: command,
192
- onChangeText: setCommand,
193
- placeholder: "pnpm test"
194
- }),
195
- /* @__PURE__ */ jsx(Label, { children: "Cwd" }),
196
- /* @__PURE__ */ jsx(Input, {
197
- testID: "runner-launch-cwd",
198
- value: cwd,
199
- onChangeText: setCwd,
200
- placeholder: "the session's own directory"
201
- }),
202
- /* @__PURE__ */ jsx(Label, { children: "Name" }),
203
- /* @__PURE__ */ jsx(Input, {
204
- testID: "runner-launch-name",
205
- value: name,
206
- onChangeText: setName,
207
- placeholder: "chosen for you when empty"
208
- }),
209
- /* @__PURE__ */ jsx(Label, { children: "Needs a terminal, for a command that prompts (requires rmux or tmux)" }),
210
- /* @__PURE__ */ jsx(Button, {
211
- testID: "runner-launch-interactive",
212
- variant: "outline",
213
- accessibilityState: { checked: interactive },
214
- onPress: () => setInteractive((value) => !value),
215
- children: interactive ? "Terminal enabled" : "Terminal off"
216
- }),
217
- /* @__PURE__ */ jsx(Label, { children: "Sent to the session" }),
218
- /* @__PURE__ */ jsx(Text, {
219
- selectable: true,
220
- testID: "runner-launch-line",
221
- style: styles$3.line,
222
- children: runnerLaunchLine(request)
223
- }),
224
- problems.length > 0 ? /* @__PURE__ */ jsx(Text, {
225
- style: styles$3.error,
226
- children: problems.join(" ")
227
- }) : null,
228
- /* @__PURE__ */ jsx(Button, {
229
- variant: "outline",
230
- onPress: onClose,
231
- children: "Cancel"
232
- }),
233
- /* @__PURE__ */ jsx(Button, {
234
- testID: "runner-launch-submit",
235
- disabled: problems.length > 0,
236
- onPress: () => {
237
- if (problems.length > 0) return;
238
- requestRunnerStart(send, sessionId, request);
239
- onClose();
240
- },
241
- children: "Launch"
243
+ const choices = ["", ...runnerFolders(folders, cwd, sessionDirectory)];
244
+ return /* @__PURE__ */ jsxs(View, {
245
+ style: styles$3.bar,
246
+ children: [
247
+ /* @__PURE__ */ jsxs(View, {
248
+ style: styles$3.line,
249
+ children: [/* @__PURE__ */ jsx(View, {
250
+ style: styles$3.grow,
251
+ children: /* @__PURE__ */ jsx(Input, {
252
+ testID: "runners-panel-command",
253
+ accessibilityLabel: "Command to run",
254
+ size: "lg",
255
+ value: command,
256
+ onChangeText: (text) => onCommandChange(plainPunctuation(text)),
257
+ onSubmitEditing: onLaunch,
258
+ placeholder: "a command, like pnpm dev",
259
+ autoCapitalize: "none",
260
+ autoCorrect: false,
261
+ returnKeyType: "go"
262
+ })
263
+ }), /* @__PURE__ */ jsx(Button, {
264
+ testID: "runners-panel-run",
265
+ variant: "primary",
266
+ size: "lg",
267
+ onPress: onLaunch,
268
+ children: command.trim() === "" ? "Open shell" : "Run"
269
+ })]
270
+ }),
271
+ /* @__PURE__ */ jsx(ListRow, {
272
+ leading: /* @__PURE__ */ jsx(FolderIcon, {
273
+ size: 14,
274
+ color: problem === void 0 ? "faint" : "red"
242
275
  }),
243
- /* @__PURE__ */ jsx(Text, {
244
- style: styles$3.meta,
245
- children: "It appears above once the runtime starts it."
246
- })
247
- ]
248
- }) })] })
276
+ title: runnerFolderLabel(cwd, sessionDirectory),
277
+ description: "Runs in",
278
+ disclosure: true,
279
+ accessibilityLabel: `Runs in ${runnerFolderLabel(cwd, sessionDirectory)}`,
280
+ onPress: () => setFolderOpen(true)
281
+ }),
282
+ problem === void 0 ? /* @__PURE__ */ jsx(Text, {
283
+ style: styles$3.meta,
284
+ children: "Leave the command empty for a plain shell."
285
+ }) : /* @__PURE__ */ jsx(Text, {
286
+ accessibilityRole: "alert",
287
+ testID: "runners-panel-problem",
288
+ style: styles$3.problem,
289
+ children: problem
290
+ }),
291
+ /* @__PURE__ */ jsx(Sheet, {
292
+ open: folderOpen,
293
+ onOpenChange: setFolderOpen,
294
+ children: /* @__PURE__ */ jsxs(SheetContent, { children: [
295
+ /* @__PURE__ */ jsx(SheetHeader, {
296
+ closeLabel: "Close folders",
297
+ children: /* @__PURE__ */ jsx(SheetTitle, { children: "Runs in" })
298
+ }),
299
+ /* @__PURE__ */ jsxs(SheetBody, { children: [/* @__PURE__ */ jsx(View, { children: choices.map((folder) => /* @__PURE__ */ jsx(ListRow, {
300
+ title: folder === "" ? "Session directory" : runnerFolderLabel(folder, sessionDirectory),
301
+ description: folder === "" ? sessionDirectory ?? "Where this session started" : folder,
302
+ selected: folder === cwd,
303
+ onPress: () => pick(folder)
304
+ }, folder)) }), /* @__PURE__ */ jsx(Input, {
305
+ testID: "runners-panel-cwd-path",
306
+ accessibilityLabel: "Another folder",
307
+ value: path,
308
+ onChangeText: (text) => setPath(plainPunctuation(text)),
309
+ onSubmitEditing: () => {
310
+ if (path.trim() !== "") pick(path.trim());
311
+ },
312
+ placeholder: "another folder, like packages/web",
313
+ autoCapitalize: "none",
314
+ autoCorrect: false,
315
+ returnKeyType: "done"
316
+ })] }),
317
+ /* @__PURE__ */ jsx(SheetFooter, { children: "A relative folder starts from the session directory." })
318
+ ] })
319
+ })
320
+ ]
249
321
  });
250
322
  }
251
323
  const styles$3 = StyleSheet.create((theme) => ({
252
- form: {
253
- gap: theme.spacing(2),
324
+ bar: {
325
+ gap: theme.spacing(1.5),
254
326
  uni__dependencies: [0]
255
327
  },
256
328
  line: {
257
- color: theme.colors.green,
258
- fontFamily: fontOf(theme.fontFamily.mono),
259
- fontSize: theme.fontSize.sm,
329
+ flexDirection: "row",
330
+ alignItems: "center",
331
+ gap: theme.spacing(2),
260
332
  uni__dependencies: [0]
261
333
  },
262
- error: {
263
- color: theme.colors.red,
334
+ grow: { flex: 1 },
335
+ meta: {
336
+ color: theme.colors.dim,
264
337
  fontFamily: fontOf(theme.fontFamily.sans),
265
- fontSize: theme.fontSize.base,
338
+ fontSize: theme.fontSize.sm,
339
+ lineHeight: theme.lineHeight.sm,
266
340
  uni__dependencies: [0]
267
341
  },
268
- meta: {
269
- color: theme.colors.dim,
342
+ problem: {
343
+ color: theme.colors.red,
270
344
  fontFamily: fontOf(theme.fontFamily.sans),
271
345
  fontSize: theme.fontSize.sm,
346
+ lineHeight: theme.lineHeight.sm,
272
347
  uni__dependencies: [0]
273
348
  }
274
349
  }));
@@ -437,6 +512,43 @@ function useRunnerTail(sessionId, runId, running) {
437
512
  }
438
513
  //#endregion
439
514
  //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnerLogPanel.native.tsx
515
+ /** The top of both runner screens: name, outcome, an action, and the command in its folder. */
516
+ function RunnerRunHeader({ run, name, sessionDirectory, action }) {
517
+ const badge = runnerBadge(run);
518
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(View, {
519
+ style: styles$2.header,
520
+ children: [
521
+ /* @__PURE__ */ jsx(Text, {
522
+ numberOfLines: 1,
523
+ style: styles$2.title,
524
+ children: name
525
+ }),
526
+ /* @__PURE__ */ jsx(StatusBadge, {
527
+ tone: badge.tone,
528
+ children: badge.label
529
+ }),
530
+ /* @__PURE__ */ jsx(View, { style: styles$2.grow }),
531
+ action
532
+ ]
533
+ }), run ? /* @__PURE__ */ jsxs(View, {
534
+ style: styles$2.commandBox,
535
+ children: [/* @__PURE__ */ jsxs(Text, {
536
+ selectable: true,
537
+ style: styles$2.command,
538
+ children: [/* @__PURE__ */ jsx(Text, {
539
+ style: styles$2.prompt,
540
+ children: "$ "
541
+ }), run.command]
542
+ }), /* @__PURE__ */ jsxs(Text, {
543
+ numberOfLines: 1,
544
+ style: styles$2.folder,
545
+ children: ["in ", runnerFolderLabel(run.cwd, sessionDirectory)]
546
+ })]
547
+ }) : /* @__PURE__ */ jsx(Text, {
548
+ style: styles$2.meta,
549
+ children: "No longer listed; its log stays readable."
550
+ })] });
551
+ }
440
552
  function runnerLogTab(run) {
441
553
  return {
442
554
  id: `runner-log-${run.id}`,
@@ -501,74 +613,66 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
501
613
  ]);
502
614
  const lines = logViewLines(slice?.text ?? "", [], 2e3).lines;
503
615
  const numbers = query === "" ? tailLineNumbers(slice?.totalLines ?? 0, lines.length) : slice?.lineNumbers ?? [];
616
+ const gutter = String(numbers.at(-1) ?? 0).length;
504
617
  const stopping = session.stopRequested.includes(runId);
618
+ const live = following && query === "" && slice?.running === true;
505
619
  return /* @__PURE__ */ jsxs(View, {
506
620
  testID: "runner-log-panel",
507
621
  style: styles$2.panel,
508
622
  children: [
509
- /* @__PURE__ */ jsx(Text, {
510
- style: styles$2.title,
511
- children: run?.name ?? runId
512
- }),
513
- /* @__PURE__ */ jsx(Text, {
514
- style: styles$2.meta,
515
- children: run?.state ?? "no longer listed; its log stays readable"
623
+ /* @__PURE__ */ jsx(RunnerRunHeader, {
624
+ run,
625
+ name: run?.name ?? runId,
626
+ sessionDirectory: session.sessionDirectory,
627
+ action: run?.state === "running" && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
628
+ variant: stopping ? "outline" : "danger-outline",
629
+ size: "sm",
630
+ disabled: stopping,
631
+ onPress: () => requestRunnerStop(sendSessionFrame, sessionId, runId),
632
+ children: stopping ? "Stopping…" : "Stop"
633
+ }) : null
516
634
  }),
517
- run ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, {
518
- selectable: true,
519
- style: styles$2.meta,
520
- children: run.command
521
- }), /* @__PURE__ */ jsx(Text, {
522
- selectable: true,
523
- style: styles$2.meta,
524
- children: run.cwd
525
- })] }) : null,
526
- run?.state === "running" && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
527
- variant: "danger-outline",
528
- disabled: stopping,
529
- onPress: () => requestRunnerStop(sendSessionFrame, sessionId, runId),
530
- children: stopping ? "Stopping…" : "Stop"
531
- }) : null,
532
- /* @__PURE__ */ jsx(Label, { children: "Search this log" }),
533
635
  /* @__PURE__ */ jsx(Input, {
534
636
  testID: "runner-log-search",
637
+ accessibilityLabel: "Search this log",
638
+ placeholder: "Search output",
535
639
  value: typed,
536
- onChangeText: setTyped
640
+ onChangeText: setTyped,
641
+ autoCapitalize: "none",
642
+ returnKeyType: "search"
537
643
  }),
538
644
  /* @__PURE__ */ jsxs(View, {
539
645
  style: styles$2.actions,
540
646
  children: [
541
647
  /* @__PURE__ */ jsx(Button, {
542
- variant: "outline",
648
+ variant: ignoreCase ? "subtle" : "outline",
649
+ size: "sm",
543
650
  accessibilityState: { selected: ignoreCase },
544
651
  onPress: () => setIgnoreCase((value) => !value),
545
- children: "aA"
652
+ children: ignoreCase ? "Any case" : "Match case"
546
653
  }),
547
- /* @__PURE__ */ jsxs(Button, {
548
- variant: "outline",
549
- disabled: query === "",
654
+ query === "" ? null : /* @__PURE__ */ jsx(Button, {
655
+ variant: context > 0 ? "subtle" : "outline",
656
+ size: "sm",
550
657
  onPress: () => setContext((value) => value === 0 ? 2 : 0),
551
- children: [
552
- "± ",
553
- context,
554
- " lines"
555
- ]
658
+ children: `± ${String(context)} lines`
556
659
  }),
557
- /* @__PURE__ */ jsx(Button, {
558
- variant: "outline",
559
- disabled: query !== "" || slice?.running !== true,
560
- accessibilityState: { selected: following },
660
+ query === "" && slice?.running === true ? /* @__PURE__ */ jsx(Button, {
661
+ variant: live ? "subtle" : "outline",
662
+ size: "sm",
663
+ accessibilityState: { selected: live },
561
664
  onPress: () => setFollowing((value) => !value),
562
- children: query !== "" ? "Follow · paused" : "Follow"
563
- })
665
+ children: live ? "Following" : "Follow"
666
+ }) : null
564
667
  ]
565
668
  }),
566
669
  /* @__PURE__ */ jsx(Text, {
567
670
  style: styles$2.meta,
568
- children: slice === void 0 ? "Reading…" : query !== "" ? `${slice.lineCount} matching of ${slice.totalLines} lines` : `${following && slice.running ? "Tailing" : "Showing"} last ${lines.length} of ${slice.totalLines} lines`
671
+ children: slice === void 0 ? error === "" ? "Reading…" : "" : query !== "" ? `${slice.lineCount} matching of ${slice.totalLines} lines` : `${live ? "Tailing" : "Showing"} last ${lines.length} of ${slice.totalLines} lines`
569
672
  }),
570
673
  /* @__PURE__ */ jsx(ScrollView, {
571
674
  style: styles$2.log,
675
+ contentContainerStyle: styles$2.logBody,
572
676
  children: error !== "" ? /* @__PURE__ */ jsx(Text, {
573
677
  accessibilityRole: "alert",
574
678
  style: styles$2.error,
@@ -581,17 +685,18 @@ function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
581
685
  children: query === "" ? "The log is empty." : "Nothing matched."
582
686
  }) : lines.map((line, index) => /* @__PURE__ */ jsxs(Text, {
583
687
  selectable: true,
584
- style: styles$2.meta,
585
- children: [
586
- numbers[index],
587
- " ",
588
- line
589
- ]
688
+ style: styles$2.line,
689
+ children: [/* @__PURE__ */ jsx(Text, {
690
+ style: styles$2.number,
691
+ children: `${String(numbers[index] ?? "").padStart(gutter)} `
692
+ }), line]
590
693
  }, `${numbers[index] ?? index}`))
591
694
  }),
592
695
  /* @__PURE__ */ jsx(Text, {
593
696
  selectable: true,
594
- style: styles$2.meta,
697
+ numberOfLines: 1,
698
+ ellipsizeMode: "middle",
699
+ style: styles$2.path,
595
700
  children: run?.logPath ?? slice?.path ?? ""
596
701
  })
597
702
  ]
@@ -605,13 +710,46 @@ const styles$2 = StyleSheet.create((theme) => ({
605
710
  backgroundColor: theme.colors.bg,
606
711
  uni__dependencies: [0]
607
712
  },
713
+ header: {
714
+ flexDirection: "row",
715
+ alignItems: "center",
716
+ gap: theme.spacing(2),
717
+ minHeight: 32,
718
+ uni__dependencies: [0]
719
+ },
720
+ grow: { flex: 1 },
608
721
  title: {
722
+ flexShrink: 1,
609
723
  color: theme.colors.hi,
610
724
  fontWeight: "600",
611
725
  fontFamily: fontOf(theme.fontFamily.sans),
612
726
  fontSize: theme.fontSize.base,
613
727
  uni__dependencies: [0]
614
728
  },
729
+ commandBox: {
730
+ gap: theme.spacing(1),
731
+ paddingHorizontal: theme.spacing(3),
732
+ paddingVertical: theme.spacing(2),
733
+ borderRadius: theme.borderRadius.md,
734
+ backgroundColor: theme.colors.deep,
735
+ uni__dependencies: [0]
736
+ },
737
+ command: {
738
+ color: theme.colors.hi,
739
+ fontFamily: fontOf(theme.fontFamily.mono),
740
+ fontSize: theme.fontSize.sm,
741
+ uni__dependencies: [0]
742
+ },
743
+ prompt: {
744
+ color: theme.colors.faint,
745
+ uni__dependencies: [0]
746
+ },
747
+ folder: {
748
+ color: theme.colors.dim,
749
+ fontFamily: fontOf(theme.fontFamily.mono),
750
+ fontSize: theme.fontSize.xs,
751
+ uni__dependencies: [0]
752
+ },
615
753
  meta: {
616
754
  color: theme.colors.dim,
617
755
  fontFamily: fontOf(theme.fontFamily.sans),
@@ -622,7 +760,7 @@ const styles$2 = StyleSheet.create((theme) => ({
622
760
  error: {
623
761
  color: theme.colors.red,
624
762
  fontFamily: fontOf(theme.fontFamily.sans),
625
- fontSize: theme.fontSize.base,
763
+ fontSize: theme.fontSize.sm,
626
764
  uni__dependencies: [0]
627
765
  },
628
766
  actions: {
@@ -633,8 +771,30 @@ const styles$2 = StyleSheet.create((theme) => ({
633
771
  },
634
772
  log: {
635
773
  flex: 1,
774
+ borderRadius: theme.borderRadius.md,
636
775
  backgroundColor: theme.colors.deep,
637
776
  uni__dependencies: [0]
777
+ },
778
+ logBody: {
779
+ padding: theme.spacing(3),
780
+ uni__dependencies: [0]
781
+ },
782
+ line: {
783
+ color: theme.colors.text,
784
+ fontFamily: fontOf(theme.fontFamily.mono),
785
+ fontSize: theme.fontSize.xs,
786
+ lineHeight: theme.lineHeight.sm,
787
+ uni__dependencies: [0]
788
+ },
789
+ number: {
790
+ color: theme.colors.faint,
791
+ uni__dependencies: [0]
792
+ },
793
+ path: {
794
+ color: theme.colors.faint,
795
+ fontFamily: fontOf(theme.fontFamily.mono),
796
+ fontSize: theme.fontSize.xs,
797
+ uni__dependencies: [0]
638
798
  }
639
799
  }));
640
800
  //#endregion
@@ -672,26 +832,78 @@ function runnerShellTab(run) {
672
832
  })
673
833
  };
674
834
  }
675
- function RunnerShellPanel({ sessionId, runId, renderRunnerTerminal }) {
835
+ /**
836
+ * One terminal runner, full screen. A phone keyboard has no Ctrl key, so
837
+ * Ctrl-C is a button; Search opens the runner's log, which the sink keeps
838
+ * scrubbed beside the raw bytes for exactly that.
839
+ */
840
+ function RunnerShellPanel({ sessionId, runId, renderRunnerTerminal, sendSessionFrame, openTransientTab }) {
841
+ const session = useRunners(sessionId);
842
+ const run = session.runs.find((candidate) => candidate.id === runId);
843
+ const live = run?.state === "running";
844
+ const stopping = session.stopRequested.includes(runId);
676
845
  const request = useMemo(() => sessionId === null ? void 0 : {
677
846
  key: `${sessionId}:${runId}`,
678
847
  read: (from, generation, signal) => readRunnerScreen(sessionId, runId, from, generation, signal),
679
848
  write: (text) => sendRunnerInput(sessionId, runId, text)
680
849
  }, [sessionId, runId]);
681
- return /* @__PURE__ */ jsx(View, {
850
+ return /* @__PURE__ */ jsxs(View, {
682
851
  style: styles$1.panel,
683
- children: request === void 0 ? /* @__PURE__ */ jsx(Text, {
684
- style: styles$1.meta,
685
- children: "Select a session to attach to this runner."
686
- }) : renderRunnerTerminal?.(request)
852
+ children: [
853
+ /* @__PURE__ */ jsx(RunnerRunHeader, {
854
+ run,
855
+ name: run?.name ?? runId,
856
+ sessionDirectory: session.sessionDirectory,
857
+ action: live && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
858
+ testID: "runner-shell-stop",
859
+ variant: stopping ? "outline" : "danger-outline",
860
+ size: "sm",
861
+ disabled: stopping,
862
+ onPress: () => requestRunnerStop(sendSessionFrame, sessionId, runId),
863
+ children: stopping ? "Stopping…" : "Stop"
864
+ }) : null
865
+ }),
866
+ /* @__PURE__ */ jsx(View, {
867
+ style: styles$1.terminal,
868
+ children: request === void 0 ? /* @__PURE__ */ jsx(Text, {
869
+ style: styles$1.meta,
870
+ children: "Select a session to attach to this runner."
871
+ }) : renderRunnerTerminal?.(request)
872
+ }),
873
+ /* @__PURE__ */ jsxs(View, {
874
+ style: styles$1.actions,
875
+ children: [run ? /* @__PURE__ */ jsxs(Button, {
876
+ testID: "runner-shell-search",
877
+ variant: "outline",
878
+ onPress: () => openTransientTab(runnerLogTab(run)),
879
+ style: styles$1.action,
880
+ children: [/* @__PURE__ */ jsx(SearchIcon, { size: 14 }), "Search output"]
881
+ }) : null, live && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
882
+ testID: "runner-shell-interrupt",
883
+ variant: "outline",
884
+ onPress: () => void sendRunnerInput(sessionId, runId, ""),
885
+ style: styles$1.action,
886
+ children: "Ctrl-C"
887
+ }) : null]
888
+ })
889
+ ]
687
890
  });
688
891
  }
689
892
  const styles$1 = StyleSheet.create((theme) => ({
690
893
  panel: {
691
894
  flex: 1,
895
+ gap: theme.spacing(2),
896
+ padding: theme.spacing(3),
692
897
  backgroundColor: theme.colors.bg,
693
898
  uni__dependencies: [0]
694
899
  },
900
+ actions: {
901
+ flexDirection: "row",
902
+ gap: theme.spacing(2),
903
+ uni__dependencies: [0]
904
+ },
905
+ action: { flex: 1 },
906
+ terminal: { flex: 1 },
695
907
  meta: {
696
908
  color: theme.colors.dim,
697
909
  fontFamily: fontOf(theme.fontFamily.sans),
@@ -701,6 +913,10 @@ const styles$1 = StyleSheet.create((theme) => ({
701
913
  }));
702
914
  //#endregion
703
915
  //#region src/extensions/workspaces/sessions/(frontend)/_components/RunnersPanel.native.tsx
916
+ /** A runner's own screen: its terminal when it has one, its log when it does not. */
917
+ function runnerTab(run) {
918
+ return run.interactive ? runnerShellTab(run) : runnerLogTab(run);
919
+ }
704
920
  function runnersTab() {
705
921
  return {
706
922
  id: "runner-runs",
@@ -708,60 +924,46 @@ function runnersTab() {
708
924
  panel: RunnersPanel
709
925
  };
710
926
  }
711
- function RunnerRow({ run, props, stopping, now, detail = false }) {
927
+ function RunnerRow({ run, props, stopping, now }) {
712
928
  const tail = useRunnerTail(props.sessionId, run.id, run.state === "running");
713
- return /* @__PURE__ */ jsxs(View, {
929
+ return /* @__PURE__ */ jsxs(Pressable, {
714
930
  testID: `activity-runner-${run.id}`,
715
- style: styles.row,
716
- children: [
717
- /* @__PURE__ */ jsx(ListRow, {
718
- leading: /* @__PURE__ */ jsx(Dot, {
931
+ accessibilityRole: "button",
932
+ accessibilityLabel: `Open ${run.name}`,
933
+ onPress: () => props.openTransientTab(runnerTab(run)),
934
+ style: ({ pressed }) => [styles.row, pressed && styles.pressed],
935
+ children: [/* @__PURE__ */ jsxs(View, {
936
+ style: styles.line,
937
+ children: [
938
+ /* @__PURE__ */ jsx(Dot, {
719
939
  tone: "yellow",
720
940
  pulse: true
721
941
  }),
722
- title: run.name,
723
- description: `${formatRunnerUptime(run.startedAt, now)}${run.interactive ? " · tty" : ""}`,
724
- onPress: () => props.openTransientTab(runnerLogTab(run))
725
- }),
726
- /* @__PURE__ */ jsx(Text, {
727
- numberOfLines: detail ? void 0 : 1,
728
- style: styles.meta,
729
- children: tail ?? run.command
730
- }),
731
- detail ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, {
732
- selectable: true,
733
- style: styles.meta,
734
- children: run.command
735
- }), /* @__PURE__ */ jsx(Text, {
736
- selectable: true,
737
- style: styles.meta,
738
- children: run.cwd
739
- })] }) : null,
740
- /* @__PURE__ */ jsxs(View, {
741
- style: styles.actions,
742
- children: [
743
- detail ? /* @__PURE__ */ jsx(Button, {
744
- variant: "outline",
745
- onPress: () => props.openTransientTab(runnerLogTab(run)),
746
- children: "Log"
747
- }) : null,
748
- detail && run.interactive && props.sessionId !== null ? /* @__PURE__ */ jsx(Button, {
749
- variant: "outline",
750
- onPress: () => props.openTransientTab(runnerShellTab(run)),
751
- children: "Shell"
752
- }) : null,
753
- props.sessionId !== null ? /* @__PURE__ */ jsx(Button, {
754
- testID: `activity-runner-stop-${run.id}`,
755
- variant: stopping ? "outline" : "danger-outline",
756
- disabled: stopping,
757
- onPress: () => {
758
- if (props.sessionId !== null) requestRunnerStop(props.sendSessionFrame, props.sessionId, run.id);
759
- },
760
- children: stopping ? "Stopping…" : "Stop"
761
- }) : null
762
- ]
763
- })
764
- ]
942
+ /* @__PURE__ */ jsx(Text, {
943
+ numberOfLines: 1,
944
+ style: styles.name,
945
+ children: run.name
946
+ }),
947
+ /* @__PURE__ */ jsx(Text, {
948
+ style: styles.meta,
949
+ children: formatRunnerUptime(run.startedAt, now)
950
+ }),
951
+ props.sessionId !== null ? /* @__PURE__ */ jsx(Button, {
952
+ testID: `activity-runner-stop-${run.id}`,
953
+ size: "xs",
954
+ variant: stopping ? "outline" : "danger-outline",
955
+ disabled: stopping,
956
+ onPress: () => {
957
+ if (props.sessionId !== null) requestRunnerStop(props.sendSessionFrame, props.sessionId, run.id);
958
+ },
959
+ children: stopping ? "Stopping…" : "Stop"
960
+ }) : null
961
+ ]
962
+ }), /* @__PURE__ */ jsx(Text, {
963
+ numberOfLines: 1,
964
+ style: styles.tail,
965
+ children: tail ?? `$ ${run.command}`
966
+ })]
765
967
  });
766
968
  }
767
969
  function RunnersActivitySection(props) {
@@ -773,15 +975,23 @@ function RunnersActivitySection(props) {
773
975
  return () => clearInterval(timer);
774
976
  }, []);
775
977
  return running.length === 0 ? /* @__PURE__ */ jsxs(View, {
776
- style: styles.row,
978
+ style: styles.idle,
777
979
  children: [/* @__PURE__ */ jsx(Text, {
778
980
  style: styles.meta,
779
981
  children: "Idle"
780
- }), props.sessionId !== null ? /* @__PURE__ */ jsx(Button, {
781
- variant: "ghost",
982
+ }), props.sessionId !== null ? /* @__PURE__ */ jsxs(Pressable, {
983
+ accessibilityRole: "button",
782
984
  testID: "activity-runner-launch",
985
+ hitSlop: 10,
783
986
  onPress: () => props.openTransientTab(runnersTab()),
784
- children: "Launch a runner"
987
+ style: styles.launch,
988
+ children: [/* @__PURE__ */ jsx(Text, {
989
+ style: styles.launchText,
990
+ children: "Launch a runner"
991
+ }), /* @__PURE__ */ jsx(ArrowRightIcon, {
992
+ size: 14,
993
+ color: "green"
994
+ })]
785
995
  }) : null]
786
996
  }) : /* @__PURE__ */ jsx(View, { children: running.map((run) => /* @__PURE__ */ jsx(RunnerRow, {
787
997
  run,
@@ -790,13 +1000,22 @@ function RunnersActivitySection(props) {
790
1000
  stopping: session.stopRequested.includes(run.id)
791
1001
  }, run.id)) });
792
1002
  }
1003
+ /**
1004
+ * Every running runner and the one place to start another: type a command
1005
+ * and Run hosts it on a terminal (plain output when the host has no rmux or
1006
+ * tmux), or leave it empty for a login shell. The started runner opens on
1007
+ * its own screen.
1008
+ */
793
1009
  function RunnersPanel(props) {
794
1010
  const { openTransientTab } = props;
795
1011
  const session = useRunners(props.sessionId);
796
1012
  const running = session.runs.filter((run) => run.state === "running");
797
- const [launching, setLaunching] = useState(false);
1013
+ const [command, setCommand] = useState("");
1014
+ const [cwd, setCwd] = useState("");
798
1015
  const [now, setNow] = useState(() => Date.now());
799
1016
  const baseline = useRef(null);
1017
+ /** What the pending launch ran, put back if the start fails. */
1018
+ const launched = useRef("");
800
1019
  const timeout = useRef(void 0);
801
1020
  useEffect(() => {
802
1021
  const timer = setInterval(() => setNow(Date.now()), 1e4);
@@ -804,81 +1023,117 @@ function RunnersPanel(props) {
804
1023
  }, []);
805
1024
  useEffect(() => {
806
1025
  if (baseline.current === null) return;
807
- const shell = session.runs.find((run) => run.state === "running" && run.interactive && !baseline.current?.has(run.id));
808
- if (shell === void 0) return;
1026
+ const started = session.runs.find((run) => run.state === "running" && !baseline.current?.has(run.id));
1027
+ if (started === void 0) return;
809
1028
  baseline.current = null;
810
1029
  clearTimeout(timeout.current);
811
- openTransientTab(runnerShellTab(shell));
1030
+ openTransientTab(runnerTab(started));
812
1031
  }, [session.runs, openTransientTab]);
1032
+ const failureId = session.startFailure?.id;
1033
+ useEffect(() => {
1034
+ if (failureId === void 0 || baseline.current === null) return;
1035
+ baseline.current = null;
1036
+ clearTimeout(timeout.current);
1037
+ setCommand(launched.current);
1038
+ }, [failureId]);
813
1039
  useEffect(() => () => clearTimeout(timeout.current), []);
1040
+ const launch = () => {
1041
+ if (props.sessionId === null) return;
1042
+ baseline.current = new Set(session.runs.map((run) => run.id));
1043
+ clearTimeout(timeout.current);
1044
+ timeout.current = setTimeout(() => {
1045
+ baseline.current = null;
1046
+ }, 3e4);
1047
+ requestRunnerStart(props.sendSessionFrame, props.sessionId, runnerRunRequest(command, cwd));
1048
+ launched.current = command;
1049
+ setCommand("");
1050
+ };
814
1051
  return /* @__PURE__ */ jsxs(View, {
815
1052
  style: styles.panel,
816
1053
  children: [
817
- /* @__PURE__ */ jsx(Text, {
818
- style: styles.title,
819
- children: running.length === 0 ? "Nothing running" : `${running.length} running`
820
- }),
821
- props.sessionId !== null ? /* @__PURE__ */ jsxs(View, {
822
- style: styles.actions,
823
- children: [/* @__PURE__ */ jsx(Button, {
824
- variant: "outline",
825
- onPress: () => {
826
- if (props.sessionId === null) return;
827
- baseline.current = new Set(session.runs.map((run) => run.id));
828
- clearTimeout(timeout.current);
829
- timeout.current = setTimeout(() => {
830
- baseline.current = null;
831
- }, 3e4);
832
- requestRunnerStart(props.sendSessionFrame, props.sessionId, RUNNER_SHELL_REQUEST);
833
- },
834
- children: "Start shell"
835
- }), /* @__PURE__ */ jsx(Button, {
836
- onPress: () => setLaunching(true),
837
- children: "Run command"
1054
+ /* @__PURE__ */ jsxs(View, {
1055
+ style: styles.header,
1056
+ children: [/* @__PURE__ */ jsx(Text, {
1057
+ style: styles.title,
1058
+ children: "Runners"
1059
+ }), /* @__PURE__ */ jsx(Text, {
1060
+ style: styles.meta,
1061
+ children: running.length === 0 ? "Nothing running" : `${running.length} running`
838
1062
  })]
1063
+ }),
1064
+ props.sessionId !== null ? /* @__PURE__ */ jsx(RunnerCommandBar, {
1065
+ command,
1066
+ onCommandChange: setCommand,
1067
+ cwd,
1068
+ onCwdChange: setCwd,
1069
+ folders: running.map((run) => run.cwd),
1070
+ ...session.sessionDirectory === void 0 ? {} : { sessionDirectory: session.sessionDirectory },
1071
+ ...session.startFailure === void 0 ? {} : { problem: session.startFailure.message },
1072
+ onLaunch: launch
839
1073
  }) : null,
840
1074
  /* @__PURE__ */ jsx(ScrollView, { children: running.length === 0 ? /* @__PURE__ */ jsx(Text, {
841
1075
  style: styles.meta,
842
- children: "A command that outlives its turn moves to a runner and appears here while it works."
843
- }) : running.map((run) => /* @__PURE__ */ jsx(RunnerRow, {
844
- run,
845
- props,
846
- now,
847
- stopping: session.stopRequested.includes(run.id),
848
- detail: true
849
- }, run.id)) }),
850
- launching && props.sessionId !== null ? /* @__PURE__ */ jsx(LaunchRunnerSheet, {
851
- sessionId: props.sessionId,
852
- send: props.sendSessionFrame,
853
- defaultCwd: running[0]?.cwd,
854
- onClose: () => setLaunching(false)
855
- }) : null
1076
+ children: props.sessionId === null ? "Open a session to start a runner." : "Nothing running yet. Type a command above, like pnpm dev, and it opens as a live terminal."
1077
+ }) : running.map((run) => /* @__PURE__ */ jsx(ListRow, {
1078
+ leading: /* @__PURE__ */ jsx(Dot, {
1079
+ tone: "yellow",
1080
+ pulse: true
1081
+ }),
1082
+ title: run.name,
1083
+ description: `${run.command} · ${formatRunnerUptime(run.startedAt, now)}`,
1084
+ disclosure: true,
1085
+ onPress: () => openTransientTab(runnerTab(run))
1086
+ }, run.id)) })
856
1087
  ]
857
1088
  });
858
1089
  }
859
1090
  const styles = StyleSheet.create((theme) => ({
860
1091
  panel: {
861
1092
  flex: 1,
862
- gap: theme.spacing(2),
863
- padding: theme.spacing(3),
1093
+ gap: theme.spacing(3),
1094
+ padding: theme.spacing(4),
864
1095
  backgroundColor: theme.colors.bg,
865
1096
  uni__dependencies: [0]
866
1097
  },
1098
+ header: {
1099
+ flexDirection: "row",
1100
+ alignItems: "center",
1101
+ gap: theme.spacing(2),
1102
+ uni__dependencies: [0]
1103
+ },
867
1104
  row: {
868
1105
  gap: theme.spacing(1),
869
1106
  paddingVertical: theme.spacing(2),
870
1107
  uni__dependencies: [0]
871
1108
  },
872
- actions: {
1109
+ pressed: { opacity: .6 },
1110
+ line: {
873
1111
  flexDirection: "row",
874
- flexWrap: "wrap",
1112
+ alignItems: "center",
875
1113
  gap: theme.spacing(2),
876
1114
  uni__dependencies: [0]
877
1115
  },
878
- title: {
1116
+ name: {
1117
+ flex: 1,
879
1118
  color: theme.colors.hi,
880
1119
  fontFamily: fontOf(theme.fontFamily.sans),
881
1120
  fontSize: theme.fontSize.base,
1121
+ lineHeight: theme.lineHeight.base,
1122
+ fontWeight: theme.fontWeight.medium,
1123
+ uni__dependencies: [0]
1124
+ },
1125
+ tail: {
1126
+ paddingLeft: theme.spacing(3.5),
1127
+ color: theme.colors.dim,
1128
+ fontFamily: fontOf(theme.fontFamily.mono),
1129
+ fontSize: theme.fontSize.sm,
1130
+ lineHeight: theme.lineHeight.sm,
1131
+ uni__dependencies: [0]
1132
+ },
1133
+ title: {
1134
+ color: theme.colors.hi,
1135
+ fontFamily: fontOf(theme.fontFamily.sans),
1136
+ fontSize: theme.fontSize.lg,
882
1137
  fontWeight: "600",
883
1138
  uni__dependencies: [0]
884
1139
  },
@@ -888,6 +1143,27 @@ const styles = StyleSheet.create((theme) => ({
888
1143
  fontSize: theme.fontSize.sm,
889
1144
  lineHeight: theme.lineHeight.sm,
890
1145
  uni__dependencies: [0]
1146
+ },
1147
+ idle: {
1148
+ flexDirection: "row",
1149
+ alignItems: "center",
1150
+ flexWrap: "wrap",
1151
+ gap: theme.spacing(4),
1152
+ uni__dependencies: [0]
1153
+ },
1154
+ launch: {
1155
+ flexDirection: "row",
1156
+ alignItems: "center",
1157
+ gap: theme.spacing(1),
1158
+ uni__dependencies: [0]
1159
+ },
1160
+ launchText: {
1161
+ color: theme.colors.green,
1162
+ fontFamily: fontOf(theme.fontFamily.sans),
1163
+ fontSize: theme.fontSize.base,
1164
+ lineHeight: theme.lineHeight.base,
1165
+ fontWeight: theme.fontWeight.medium,
1166
+ uni__dependencies: [0]
891
1167
  }
892
1168
  }));
893
1169
  //#endregion
@@ -895,6 +1171,7 @@ const styles = StyleSheet.create((theme) => ({
895
1171
  var runners_native_default = defineActivityGroup({
896
1172
  name: "runners",
897
1173
  keys: "r l",
1174
+ icon: () => createElement(PlayCircleIcon),
898
1175
  statusKey: "po-runner-runners",
899
1176
  activeSource: runnerActivitySource,
900
1177
  transientTab: runnersTab,