@agimon-ai/po-runner 0.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +257 -0
- package/dist/ansiScrub-CsqTSaVT.mjs +71 -0
- package/dist/ansiScrub-CsqTSaVT.mjs.map +1 -0
- package/dist/ansiScrub-D5gpHDch.cjs +100 -0
- package/dist/ansiScrub-D5gpHDch.cjs.map +1 -0
- package/dist/ansiScrub.cjs +7 -0
- package/dist/ansiScrub.d.mts +26 -0
- package/dist/ansiScrub.d.mts.map +1 -0
- package/dist/ansiScrub.mjs +2 -0
- package/dist/apiContracts.cjs +162 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +157 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/bashResult-DNgtahGR.cjs +629 -0
- package/dist/bashResult-DNgtahGR.cjs.map +1 -0
- package/dist/bashResult-DcrRaBLv.d.mts +49 -0
- package/dist/bashResult-DcrRaBLv.d.mts.map +1 -0
- package/dist/bashResult-bIVSzoj-.mjs +546 -0
- package/dist/bashResult-bIVSzoj-.mjs.map +1 -0
- package/dist/bashRunService-CJ02XaAq.mjs +440 -0
- package/dist/bashRunService-CJ02XaAq.mjs.map +1 -0
- package/dist/bashRunService-Ci0cGdYO.d.mts +71 -0
- package/dist/bashRunService-Ci0cGdYO.d.mts.map +1 -0
- package/dist/bashRunService-DyqxOSlx.cjs +445 -0
- package/dist/bashRunService-DyqxOSlx.cjs.map +1 -0
- package/dist/bashRunService.cjs +3 -0
- package/dist/bashRunService.d.mts +36 -0
- package/dist/bashRunService.d.mts.map +1 -0
- package/dist/bashRunService.mjs +2 -0
- package/dist/bashSchema.cjs +5 -0
- package/dist/bashSchema.d.mts +7 -0
- package/dist/bashSchema.d.mts.map +1 -0
- package/dist/bashSchema.mjs +2 -0
- package/dist/bashTool-B1EwZLo_.d.mts +20 -0
- package/dist/bashTool-B1EwZLo_.d.mts.map +1 -0
- package/dist/bashTool-C-k7fmyh.mjs +24 -0
- package/dist/bashTool-C-k7fmyh.mjs.map +1 -0
- package/dist/bashTool-CdgtqVFD.cjs +41 -0
- package/dist/bashTool-CdgtqVFD.cjs.map +1 -0
- package/dist/bashTool.cjs +13 -0
- package/dist/bashTool.cjs.map +1 -0
- package/dist/bashTool.d.mts +8 -0
- package/dist/bashTool.d.mts.map +1 -0
- package/dist/bashTool.mjs +11 -0
- package/dist/bashTool.mjs.map +1 -0
- package/dist/bin/cli.cjs +78 -0
- package/dist/bin/cli.cjs.map +1 -0
- package/dist/bin/cli.d.mts +4 -0
- package/dist/bin/cli.d.mts.map +1 -0
- package/dist/bin/cli.mjs +77 -0
- package/dist/bin/cli.mjs.map +1 -0
- package/dist/bin/logSink.cjs +102 -0
- package/dist/bin/logSink.cjs.map +1 -0
- package/dist/bin/logSink.d.mts +4 -0
- package/dist/bin/logSink.d.mts.map +1 -0
- package/dist/bin/logSink.mjs +99 -0
- package/dist/bin/logSink.mjs.map +1 -0
- package/dist/bin/runnerHost.cjs +131 -0
- package/dist/bin/runnerHost.cjs.map +1 -0
- package/dist/bin/runnerHost.d.mts +4 -0
- package/dist/bin/runnerHost.d.mts.map +1 -0
- package/dist/bin/runnerHost.mjs +127 -0
- package/dist/bin/runnerHost.mjs.map +1 -0
- package/dist/cliApp-Dj9nwaOv.cjs +244 -0
- package/dist/cliApp-Dj9nwaOv.cjs.map +1 -0
- package/dist/cliApp-v8Z3rMCi.mjs +237 -0
- package/dist/cliApp-v8Z3rMCi.mjs.map +1 -0
- package/dist/cliApp.cjs +3 -0
- package/dist/cliApp.d.mts +18 -0
- package/dist/cliApp.d.mts.map +1 -0
- package/dist/cliApp.mjs +2 -0
- package/dist/clock-Cv2pddgo.mjs +16 -0
- package/dist/clock-Cv2pddgo.mjs.map +1 -0
- package/dist/clock-euJkweLq.d.mts +11 -0
- package/dist/clock-euJkweLq.d.mts.map +1 -0
- package/dist/clock-jqnLWziT.cjs +21 -0
- package/dist/clock-jqnLWziT.cjs.map +1 -0
- package/dist/clock.cjs +3 -0
- package/dist/clock.d.mts +10 -0
- package/dist/clock.d.mts.map +1 -0
- package/dist/clock.mjs +2 -0
- package/dist/compaction-4QSUsBxq.cjs +40 -0
- package/dist/compaction-4QSUsBxq.cjs.map +1 -0
- package/dist/compaction-DCw80VZj.mjs +29 -0
- package/dist/compaction-DCw80VZj.mjs.map +1 -0
- package/dist/compaction.cjs +4 -0
- package/dist/compaction.d.mts +10 -0
- package/dist/compaction.d.mts.map +1 -0
- package/dist/compaction.mjs +2 -0
- package/dist/config.cjs +41 -0
- package/dist/config.d.mts +2 -0
- package/dist/config.mjs +2 -0
- package/dist/extensions/mcp-ui.mjs +23 -0
- package/dist/extensions/mcp-ui.mjs.map +1 -0
- package/dist/extensions/mcp.cjs +142 -0
- package/dist/extensions/mcp.cjs.map +1 -0
- package/dist/extensions/mcp.d.mts +5 -0
- package/dist/extensions/mcp.d.mts.map +1 -0
- package/dist/extensions/mcp.mjs +114 -0
- package/dist/extensions/mcp.mjs.map +1 -0
- package/dist/extensions/server.cjs +1156 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +1146 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/extensions/web.mjs +1785 -0
- package/dist/extensions/web.mjs.map +1 -0
- package/dist/headless-BGYfIlNk.mjs +169 -0
- package/dist/headless-BGYfIlNk.mjs.map +1 -0
- package/dist/headless-CHUsBGhg.cjs +180 -0
- package/dist/headless-CHUsBGhg.cjs.map +1 -0
- package/dist/index-BZL7keVW.d.mts +31 -0
- package/dist/index-BZL7keVW.d.mts.map +1 -0
- package/dist/index-BwB_jTI9.d.mts +12 -0
- package/dist/index-BwB_jTI9.d.mts.map +1 -0
- package/dist/index-D9-IXax4.d.mts +43 -0
- package/dist/index-D9-IXax4.d.mts.map +1 -0
- package/dist/index-DBz0DxnL.d.mts +39 -0
- package/dist/index-DBz0DxnL.d.mts.map +1 -0
- package/dist/index-DM7yJpMs.d.mts +25 -0
- package/dist/index-DM7yJpMs.d.mts.map +1 -0
- package/dist/index-Dhi8asLH.d.mts +22 -0
- package/dist/index-Dhi8asLH.d.mts.map +1 -0
- package/dist/index.cjs +19 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +6 -0
- package/dist/launcher-BDm6-DiQ.mjs +150 -0
- package/dist/launcher-BDm6-DiQ.mjs.map +1 -0
- package/dist/launcher-BG5t9nvG.d.mts +39 -0
- package/dist/launcher-BG5t9nvG.d.mts.map +1 -0
- package/dist/launcher-SlyE8R3b.cjs +157 -0
- package/dist/launcher-SlyE8R3b.cjs.map +1 -0
- package/dist/launcher.cjs +3 -0
- package/dist/launcher.d.mts +33 -0
- package/dist/launcher.d.mts.map +1 -0
- package/dist/launcher.mjs +2 -0
- package/dist/lifeline-BZaQA0rm.mjs +88 -0
- package/dist/lifeline-BZaQA0rm.mjs.map +1 -0
- package/dist/lifeline-BuV_erib.cjs +98 -0
- package/dist/lifeline-BuV_erib.cjs.map +1 -0
- package/dist/lifeline-eJdZV0nN.d.mts +19 -0
- package/dist/lifeline-eJdZV0nN.d.mts.map +1 -0
- package/dist/lifeline.cjs +6 -0
- package/dist/lifeline.d.mts +21 -0
- package/dist/lifeline.d.mts.map +1 -0
- package/dist/lifeline.mjs +3 -0
- package/dist/logFile-C7oQ_v-g.d.mts +16 -0
- package/dist/logFile-C7oQ_v-g.d.mts.map +1 -0
- package/dist/logFile-CNvepZh0.cjs +67 -0
- package/dist/logFile-CNvepZh0.cjs.map +1 -0
- package/dist/logFile-KKPbkC-a.mjs +60 -0
- package/dist/logFile-KKPbkC-a.mjs.map +1 -0
- package/dist/logFile.cjs +3 -0
- package/dist/logFile.d.mts +11 -0
- package/dist/logFile.d.mts.map +1 -0
- package/dist/logFile.mjs +2 -0
- package/dist/logReader-B6J7L7Ua.cjs +282 -0
- package/dist/logReader-B6J7L7Ua.cjs.map +1 -0
- package/dist/logReader-MdW9ffvV.d.mts +43 -0
- package/dist/logReader-MdW9ffvV.d.mts.map +1 -0
- package/dist/logReader-UfbDbwQl.mjs +257 -0
- package/dist/logReader-UfbDbwQl.mjs.map +1 -0
- package/dist/logReader.cjs +4 -0
- package/dist/logReader.d.mts +3 -0
- package/dist/logReader.mjs +2 -0
- package/dist/processControl-BAOa3oo_.d.mts +14 -0
- package/dist/processControl-BAOa3oo_.d.mts.map +1 -0
- package/dist/processControl-DApTpV16.mjs +26 -0
- package/dist/processControl-DApTpV16.mjs.map +1 -0
- package/dist/processControl-DNJ1WZ9N.cjs +31 -0
- package/dist/processControl-DNJ1WZ9N.cjs.map +1 -0
- package/dist/processControl.cjs +3 -0
- package/dist/processControl.d.mts +10 -0
- package/dist/processControl.d.mts.map +1 -0
- package/dist/processControl.mjs +2 -0
- package/dist/ptyHost-DWWtRrGn.d.mts +38 -0
- package/dist/ptyHost-DWWtRrGn.d.mts.map +1 -0
- package/dist/ptyHost-DlQtTB8F.mjs +150 -0
- package/dist/ptyHost-DlQtTB8F.mjs.map +1 -0
- package/dist/ptyHost-MI2Z6WTG.cjs +157 -0
- package/dist/ptyHost-MI2Z6WTG.cjs.map +1 -0
- package/dist/ptyHost.cjs +3 -0
- package/dist/ptyHost.d.mts +26 -0
- package/dist/ptyHost.d.mts.map +1 -0
- package/dist/ptyHost.mjs +2 -0
- package/dist/ptySpawner-B6GsueZd.d.mts +28 -0
- package/dist/ptySpawner-B6GsueZd.d.mts.map +1 -0
- package/dist/ptySpawner-DEi1KOnd.mjs +17 -0
- package/dist/ptySpawner-DEi1KOnd.mjs.map +1 -0
- package/dist/ptySpawner-ijw0g5_T.cjs +22 -0
- package/dist/ptySpawner-ijw0g5_T.cjs.map +1 -0
- package/dist/ptySpawner.cjs +3 -0
- package/dist/ptySpawner.d.mts +14 -0
- package/dist/ptySpawner.d.mts.map +1 -0
- package/dist/ptySpawner.mjs +2 -0
- package/dist/reconcile-BugwYFbV.cjs +133 -0
- package/dist/reconcile-BugwYFbV.cjs.map +1 -0
- package/dist/reconcile-DMeNSxwn.mjs +114 -0
- package/dist/reconcile-DMeNSxwn.mjs.map +1 -0
- package/dist/reconcile.cjs +5 -0
- package/dist/reconcile.d.mts +36 -0
- package/dist/reconcile.d.mts.map +1 -0
- package/dist/reconcile.mjs +2 -0
- package/dist/responseEnvelope.cjs +19 -0
- package/dist/responseEnvelope.d.mts +50 -0
- package/dist/responseEnvelope.d.mts.map +1 -0
- package/dist/responseEnvelope.mjs +3 -0
- package/dist/rmuxBackend-CaEWooWn.d.mts +36 -0
- package/dist/rmuxBackend-CaEWooWn.d.mts.map +1 -0
- package/dist/rmuxBackend-DfyQwJrc.mjs +509 -0
- package/dist/rmuxBackend-DfyQwJrc.mjs.map +1 -0
- package/dist/rmuxBackend-DlxgqRZ1.cjs +523 -0
- package/dist/rmuxBackend-DlxgqRZ1.cjs.map +1 -0
- package/dist/rmuxBackend.cjs +4 -0
- package/dist/rmuxBackend.d.mts +3 -0
- package/dist/rmuxBackend.mjs +2 -0
- package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
- package/dist/rtkProcessor-B4_urXQr.mjs +10 -0
- package/dist/rtkProcessor-B4_urXQr.mjs.map +1 -0
- package/dist/rtkProcessor-B9KPdyzr.mjs +309 -0
- package/dist/rtkProcessor-B9KPdyzr.mjs.map +1 -0
- package/dist/rtkProcessor-Bc4L4o_8.cjs +335 -0
- package/dist/rtkProcessor-Bc4L4o_8.cjs.map +1 -0
- package/dist/rtkProcessor-CVZvNhkJ.d.mts +35 -0
- package/dist/rtkProcessor-CVZvNhkJ.d.mts.map +1 -0
- package/dist/rtkProcessor-CsrCPyTQ.cjs +33 -0
- package/dist/rtkProcessor-CsrCPyTQ.cjs.map +1 -0
- package/dist/rtkProcessor.cjs +11 -0
- package/dist/rtkProcessor.d.mts +7 -0
- package/dist/rtkProcessor.d.mts.map +1 -0
- package/dist/rtkProcessor.mjs +3 -0
- package/dist/runnerConfig-BQBlVAXn.mjs +134 -0
- package/dist/runnerConfig-BQBlVAXn.mjs.map +1 -0
- package/dist/runnerConfig-BjtulXBI.d.mts +57 -0
- package/dist/runnerConfig-BjtulXBI.d.mts.map +1 -0
- package/dist/runnerConfig-D2tGq5ZM.cjs +367 -0
- package/dist/runnerConfig-D2tGq5ZM.cjs.map +1 -0
- package/dist/runnerDependencies-DruByBGS.mjs +647 -0
- package/dist/runnerDependencies-DruByBGS.mjs.map +1 -0
- package/dist/runnerDependencies-KMZigIxB.cjs +667 -0
- package/dist/runnerDependencies-KMZigIxB.cjs.map +1 -0
- package/dist/runnerDependencies.cjs +3 -0
- package/dist/runnerDependencies.d.mts +2 -0
- package/dist/runnerDependencies.mjs +2 -0
- package/dist/runnerNamer-B8FH7n2v.mjs +44 -0
- package/dist/runnerNamer-B8FH7n2v.mjs.map +1 -0
- package/dist/runnerNamer-we1XVYfM.cjs +49 -0
- package/dist/runnerNamer-we1XVYfM.cjs.map +1 -0
- package/dist/runnerNamer.cjs +3 -0
- package/dist/runnerNamer.d.mts +11 -0
- package/dist/runnerNamer.d.mts.map +1 -0
- package/dist/runnerNamer.mjs +2 -0
- package/dist/runnerPaths-BTEuzxw0.cjs +471 -0
- package/dist/runnerPaths-BTEuzxw0.cjs.map +1 -0
- package/dist/runnerPaths-B_dAAeco.mjs +451 -0
- package/dist/runnerPaths-B_dAAeco.mjs.map +1 -0
- package/dist/runnerPaths.cjs +3 -0
- package/dist/runnerPaths.d.mts +50 -0
- package/dist/runnerPaths.d.mts.map +1 -0
- package/dist/runnerPaths.mjs +2 -0
- package/dist/runnerRegistry-BmkZTJHi.cjs +371 -0
- package/dist/runnerRegistry-BmkZTJHi.cjs.map +1 -0
- package/dist/runnerRegistry-CeOHEjBv.mjs +339 -0
- package/dist/runnerRegistry-CeOHEjBv.mjs.map +1 -0
- package/dist/runnerRegistry-CtJiD02R.d.mts +88 -0
- package/dist/runnerRegistry-CtJiD02R.d.mts.map +1 -0
- package/dist/runnerRegistry.cjs +4 -0
- package/dist/runnerRegistry.d.mts +3 -0
- package/dist/runnerRegistry.mjs +2 -0
- package/dist/runnerSettings-DiV1lvzx.d.mts +34 -0
- package/dist/runnerSettings-DiV1lvzx.d.mts.map +1 -0
- package/dist/services/lifeline/client.cjs +52 -0
- package/dist/services/lifeline/client.cjs.map +1 -0
- package/dist/services/lifeline/client.d.mts +22 -0
- package/dist/services/lifeline/client.d.mts.map +1 -0
- package/dist/services/lifeline/client.mjs +48 -0
- package/dist/services/lifeline/client.mjs.map +1 -0
- package/dist/services/runnerSupervisor/index.cjs +207 -0
- package/dist/services/runnerSupervisor/index.cjs.map +1 -0
- package/dist/services/runnerSupervisor/index.d.mts +71 -0
- package/dist/services/runnerSupervisor/index.d.mts.map +1 -0
- package/dist/services/runnerSupervisor/index.mjs +195 -0
- package/dist/services/runnerSupervisor/index.mjs.map +1 -0
- package/dist/session-BC43uEyG.cjs +29 -0
- package/dist/session-BC43uEyG.cjs.map +1 -0
- package/dist/session-Chsb_Nh2.mjs +18 -0
- package/dist/session-Chsb_Nh2.mjs.map +1 -0
- package/dist/session-DE2Acesp.mjs +6 -0
- package/dist/session-DE2Acesp.mjs.map +1 -0
- package/dist/session-SfIPtdZO.cjs +11 -0
- package/dist/session-SfIPtdZO.cjs.map +1 -0
- package/dist/session.cjs +6 -0
- package/dist/session.d.mts +11 -0
- package/dist/session.d.mts.map +1 -0
- package/dist/session.mjs +3 -0
- package/dist/spawner-BxFsINXP.cjs +68 -0
- package/dist/spawner-BxFsINXP.cjs.map +1 -0
- package/dist/spawner-CdrN_HNk.d.mts +35 -0
- package/dist/spawner-CdrN_HNk.d.mts.map +1 -0
- package/dist/spawner-LoVsn55-.mjs +63 -0
- package/dist/spawner-LoVsn55-.mjs.map +1 -0
- package/dist/spawner.cjs +3 -0
- package/dist/spawner.d.mts +9 -0
- package/dist/spawner.d.mts.map +1 -0
- package/dist/spawner.mjs +2 -0
- package/dist/type-BXM03qSw.d.mts +44 -0
- package/dist/type-BXM03qSw.d.mts.map +1 -0
- package/dist/type-C31e1gla.d.mts +43 -0
- package/dist/type-C31e1gla.d.mts.map +1 -0
- package/dist/type-vWQNTahw.d.mts +12 -0
- package/dist/type-vWQNTahw.d.mts.map +1 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.mts +3 -0
- package/dist/types.mjs +1 -0
- package/llms.txt +9 -0
- package/package.json +303 -0
- package/skills/doom-runner/SKILL.md +78 -0
- package/src/prompts/po-use-runner/SKILL.md +47 -0
|
@@ -0,0 +1,1785 @@
|
|
|
1
|
+
import { apiResponse, createApiClient, defineActivityGroup, defineApiRoutes, 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";
|
|
3
|
+
import { useStore } from "@tanstack/react-store";
|
|
4
|
+
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
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
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/launchLine.ts
|
|
66
|
+
/**
|
|
67
|
+
* The `/runners start` line the launch form sends.
|
|
68
|
+
*
|
|
69
|
+
* WHY THE BUILDER LIVES HERE AND THE PARSER DOES NOT:
|
|
70
|
+
* Cockpit code is compiled into the host bundle and may import only this
|
|
71
|
+
* package's own web and type modules, so it cannot share the session's parser
|
|
72
|
+
* in src/services. The two are pinned together by a test that parses what this
|
|
73
|
+
* writes, which is the only guarantee that matters: the line has to survive
|
|
74
|
+
* the trip.
|
|
75
|
+
*/
|
|
76
|
+
const RUNNERS_START_LINE = "/runners start";
|
|
77
|
+
/** The no-form launch used when a reader asks for a shell rather than a command. */
|
|
78
|
+
const RUNNER_SHELL_REQUEST = {
|
|
79
|
+
command: "exec \"${SHELL:-/bin/bash}\" -l",
|
|
80
|
+
name: "shell",
|
|
81
|
+
interactive: true
|
|
82
|
+
};
|
|
83
|
+
/** Quotes a value that carries whitespace, which is how a value survives tokenizing. */
|
|
84
|
+
function quote(value) {
|
|
85
|
+
return /\s/u.test(value) ? `"${value}"` : value;
|
|
86
|
+
}
|
|
87
|
+
function runnerLaunchLine(request) {
|
|
88
|
+
const cwd = request.cwd?.trim() ?? "";
|
|
89
|
+
const name = request.name?.trim() ?? "";
|
|
90
|
+
const pairs = [
|
|
91
|
+
...name === "" ? [] : [`name=${quote(name)}`],
|
|
92
|
+
...cwd === "" ? [] : [`cwd=${quote(cwd)}`],
|
|
93
|
+
...request.interactive === true ? ["interactive=true"] : []
|
|
94
|
+
];
|
|
95
|
+
return [
|
|
96
|
+
RUNNERS_START_LINE,
|
|
97
|
+
...pairs,
|
|
98
|
+
"--",
|
|
99
|
+
request.command.trim()
|
|
100
|
+
].join(" ");
|
|
101
|
+
}
|
|
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
|
+
//#endregion
|
|
109
|
+
//#region src/constants/webRunners.ts
|
|
110
|
+
const RUNNER_RUNS_TYPE = "runner_runs";
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/runnersStore.ts
|
|
113
|
+
/** The runtime's slash verb that stops one runner headlessly. */
|
|
114
|
+
const STOP_COMMAND = "/runners stop";
|
|
115
|
+
const runners = defineSessionStore({
|
|
116
|
+
runs: [],
|
|
117
|
+
stopRequested: []
|
|
118
|
+
});
|
|
119
|
+
/** The runner channel keeps the dock's busy state aligned with its rendered run list. */
|
|
120
|
+
const runnerActivitySource = {
|
|
121
|
+
subscribe(listener) {
|
|
122
|
+
const subscription = runners.store.subscribe(listener);
|
|
123
|
+
return () => subscription.unsubscribe();
|
|
124
|
+
},
|
|
125
|
+
isActive(sessionId) {
|
|
126
|
+
return runners.select(runners.store.state, sessionId).runs.some((run) => run.state === "running");
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
function isRecord$3(value) {
|
|
130
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Asks the runtime to stop a runner through the same slash command the TUI's
|
|
134
|
+
* Runner Space uses. The request is remembered until the runner's record
|
|
135
|
+
* reports an exit; the runtime, not this click, decides when that is.
|
|
136
|
+
*/
|
|
137
|
+
function requestRunnerStop(send, sessionId, id) {
|
|
138
|
+
send(sessionId, {
|
|
139
|
+
type: "prompt",
|
|
140
|
+
message: `${STOP_COMMAND} ${id}`
|
|
141
|
+
});
|
|
142
|
+
runners.update(sessionId, (current) => ({
|
|
143
|
+
...current,
|
|
144
|
+
stopRequested: [...current.stopRequested.filter((known) => known !== id), id]
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Asks the runtime to start a runner, through the same prompt frame the stop
|
|
149
|
+
* request uses and the same way agents and workflows launch their own work.
|
|
150
|
+
*
|
|
151
|
+
* Nothing is recorded as pending here. The runtime names the run, and it
|
|
152
|
+
* arrives in the feed on its own; guessing at an id before it exists would
|
|
153
|
+
* only have to be reconciled later.
|
|
154
|
+
*/
|
|
155
|
+
function requestRunnerStart(send, sessionId, request) {
|
|
156
|
+
send(sessionId, {
|
|
157
|
+
type: "prompt",
|
|
158
|
+
message: runnerLaunchLine(request)
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/** The plugin's session data channel: 'runner_runs' payloads into the store. */
|
|
162
|
+
const runnerRunsChannel = runners.channel({
|
|
163
|
+
channel: RUNNER_RUNS_TYPE,
|
|
164
|
+
parse(input) {
|
|
165
|
+
if (!isRecord$3(input) || !Array.isArray(input.runs)) return null;
|
|
166
|
+
return { runs: input.runs.filter(isRecord$3) };
|
|
167
|
+
},
|
|
168
|
+
reduce(current, { runs }) {
|
|
169
|
+
const stillRunning = new Set(runs.filter((run) => run.state === "running").map((run) => run.id));
|
|
170
|
+
return {
|
|
171
|
+
runs,
|
|
172
|
+
stopRequested: current.stopRequested.filter((id) => stillRunning.has(id))
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
//#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-doom-text",
|
|
183
|
+
children
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
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.
|
|
190
|
+
*
|
|
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.
|
|
194
|
+
*/
|
|
195
|
+
function LaunchRunnerDialog({ sessionId, sendSessionFrame, defaultCwd, onClose }) {
|
|
196
|
+
const [command, setCommand] = useState("");
|
|
197
|
+
const [cwd, setCwd] = useState(defaultCwd ?? "");
|
|
198
|
+
const [name, setName] = useState("");
|
|
199
|
+
const [interactive, setInteractive] = useState(false);
|
|
200
|
+
const request = {
|
|
201
|
+
command,
|
|
202
|
+
cwd,
|
|
203
|
+
name,
|
|
204
|
+
interactive
|
|
205
|
+
};
|
|
206
|
+
const problems = launchProblems(request);
|
|
207
|
+
const line = runnerLaunchLine(request);
|
|
208
|
+
const launch = () => {
|
|
209
|
+
if (problems.length > 0) return;
|
|
210
|
+
requestRunnerStart(sendSessionFrame, sessionId, request);
|
|
211
|
+
onClose();
|
|
212
|
+
};
|
|
213
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
214
|
+
open: true,
|
|
215
|
+
onOpenChange: (open) => {
|
|
216
|
+
if (!open) onClose();
|
|
217
|
+
},
|
|
218
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
219
|
+
width: "lg",
|
|
220
|
+
"data-testid": "runner-launch-dialog",
|
|
221
|
+
"aria-describedby": void 0,
|
|
222
|
+
children: [/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, {
|
|
223
|
+
"data-testid": "runner-launch-title",
|
|
224
|
+
children: "launch a runner"
|
|
225
|
+
}) }), /* @__PURE__ */ jsxs(DialogBody, { children: [
|
|
226
|
+
/* @__PURE__ */ jsxs("div", {
|
|
227
|
+
className: "flex flex-col gap-1",
|
|
228
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, { children: "Command" }), /* @__PURE__ */ jsx(Textarea, {
|
|
229
|
+
"data-testid": "runner-launch-command",
|
|
230
|
+
rows: COMMAND_ROWS,
|
|
231
|
+
value: command,
|
|
232
|
+
placeholder: "pnpm test",
|
|
233
|
+
onChange: (event) => setCommand(event.target.value)
|
|
234
|
+
})]
|
|
235
|
+
}),
|
|
236
|
+
/* @__PURE__ */ jsxs("div", {
|
|
237
|
+
className: "flex flex-col gap-2.5 sm:flex-row",
|
|
238
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
239
|
+
className: "flex min-w-0 flex-1 flex-col gap-1",
|
|
240
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, { children: "Cwd" }), /* @__PURE__ */ jsx(Input, {
|
|
241
|
+
size: "sm",
|
|
242
|
+
"data-testid": "runner-launch-cwd",
|
|
243
|
+
value: cwd,
|
|
244
|
+
placeholder: "the session's own directory",
|
|
245
|
+
onChange: (event) => setCwd(event.target.value)
|
|
246
|
+
})]
|
|
247
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
248
|
+
className: "flex min-w-0 flex-1 flex-col gap-1",
|
|
249
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, { children: "Name" }), /* @__PURE__ */ jsx(Input, {
|
|
250
|
+
size: "sm",
|
|
251
|
+
"data-testid": "runner-launch-name",
|
|
252
|
+
value: name,
|
|
253
|
+
placeholder: "chosen for you when empty",
|
|
254
|
+
onChange: (event) => setName(event.target.value)
|
|
255
|
+
})]
|
|
256
|
+
})]
|
|
257
|
+
}),
|
|
258
|
+
/* @__PURE__ */ jsxs("div", {
|
|
259
|
+
className: "flex items-center gap-2",
|
|
260
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
261
|
+
id: INTERACTIVE_FIELD_ID,
|
|
262
|
+
"data-testid": "runner-launch-interactive",
|
|
263
|
+
checked: interactive,
|
|
264
|
+
onCheckedChange: (checked) => setInteractive(checked === true)
|
|
265
|
+
}), /* @__PURE__ */ jsx("label", {
|
|
266
|
+
htmlFor: INTERACTIVE_FIELD_ID,
|
|
267
|
+
className: "text-base text-doom-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-doom-border bg-doom-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-doom-green",
|
|
276
|
+
children: line
|
|
277
|
+
})]
|
|
278
|
+
}),
|
|
279
|
+
problems.length > 0 ? /* @__PURE__ */ jsx("p", {
|
|
280
|
+
"data-testid": "runner-launch-problem",
|
|
281
|
+
className: "text-base text-doom-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-doom-dim sm:w-auto",
|
|
289
|
+
children: "it appears above once the runtime starts it"
|
|
290
|
+
}),
|
|
291
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
|
|
292
|
+
/* @__PURE__ */ jsx(Button, {
|
|
293
|
+
variant: "outline",
|
|
294
|
+
size: "xs",
|
|
295
|
+
"data-testid": "runner-launch-cancel",
|
|
296
|
+
onClick: onClose,
|
|
297
|
+
children: "cancel"
|
|
298
|
+
}),
|
|
299
|
+
/* @__PURE__ */ jsx(Button, {
|
|
300
|
+
variant: "primary",
|
|
301
|
+
size: "xs",
|
|
302
|
+
"data-testid": "runner-launch-submit",
|
|
303
|
+
disabled: problems.length > 0,
|
|
304
|
+
onClick: launch,
|
|
305
|
+
children: "launch"
|
|
306
|
+
})
|
|
307
|
+
]
|
|
308
|
+
})
|
|
309
|
+
] })]
|
|
310
|
+
})
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/constants/webRunnerLog.ts
|
|
315
|
+
const RUN_ID_PARAM = "runId";
|
|
316
|
+
const RUNNER_SCREEN_EVENT = "screen";
|
|
317
|
+
const RUNNER_LOG_STREAM_EVENT = "append";
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/types/webRunnerLog.ts
|
|
320
|
+
/** The named SSE event carrying a RunnerLogStreamEvent payload. */
|
|
321
|
+
/**
|
|
322
|
+
* The named SSE event that carries nothing.
|
|
323
|
+
*
|
|
324
|
+
* A runner can be alive and silent for minutes, which on the wire is
|
|
325
|
+
* indistinguishable from a connection that died. This is sent on a timer so
|
|
326
|
+
* the socket keeps proving itself. Readers that only listen for `append`
|
|
327
|
+
* ignore it, which is what makes it safe to add.
|
|
328
|
+
*/
|
|
329
|
+
/** Query parameter names, shared so the page and the route cannot drift apart. */
|
|
330
|
+
const RUNNER_LOG_PARAMS = {
|
|
331
|
+
lines: "lines",
|
|
332
|
+
grep: "grep",
|
|
333
|
+
ignoreCase: "ignoreCase",
|
|
334
|
+
contextLines: "contextLines",
|
|
335
|
+
from: "from"
|
|
336
|
+
};
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/types/apiRoutes.ts
|
|
339
|
+
/**
|
|
340
|
+
* This package's routes, as data.
|
|
341
|
+
*
|
|
342
|
+
* No scope and no base path: the build reads both off
|
|
343
|
+
* `src/extensions/workspaces/sessions/(backend)/api/runner/`, so the mount is
|
|
344
|
+
* stated once, by the folder that creates it. This package stated it four times
|
|
345
|
+
* before, once per URL builder, each of them re-spelling `/plugins/` and the
|
|
346
|
+
* session prefix by hand.
|
|
347
|
+
*
|
|
348
|
+
* The Hono app registers these paths verbatim and the browser addresses them,
|
|
349
|
+
* so a path cannot move on one side alone. `:runId` is Hono's own parameter
|
|
350
|
+
* syntax for that reason; a caller supplies the value as `params`, and the
|
|
351
|
+
* client encodes the segment.
|
|
352
|
+
*/
|
|
353
|
+
var apiRoutes_default = defineApiRoutes({
|
|
354
|
+
log: {
|
|
355
|
+
method: "GET",
|
|
356
|
+
path: `/runners/:${RUN_ID_PARAM}/log`,
|
|
357
|
+
query: [
|
|
358
|
+
RUNNER_LOG_PARAMS.lines,
|
|
359
|
+
RUNNER_LOG_PARAMS.grep,
|
|
360
|
+
RUNNER_LOG_PARAMS.ignoreCase,
|
|
361
|
+
RUNNER_LOG_PARAMS.contextLines
|
|
362
|
+
],
|
|
363
|
+
response: apiResponse()
|
|
364
|
+
},
|
|
365
|
+
/**
|
|
366
|
+
* The live tail of that same log.
|
|
367
|
+
*
|
|
368
|
+
* `stream: true`, so the client exposes a URL and no call method: the
|
|
369
|
+
* consumer is the browser's own `EventSource`, and a relayed call would
|
|
370
|
+
* buffer a stream that never ends.
|
|
371
|
+
*/
|
|
372
|
+
logStream: {
|
|
373
|
+
method: "GET",
|
|
374
|
+
path: `/runners/:${RUN_ID_PARAM}/log/stream`,
|
|
375
|
+
query: [RUNNER_LOG_PARAMS.from],
|
|
376
|
+
stream: true
|
|
377
|
+
},
|
|
378
|
+
/** The unscrubbed pane bytes, for a terminal emulator rather than a reader. */
|
|
379
|
+
screenStream: {
|
|
380
|
+
method: "GET",
|
|
381
|
+
path: `/runners/:${RUN_ID_PARAM}/screen/stream`,
|
|
382
|
+
query: [RUNNER_LOG_PARAMS.from],
|
|
383
|
+
stream: true
|
|
384
|
+
},
|
|
385
|
+
input: {
|
|
386
|
+
method: "POST",
|
|
387
|
+
path: `/runners/:${RUN_ID_PARAM}/screen/input`
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
const api = createApiClient(apiRoutes_default, {
|
|
391
|
+
scopes: ["session"],
|
|
392
|
+
basePath: "runner",
|
|
393
|
+
transport: (input, init) => sealedTransport.fetch(input, init),
|
|
394
|
+
sessionAddress: sessionApiAddress
|
|
395
|
+
});
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/logApi.ts
|
|
398
|
+
const UNREACHABLE = "The cockpit hub is unreachable.";
|
|
399
|
+
function isRecord$2(value) {
|
|
400
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
401
|
+
}
|
|
402
|
+
/** Reads one runner's log: the last N lines, or the lines matching a substring with context. */
|
|
403
|
+
async function fetchRunnerLog(sessionId, runId, params = {}, signal) {
|
|
404
|
+
const result = await api.session(sessionId).log({
|
|
405
|
+
params: { [RUN_ID_PARAM]: runId },
|
|
406
|
+
query: {
|
|
407
|
+
[RUNNER_LOG_PARAMS.lines]: params.lines,
|
|
408
|
+
[RUNNER_LOG_PARAMS.grep]: params.grep === "" ? void 0 : params.grep,
|
|
409
|
+
[RUNNER_LOG_PARAMS.ignoreCase]: params.ignoreCase === true ? true : void 0,
|
|
410
|
+
[RUNNER_LOG_PARAMS.contextLines]: params.contextLines
|
|
411
|
+
},
|
|
412
|
+
...signal === void 0 ? {} : { signal }
|
|
413
|
+
});
|
|
414
|
+
if (!result.ok) {
|
|
415
|
+
if (result.status === 0) return { error: signal?.aborted === true ? "" : UNREACHABLE };
|
|
416
|
+
return { error: result.error === "" ? `The hub answered ${String(result.status)}.` : result.error };
|
|
417
|
+
}
|
|
418
|
+
if (!isRecord$2(result.data) || typeof result.data.text !== "string") return { error: `The hub answered ${String(result.status)}.` };
|
|
419
|
+
return { slice: result.data };
|
|
420
|
+
}
|
|
421
|
+
/** Attempts before a dropped stream is reported as lost rather than retried. */
|
|
422
|
+
const MAX_RETRIES$1 = 5;
|
|
423
|
+
const RETRY_BASE_MS$1 = 500;
|
|
424
|
+
/**
|
|
425
|
+
* Follows a running log from the offset already read, so no line is missed
|
|
426
|
+
* between the slice and the stream. The stream never filters; a page with a
|
|
427
|
+
* query stops following and reads a filtered slice instead.
|
|
428
|
+
*
|
|
429
|
+
* A dropped connection is retried with backoff from the offset the last event
|
|
430
|
+
* reported, not from the offset the page started at, so a reconnect resumes
|
|
431
|
+
* instead of replaying. The browser's own EventSource retry cannot do this:
|
|
432
|
+
* it reopens the URL it was given, whose offset is stale the moment a line
|
|
433
|
+
* arrives, so each of its reconnects would duplicate everything since.
|
|
434
|
+
*/
|
|
435
|
+
function followRunnerLog(sessionId, runId, from, handlers) {
|
|
436
|
+
let offset = from;
|
|
437
|
+
let retries = 0;
|
|
438
|
+
let closed = false;
|
|
439
|
+
let source;
|
|
440
|
+
let timer;
|
|
441
|
+
const open = () => {
|
|
442
|
+
if (closed) return;
|
|
443
|
+
const current = new EventSource(api.session(sessionId).logStream.url({
|
|
444
|
+
params: { [RUN_ID_PARAM]: runId },
|
|
445
|
+
query: { [RUNNER_LOG_PARAMS.from]: offset }
|
|
446
|
+
}));
|
|
447
|
+
source = current;
|
|
448
|
+
current.addEventListener(RUNNER_LOG_STREAM_EVENT, (message) => {
|
|
449
|
+
let parsed;
|
|
450
|
+
try {
|
|
451
|
+
parsed = JSON.parse(message.data);
|
|
452
|
+
} catch {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (!isRecord$2(parsed) || !Array.isArray(parsed.lines)) return;
|
|
456
|
+
const event = parsed;
|
|
457
|
+
if (typeof event.offset === "number") offset = event.offset;
|
|
458
|
+
retries = 0;
|
|
459
|
+
if (event.ended === true) {
|
|
460
|
+
closed = true;
|
|
461
|
+
current.close();
|
|
462
|
+
}
|
|
463
|
+
handlers.onEvent(event);
|
|
464
|
+
});
|
|
465
|
+
current.addEventListener("error", () => {
|
|
466
|
+
current.close();
|
|
467
|
+
if (closed) return;
|
|
468
|
+
retries += 1;
|
|
469
|
+
if (retries > MAX_RETRIES$1) {
|
|
470
|
+
handlers.onLost();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
timer = setTimeout(open, RETRY_BASE_MS$1 * 2 ** (retries - 1));
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
open();
|
|
477
|
+
return { close: () => {
|
|
478
|
+
closed = true;
|
|
479
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
480
|
+
source?.close();
|
|
481
|
+
} };
|
|
482
|
+
}
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/RunnerLogPanel.tsx
|
|
485
|
+
const TICK_MS$2 = 1e4;
|
|
486
|
+
/** The tab id doubles as the URL segment, so it stays plain and unique across plugins. */
|
|
487
|
+
const TAB_ID_PREFIX$1 = "runner-log-";
|
|
488
|
+
/** Lines held in the view; a follow that ran all day must not grow without bound. */
|
|
489
|
+
const MAX_VIEW_LINES = 2e3;
|
|
490
|
+
/** Context lines each side of a match, the same either-side window grep would use. */
|
|
491
|
+
const CONTEXT_LINES = 2;
|
|
492
|
+
/** A query is typed a character at a time; each one costs a whole-file read on the hub. */
|
|
493
|
+
const SEARCH_DEBOUNCE_MS = 200;
|
|
494
|
+
/** How close to the bottom still counts as "at the bottom" for following. */
|
|
495
|
+
const NEAR_BOTTOM_PX = 24;
|
|
496
|
+
const BYTES_PER_KB$1 = 1024;
|
|
497
|
+
const BYTES_PER_MB$1 = BYTES_PER_KB$1 * BYTES_PER_KB$1;
|
|
498
|
+
/** The temporary tab for one runner's log; the host keeps this panel while the tab is open. */
|
|
499
|
+
function runnerLogTab(run) {
|
|
500
|
+
const runId = run.id;
|
|
501
|
+
return {
|
|
502
|
+
id: `${TAB_ID_PREFIX$1}${runId}`,
|
|
503
|
+
label: `log · ${run.name}`,
|
|
504
|
+
panel: (props) => /* @__PURE__ */ jsx(RunnerLogPanel, {
|
|
505
|
+
...props,
|
|
506
|
+
runId
|
|
507
|
+
})
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function formatSize$1(bytes) {
|
|
511
|
+
if (bytes < BYTES_PER_KB$1) return `${bytes} B`;
|
|
512
|
+
if (bytes < BYTES_PER_MB$1) return `${(bytes / BYTES_PER_KB$1).toFixed(1)} KB`;
|
|
513
|
+
return `${(bytes / BYTES_PER_MB$1).toFixed(1)} MB`;
|
|
514
|
+
}
|
|
515
|
+
function badgeOf(run) {
|
|
516
|
+
if (run === void 0) return {
|
|
517
|
+
tone: "neutral",
|
|
518
|
+
label: "gone"
|
|
519
|
+
};
|
|
520
|
+
if (run.state === "running") return {
|
|
521
|
+
tone: "running",
|
|
522
|
+
label: "running"
|
|
523
|
+
};
|
|
524
|
+
if (run.exit === void 0 || run.exit.reason === "stopped") return {
|
|
525
|
+
tone: "neutral",
|
|
526
|
+
label: "stopped"
|
|
527
|
+
};
|
|
528
|
+
return run.exit.reason === "completed" && (run.exit.code === 0 || run.exit.code === null) ? {
|
|
529
|
+
tone: "ok",
|
|
530
|
+
label: "done"
|
|
531
|
+
} : {
|
|
532
|
+
tone: "error",
|
|
533
|
+
label: run.exit.reason.replace("_", " ")
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
/** Whether one line is a match rather than the context printed around one. */
|
|
537
|
+
function isMatch(line, needle, ignoreCase) {
|
|
538
|
+
if (needle === "") return false;
|
|
539
|
+
return ignoreCase ? line.toLowerCase().includes(needle.toLowerCase()) : line.includes(needle);
|
|
540
|
+
}
|
|
541
|
+
/** One fact of the runner, on its own line so a long command is never elided. */
|
|
542
|
+
function MetaRow({ label, value }) {
|
|
543
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
544
|
+
className: "flex min-w-0 gap-2.5",
|
|
545
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
546
|
+
className: "w-14 shrink-0 text-base text-doom-dim",
|
|
547
|
+
children: label
|
|
548
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
549
|
+
className: "min-w-0 flex-1 break-all font-mono text-sm text-doom-dim",
|
|
550
|
+
children: value
|
|
551
|
+
})]
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* One rendered log line: its number, whether it matched, and the text.
|
|
556
|
+
*
|
|
557
|
+
* A row per line rather than one block of joined text. The block form re-parsed
|
|
558
|
+
* every line's escapes whenever a single line arrived, because the parse was
|
|
559
|
+
* keyed on the joined string; keyed by line number instead, an append only
|
|
560
|
+
* costs the line that appended.
|
|
561
|
+
*/
|
|
562
|
+
function LogRow({ number, line, matched }) {
|
|
563
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
564
|
+
"data-testid": "runner-log-row",
|
|
565
|
+
"data-matched": matched,
|
|
566
|
+
className: "flex min-w-0 gap-2",
|
|
567
|
+
children: [
|
|
568
|
+
number === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
569
|
+
className: "w-12 shrink-0 select-none text-right text-xs leading-relaxed text-doom-faint",
|
|
570
|
+
children: number
|
|
571
|
+
}),
|
|
572
|
+
/* @__PURE__ */ jsx("span", {
|
|
573
|
+
"aria-hidden": true,
|
|
574
|
+
className: `w-4 shrink-0 select-none text-xs leading-relaxed ${matched ? "text-doom-yellow" : "text-doom-faint"}`,
|
|
575
|
+
children: matched ? ">>" : ""
|
|
576
|
+
}),
|
|
577
|
+
/* @__PURE__ */ jsx(AnsiLine, {
|
|
578
|
+
line,
|
|
579
|
+
className: `min-w-0 flex-1 whitespace-pre-wrap break-words font-mono text-xs leading-relaxed ${matched ? "text-doom-text" : "text-doom-dim"}`
|
|
580
|
+
})
|
|
581
|
+
]
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* One runner's log: the bounded tail, following while the runner writes, and
|
|
586
|
+
* a grep over the whole file when the reader types a query.
|
|
587
|
+
*
|
|
588
|
+
* Search is not find-next. The reader matches a literal substring and answers
|
|
589
|
+
* with the matching lines and their context, so a query replaces the view
|
|
590
|
+
* rather than moving a cursor through it, and following pauses while one is
|
|
591
|
+
* set because a filtered view is a snapshot of a file that is still growing.
|
|
592
|
+
*/
|
|
593
|
+
function RunnerLogPanel({ sessionId, runId, sendSessionFrame }) {
|
|
594
|
+
const { runs, stopRequested } = useStore(runners.store, (state) => runners.select(state, sessionId));
|
|
595
|
+
const run = runs.find((candidate) => candidate.id === runId);
|
|
596
|
+
const [now, setNow] = useState(() => Date.now());
|
|
597
|
+
const [typed, setTyped] = useState("");
|
|
598
|
+
const [query, setQuery] = useState("");
|
|
599
|
+
const [ignoreCase, setIgnoreCase] = useState(false);
|
|
600
|
+
const [context, setContext] = useState(CONTEXT_LINES);
|
|
601
|
+
const [following, setFollowing] = useState(true);
|
|
602
|
+
const [slice, setSlice] = useState(void 0);
|
|
603
|
+
const [streamFrom, setStreamFrom] = useState(void 0);
|
|
604
|
+
const [appended, setAppended] = useState([]);
|
|
605
|
+
const [error, setError] = useState(void 0);
|
|
606
|
+
/** False once the reader scrolls up: following must not yank them back down. */
|
|
607
|
+
const [pinned, setPinned] = useState(true);
|
|
608
|
+
const bottom = useRef(null);
|
|
609
|
+
const body = useRef(null);
|
|
610
|
+
const filtering = query !== "";
|
|
611
|
+
const live = isFollowingLive(following, filtering, slice?.running === true);
|
|
612
|
+
useEffect(() => {
|
|
613
|
+
const timer = setInterval(() => setNow(Date.now()), TICK_MS$2);
|
|
614
|
+
return () => clearInterval(timer);
|
|
615
|
+
}, []);
|
|
616
|
+
useEffect(() => {
|
|
617
|
+
const timer = setTimeout(() => setQuery(typed), SEARCH_DEBOUNCE_MS);
|
|
618
|
+
return () => clearTimeout(timer);
|
|
619
|
+
}, [typed]);
|
|
620
|
+
const load = useCallback((signal) => {
|
|
621
|
+
if (sessionId === null) return;
|
|
622
|
+
fetchRunnerLog(sessionId, runId, filtering ? {
|
|
623
|
+
grep: query,
|
|
624
|
+
ignoreCase,
|
|
625
|
+
contextLines: context
|
|
626
|
+
} : {}, signal).then((result) => {
|
|
627
|
+
if (signal.aborted) return;
|
|
628
|
+
if ("error" in result) {
|
|
629
|
+
if (result.error !== "") setError(result.error);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
setError(void 0);
|
|
633
|
+
setAppended([]);
|
|
634
|
+
setSlice(result.slice);
|
|
635
|
+
setStreamFrom(result.slice.completeBytes);
|
|
636
|
+
});
|
|
637
|
+
}, [
|
|
638
|
+
sessionId,
|
|
639
|
+
runId,
|
|
640
|
+
filtering,
|
|
641
|
+
query,
|
|
642
|
+
ignoreCase,
|
|
643
|
+
context
|
|
644
|
+
]);
|
|
645
|
+
useEffect(() => {
|
|
646
|
+
const controller = new AbortController();
|
|
647
|
+
load(controller.signal);
|
|
648
|
+
return () => controller.abort();
|
|
649
|
+
}, [load]);
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
if (!live || sessionId === null || streamFrom === void 0) return;
|
|
652
|
+
const follow = followRunnerLog(sessionId, runId, streamFrom, {
|
|
653
|
+
onEvent: (event) => {
|
|
654
|
+
if (event.lines.length > 0) setAppended((current) => [...current, ...event.lines].slice(-2e3));
|
|
655
|
+
if (event.ended === true) setFollowing(false);
|
|
656
|
+
},
|
|
657
|
+
onLost: () => setFollowing(false)
|
|
658
|
+
});
|
|
659
|
+
return () => follow.close();
|
|
660
|
+
}, [
|
|
661
|
+
live,
|
|
662
|
+
sessionId,
|
|
663
|
+
runId,
|
|
664
|
+
streamFrom
|
|
665
|
+
]);
|
|
666
|
+
const partialTail = slice !== void 0 && slice.exists && slice.completeBytes < slice.fileSize;
|
|
667
|
+
const view = logViewLines(slice?.text ?? "", appended, MAX_VIEW_LINES, { dropPartialTail: live && partialTail });
|
|
668
|
+
const numbers = useMemo(() => {
|
|
669
|
+
if (slice === void 0) return [];
|
|
670
|
+
if (filtering) return slice.lineNumbers ?? [];
|
|
671
|
+
return tailLineNumbers(slice.totalLines + appended.length - (live && partialTail ? 1 : 0), view.lines.length);
|
|
672
|
+
}, [
|
|
673
|
+
slice,
|
|
674
|
+
filtering,
|
|
675
|
+
appended.length,
|
|
676
|
+
live,
|
|
677
|
+
partialTail,
|
|
678
|
+
view.lines.length
|
|
679
|
+
]);
|
|
680
|
+
useEffect(() => {
|
|
681
|
+
if (live && pinned) bottom.current?.scrollIntoView({ block: "end" });
|
|
682
|
+
}, [
|
|
683
|
+
live,
|
|
684
|
+
pinned,
|
|
685
|
+
view.lines.length
|
|
686
|
+
]);
|
|
687
|
+
const onScroll = () => {
|
|
688
|
+
const element = body.current;
|
|
689
|
+
if (element === null) return;
|
|
690
|
+
setPinned(element.scrollHeight - element.scrollTop - element.clientHeight <= NEAR_BOTTOM_PX);
|
|
691
|
+
};
|
|
692
|
+
const badge = badgeOf(run);
|
|
693
|
+
const stopping = run !== void 0 && stopRequested.includes(run.id);
|
|
694
|
+
const elapsed = run === void 0 ? "" : formatRunnerUptime(run.startedAt, run.exit ? Date.parse(run.exit.finishedAt) : now);
|
|
695
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
696
|
+
"data-testid": "runner-log-panel",
|
|
697
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
698
|
+
children: [
|
|
699
|
+
/* @__PURE__ */ jsxs("div", {
|
|
700
|
+
className: "flex min-h-11 shrink-0 flex-wrap items-center gap-2 border-b border-doom-border-soft px-3 py-2 sm:h-11 sm:flex-nowrap sm:gap-2.5 sm:px-[26px] sm:py-0",
|
|
701
|
+
children: [
|
|
702
|
+
/* @__PURE__ */ jsx("span", {
|
|
703
|
+
"data-testid": "runner-log-name",
|
|
704
|
+
className: "shrink-0 truncate text-base font-semibold text-doom-hi",
|
|
705
|
+
children: run?.name ?? runId
|
|
706
|
+
}),
|
|
707
|
+
/* @__PURE__ */ jsx(StatusBadge, {
|
|
708
|
+
tone: badge.tone,
|
|
709
|
+
"data-testid": "runner-log-state",
|
|
710
|
+
children: badge.label
|
|
711
|
+
}),
|
|
712
|
+
run ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
713
|
+
className: "shrink-0 text-sm text-doom-dim",
|
|
714
|
+
children: elapsed
|
|
715
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
716
|
+
className: "shrink-0 text-sm text-doom-dim",
|
|
717
|
+
children: `pid ${String(run.pid)} · ${run.backend}${run.interactive ? " · tty" : ""}`
|
|
718
|
+
})] }) : /* @__PURE__ */ jsx("span", {
|
|
719
|
+
"data-testid": "runner-log-gone",
|
|
720
|
+
className: "text-sm text-doom-dim",
|
|
721
|
+
children: "no longer listed; its log stays readable"
|
|
722
|
+
}),
|
|
723
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
|
|
724
|
+
run !== void 0 && run.state === "running" && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
|
|
725
|
+
variant: stopping ? "outline" : "danger-outline",
|
|
726
|
+
size: "xs",
|
|
727
|
+
"data-testid": "runner-log-stop",
|
|
728
|
+
disabled: stopping,
|
|
729
|
+
title: stopping ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
|
|
730
|
+
onClick: () => requestRunnerStop(sendSessionFrame, sessionId, run.id),
|
|
731
|
+
className: "px-2 ",
|
|
732
|
+
children: stopping ? "stopping…" : "stop"
|
|
733
|
+
}) : null
|
|
734
|
+
]
|
|
735
|
+
}),
|
|
736
|
+
run ? /* @__PURE__ */ jsxs("div", {
|
|
737
|
+
className: "flex shrink-0 flex-col gap-1.5 border-b border-doom-border-soft px-3 py-2.5 sm:px-[26px]",
|
|
738
|
+
children: [/* @__PURE__ */ jsx(MetaRow, {
|
|
739
|
+
label: "command",
|
|
740
|
+
value: run.command
|
|
741
|
+
}), /* @__PURE__ */ jsx(MetaRow, {
|
|
742
|
+
label: "cwd",
|
|
743
|
+
value: run.cwd
|
|
744
|
+
})]
|
|
745
|
+
}) : null,
|
|
746
|
+
/* @__PURE__ */ jsxs("div", {
|
|
747
|
+
className: "flex min-h-10 shrink-0 flex-wrap items-center gap-2 border-b border-doom-border-soft px-3 py-2 sm:h-10 sm:flex-nowrap sm:gap-2.5 sm:px-[26px] sm:py-0",
|
|
748
|
+
children: [
|
|
749
|
+
/* @__PURE__ */ jsxs("span", {
|
|
750
|
+
className: "relative flex min-w-0 flex-1 items-center sm:w-[300px] sm:flex-none",
|
|
751
|
+
children: [/* @__PURE__ */ jsx(SearchIcon, { className: "pointer-events-none absolute left-2.5 h-3 w-3 text-doom-faint" }), /* @__PURE__ */ jsx(Input, {
|
|
752
|
+
size: "sm",
|
|
753
|
+
"data-testid": "runner-log-search",
|
|
754
|
+
value: typed,
|
|
755
|
+
placeholder: "search this log",
|
|
756
|
+
onChange: (event) => setTyped(event.target.value),
|
|
757
|
+
className: "pl-7 "
|
|
758
|
+
})]
|
|
759
|
+
}),
|
|
760
|
+
/* @__PURE__ */ jsx(Button, {
|
|
761
|
+
variant: ignoreCase ? "subtle" : "outline",
|
|
762
|
+
size: "xs",
|
|
763
|
+
"data-testid": "runner-log-ignore-case",
|
|
764
|
+
"aria-pressed": ignoreCase,
|
|
765
|
+
title: "match regardless of case",
|
|
766
|
+
onClick: () => setIgnoreCase((current) => !current),
|
|
767
|
+
className: "px-2 ",
|
|
768
|
+
children: "aA"
|
|
769
|
+
}),
|
|
770
|
+
/* @__PURE__ */ jsx(Button, {
|
|
771
|
+
variant: context > 0 && filtering ? "subtle" : "outline",
|
|
772
|
+
size: "xs",
|
|
773
|
+
"data-testid": "runner-log-context",
|
|
774
|
+
disabled: !filtering,
|
|
775
|
+
title: "lines of context kept either side of a match",
|
|
776
|
+
onClick: () => setContext((current) => current === 0 ? CONTEXT_LINES : 0),
|
|
777
|
+
className: "px-2 ",
|
|
778
|
+
children: `± ${String(context)} lines`
|
|
779
|
+
}),
|
|
780
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
|
|
781
|
+
/* @__PURE__ */ jsxs("span", {
|
|
782
|
+
"data-testid": "runner-log-stats",
|
|
783
|
+
className: "min-w-0 basis-full truncate text-sm text-doom-dim sm:basis-auto",
|
|
784
|
+
children: [slice === void 0 ? "reading…" : filtering ? `${String(slice.lineCount)} matching of ${slice.totalLines.toLocaleString("en-US")} lines` : `${live ? "tailing" : "showing"} last ${String(view.lines.length)} of ${slice.totalLines.toLocaleString("en-US")} lines`, view.hidden > 0 ? ` · ${view.hidden.toLocaleString("en-US")} older hidden` : ""]
|
|
785
|
+
}),
|
|
786
|
+
/* @__PURE__ */ jsx(Button, {
|
|
787
|
+
variant: live ? "subtle" : "outline",
|
|
788
|
+
size: "xs",
|
|
789
|
+
"data-testid": "runner-log-follow",
|
|
790
|
+
"aria-pressed": live,
|
|
791
|
+
disabled: filtering || slice?.running !== true,
|
|
792
|
+
title: filtering ? "a filtered view is a snapshot; clear the query to follow" : "follow the log as it grows",
|
|
793
|
+
onClick: () => setFollowing((current) => !current),
|
|
794
|
+
className: "px-2 ",
|
|
795
|
+
children: filtering ? "follow · paused" : "follow"
|
|
796
|
+
})
|
|
797
|
+
]
|
|
798
|
+
}),
|
|
799
|
+
/* @__PURE__ */ jsxs("div", {
|
|
800
|
+
className: "relative min-h-0 flex-1",
|
|
801
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
802
|
+
ref: body,
|
|
803
|
+
onScroll,
|
|
804
|
+
"data-testid": "runner-log-body",
|
|
805
|
+
className: "h-full overflow-auto bg-doom-deep px-3 py-3 sm:px-[26px]",
|
|
806
|
+
children: [error !== void 0 ? /* @__PURE__ */ jsx("p", {
|
|
807
|
+
"data-testid": "runner-log-error",
|
|
808
|
+
className: "text-base text-doom-red",
|
|
809
|
+
children: error
|
|
810
|
+
}) : slice === void 0 ? /* @__PURE__ */ jsx("p", {
|
|
811
|
+
"data-testid": "runner-log-reading",
|
|
812
|
+
className: "text-sm text-doom-dim",
|
|
813
|
+
children: "reading…"
|
|
814
|
+
}) : slice.exists === false ? /* @__PURE__ */ jsx("p", {
|
|
815
|
+
className: "text-sm text-doom-dim",
|
|
816
|
+
children: "this runner has not written a log yet"
|
|
817
|
+
}) : view.lines.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
818
|
+
className: "text-sm text-doom-dim",
|
|
819
|
+
children: filtering ? "nothing matched" : "the log is empty"
|
|
820
|
+
}) : view.lines.map((line, index) => {
|
|
821
|
+
const number = numbers[index];
|
|
822
|
+
const previous = numbers[index - 1];
|
|
823
|
+
const gap = previous !== void 0 && number !== void 0 && number - previous > 1;
|
|
824
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [gap ? /* @__PURE__ */ jsx("div", {
|
|
825
|
+
"data-testid": "runner-log-gap",
|
|
826
|
+
"aria-hidden": true,
|
|
827
|
+
className: "select-none py-0.5 pl-12 text-xs leading-relaxed text-doom-faint",
|
|
828
|
+
children: "⋯"
|
|
829
|
+
}) : null, /* @__PURE__ */ jsx(LogRow, {
|
|
830
|
+
number,
|
|
831
|
+
line,
|
|
832
|
+
matched: filtering && isMatch(line, query, ignoreCase)
|
|
833
|
+
})] }, number ?? `row-${String(index)}`);
|
|
834
|
+
}), /* @__PURE__ */ jsx("div", { ref: bottom })]
|
|
835
|
+
}), live && !pinned ? /* @__PURE__ */ jsx(Button, {
|
|
836
|
+
variant: "subtle",
|
|
837
|
+
size: "xs",
|
|
838
|
+
"data-testid": "runner-log-jump",
|
|
839
|
+
title: "scroll back to the newest lines and keep following",
|
|
840
|
+
onClick: () => {
|
|
841
|
+
setPinned(true);
|
|
842
|
+
bottom.current?.scrollIntoView({ block: "end" });
|
|
843
|
+
},
|
|
844
|
+
className: "absolute bottom-3 right-3 px-2 ",
|
|
845
|
+
children: "jump to latest"
|
|
846
|
+
}) : null]
|
|
847
|
+
}),
|
|
848
|
+
/* @__PURE__ */ jsxs("div", {
|
|
849
|
+
className: "flex h-8 shrink-0 items-center gap-2.5 border-t border-doom-border-soft px-3 sm:px-[26px]",
|
|
850
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
851
|
+
"data-testid": "runner-log-path",
|
|
852
|
+
className: "min-w-0 flex-1 truncate font-mono text-sm text-doom-dim",
|
|
853
|
+
children: run?.logPath ?? slice?.path ?? ""
|
|
854
|
+
}), slice !== void 0 ? /* @__PURE__ */ jsx("span", {
|
|
855
|
+
className: "shrink-0 text-sm text-doom-dim",
|
|
856
|
+
children: formatSize$1(slice.fileSize)
|
|
857
|
+
}) : null]
|
|
858
|
+
})
|
|
859
|
+
]
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/screenApi.ts
|
|
864
|
+
/**
|
|
865
|
+
* The page's half of the attached pane.
|
|
866
|
+
*
|
|
867
|
+
* Two directions over two mechanisms, because the hub proxies requests and
|
|
868
|
+
* server-sent events but cannot upgrade a socket: pane bytes arrive on an
|
|
869
|
+
* EventSource, and keystrokes go back as ordinary POSTs. Both addresses come
|
|
870
|
+
* from the generated client: the stream route offers a URL and no call method,
|
|
871
|
+
* which is exactly what an EventSource wants and all it can be given.
|
|
872
|
+
*/
|
|
873
|
+
/** Turns one base64 chunk back into the bytes a terminal was sent. */
|
|
874
|
+
function decodeChunk(chunk) {
|
|
875
|
+
const binary = atob(chunk);
|
|
876
|
+
const bytes = new Uint8Array(binary.length);
|
|
877
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
878
|
+
return bytes;
|
|
879
|
+
}
|
|
880
|
+
/** Attempts before a dropped screen stream is reported as lost rather than retried. */
|
|
881
|
+
const MAX_RETRIES = 5;
|
|
882
|
+
const RETRY_BASE_MS = 500;
|
|
883
|
+
function isRecord$1(value) {
|
|
884
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
885
|
+
}
|
|
886
|
+
function watchRunnerScreen(sessionId, runId, from, handlers) {
|
|
887
|
+
let offset = from;
|
|
888
|
+
let retries = 0;
|
|
889
|
+
let closed = false;
|
|
890
|
+
let source;
|
|
891
|
+
let timer;
|
|
892
|
+
const open = () => {
|
|
893
|
+
if (closed) return;
|
|
894
|
+
const current = new EventSource(api.session(sessionId).screenStream.url({
|
|
895
|
+
params: { [RUN_ID_PARAM]: runId },
|
|
896
|
+
query: { [RUNNER_LOG_PARAMS.from]: offset }
|
|
897
|
+
}));
|
|
898
|
+
source = current;
|
|
899
|
+
current.addEventListener(RUNNER_SCREEN_EVENT, (message) => {
|
|
900
|
+
let parsed;
|
|
901
|
+
try {
|
|
902
|
+
parsed = JSON.parse(message.data);
|
|
903
|
+
} catch {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (!isRecord$1(parsed) || typeof parsed.chunk !== "string") return;
|
|
907
|
+
const event = parsed;
|
|
908
|
+
if (typeof event.offset === "number") offset = event.offset;
|
|
909
|
+
retries = 0;
|
|
910
|
+
if (event.ended === true) {
|
|
911
|
+
closed = true;
|
|
912
|
+
current.close();
|
|
913
|
+
}
|
|
914
|
+
handlers.onEvent(event);
|
|
915
|
+
});
|
|
916
|
+
current.addEventListener("error", () => {
|
|
917
|
+
current.close();
|
|
918
|
+
if (closed) return;
|
|
919
|
+
retries += 1;
|
|
920
|
+
if (retries > MAX_RETRIES) {
|
|
921
|
+
handlers.onLost();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
timer = setTimeout(open, RETRY_BASE_MS * 2 ** (retries - 1));
|
|
925
|
+
});
|
|
926
|
+
};
|
|
927
|
+
open();
|
|
928
|
+
return { close: () => {
|
|
929
|
+
closed = true;
|
|
930
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
931
|
+
source?.close();
|
|
932
|
+
} };
|
|
933
|
+
}
|
|
934
|
+
/** Sends one batch of keystrokes. Resolves to whether the pane took them. */
|
|
935
|
+
async function sendRunnerInput(sessionId, runId, text) {
|
|
936
|
+
const body = { text };
|
|
937
|
+
return (await api.session(sessionId).input({
|
|
938
|
+
params: { [RUN_ID_PARAM]: runId },
|
|
939
|
+
body
|
|
940
|
+
})).ok;
|
|
941
|
+
}
|
|
942
|
+
//#endregion
|
|
943
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/RunnerShellPanel.tsx
|
|
944
|
+
const TAB_ID_PREFIX = "runner-shell-";
|
|
945
|
+
/** Keystrokes are gathered for this long and sent as one request. */
|
|
946
|
+
const FLUSH_MS = 30;
|
|
947
|
+
/** The temporary tab holding one interactive runner's pane. */
|
|
948
|
+
function runnerShellTab(run) {
|
|
949
|
+
const runId = run.id;
|
|
950
|
+
return {
|
|
951
|
+
id: `${TAB_ID_PREFIX}${runId}`,
|
|
952
|
+
label: `shell · ${run.name}`,
|
|
953
|
+
panel: (props) => /* @__PURE__ */ jsx(RunnerShellPanel, {
|
|
954
|
+
...props,
|
|
955
|
+
runId
|
|
956
|
+
})
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* An interactive runner's pane, attached.
|
|
961
|
+
*
|
|
962
|
+
* A real terminal emulator, fed the pane's own bytes. The runner's sink keeps
|
|
963
|
+
* an unscrubbed copy of its output beside the log for exactly this: the log is
|
|
964
|
+
* stripped of cursor movement so it stays worth grepping, which is precisely
|
|
965
|
+
* what a terminal needs. Because these are the original bytes, colour, cursor
|
|
966
|
+
* placement and full-screen programs all render the way they would in a shell.
|
|
967
|
+
*
|
|
968
|
+
* xterm is loaded when the tab opens rather than with the plugin. Most sessions
|
|
969
|
+
* never attach to anything, and an emulator is not a small thing to carry.
|
|
970
|
+
*/
|
|
971
|
+
function RunnerShellPanel({ sessionId, runId }) {
|
|
972
|
+
const { runs } = useStore(runners.store, (state) => runners.select(state, sessionId));
|
|
973
|
+
const run = runs.find((candidate) => candidate.id === runId);
|
|
974
|
+
const [ended, setEnded] = useState(false);
|
|
975
|
+
const [lost, setLost] = useState(false);
|
|
976
|
+
const [ready, setReady] = useState(false);
|
|
977
|
+
const terminal = useRef(null);
|
|
978
|
+
const pending = useRef([]);
|
|
979
|
+
const flush = useRef(void 0);
|
|
980
|
+
useEffect(() => {
|
|
981
|
+
if (sessionId === null || !ready) return;
|
|
982
|
+
const watch = watchRunnerScreen(sessionId, runId, 0, {
|
|
983
|
+
onEvent: (event) => {
|
|
984
|
+
if (event.chunk !== "") terminal.current?.write(decodeChunk(event.chunk));
|
|
985
|
+
if (event.ended === true) setEnded(true);
|
|
986
|
+
},
|
|
987
|
+
onLost: () => setLost(true)
|
|
988
|
+
});
|
|
989
|
+
return () => watch.close();
|
|
990
|
+
}, [
|
|
991
|
+
sessionId,
|
|
992
|
+
runId,
|
|
993
|
+
ready
|
|
994
|
+
]);
|
|
995
|
+
useEffect(() => () => {
|
|
996
|
+
if (flush.current !== void 0) clearTimeout(flush.current);
|
|
997
|
+
}, []);
|
|
998
|
+
const send = (text) => {
|
|
999
|
+
if (sessionId === null) return;
|
|
1000
|
+
pending.current.push(text);
|
|
1001
|
+
if (flush.current !== void 0) return;
|
|
1002
|
+
flush.current = setTimeout(() => {
|
|
1003
|
+
const batch = pending.current.join("");
|
|
1004
|
+
pending.current = [];
|
|
1005
|
+
flush.current = void 0;
|
|
1006
|
+
if (batch !== "") sendRunnerInput(sessionId, runId, batch);
|
|
1007
|
+
}, FLUSH_MS);
|
|
1008
|
+
};
|
|
1009
|
+
const live = run?.state === "running" && !ended;
|
|
1010
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1011
|
+
"data-testid": "runner-shell-panel",
|
|
1012
|
+
className: "flex min-h-0 flex-1 flex-col",
|
|
1013
|
+
children: [
|
|
1014
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1015
|
+
className: "flex min-h-11 shrink-0 items-center gap-2.5 border-b border-doom-border-soft px-3 sm:h-11 sm:px-[26px]",
|
|
1016
|
+
children: [
|
|
1017
|
+
/* @__PURE__ */ jsx("span", {
|
|
1018
|
+
"data-testid": "runner-shell-name",
|
|
1019
|
+
className: "shrink-0 truncate text-base font-semibold text-doom-hi",
|
|
1020
|
+
children: run?.name ?? runId
|
|
1021
|
+
}),
|
|
1022
|
+
/* @__PURE__ */ jsx(StatusBadge, {
|
|
1023
|
+
tone: live ? "running" : "neutral",
|
|
1024
|
+
"data-testid": "runner-shell-state",
|
|
1025
|
+
children: live ? "attached" : "detached"
|
|
1026
|
+
}),
|
|
1027
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
|
|
1028
|
+
lost ? /* @__PURE__ */ jsx("span", {
|
|
1029
|
+
"data-testid": "runner-shell-lost",
|
|
1030
|
+
className: "shrink-0 text-base text-doom-red",
|
|
1031
|
+
children: "the connection to this pane dropped"
|
|
1032
|
+
}) : null,
|
|
1033
|
+
live ? /* @__PURE__ */ jsx(Button, {
|
|
1034
|
+
variant: "outline",
|
|
1035
|
+
size: "xs",
|
|
1036
|
+
"data-testid": "runner-shell-interrupt",
|
|
1037
|
+
title: "send ctrl-c",
|
|
1038
|
+
onClick: () => send(""),
|
|
1039
|
+
className: "px-2 ",
|
|
1040
|
+
children: "ctrl-c"
|
|
1041
|
+
}) : null
|
|
1042
|
+
]
|
|
1043
|
+
}),
|
|
1044
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1045
|
+
className: "relative min-h-0 flex-1 overflow-hidden bg-doom-deep px-3 py-2 sm:px-[26px]",
|
|
1046
|
+
children: [/* @__PURE__ */ jsx(TerminalView, {
|
|
1047
|
+
ref: terminal,
|
|
1048
|
+
"data-testid": "runner-shell-terminal",
|
|
1049
|
+
onData: (text) => send(text),
|
|
1050
|
+
onReady: () => setReady(true)
|
|
1051
|
+
}), ready ? null : /* @__PURE__ */ jsx("p", {
|
|
1052
|
+
"data-testid": "runner-shell-reading",
|
|
1053
|
+
className: "text-sm text-doom-dim",
|
|
1054
|
+
children: "attaching…"
|
|
1055
|
+
})]
|
|
1056
|
+
}),
|
|
1057
|
+
/* @__PURE__ */ jsx("div", {
|
|
1058
|
+
className: "flex h-8 shrink-0 items-center border-t border-doom-border-soft px-3 sm:px-[26px]",
|
|
1059
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1060
|
+
className: "min-w-0 flex-1 truncate text-sm text-doom-dim",
|
|
1061
|
+
children: live ? "typing goes straight to the runner" : "this runner is no longer accepting input"
|
|
1062
|
+
})
|
|
1063
|
+
})
|
|
1064
|
+
]
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/runnerTail.ts
|
|
1069
|
+
/**
|
|
1070
|
+
* The newest line a running runner has written, for a row that has one line to
|
|
1071
|
+
* spend.
|
|
1072
|
+
*
|
|
1073
|
+
* The dock answers "what is happening", and a command that has not changed
|
|
1074
|
+
* since it started answers "what was asked". The last line of the log is the
|
|
1075
|
+
* closest thing to progress the cockpit can show without opening the log, so a
|
|
1076
|
+
* row that is still running trades its command for it.
|
|
1077
|
+
*
|
|
1078
|
+
* The line comes from the same two calls the log panel makes: read the tail,
|
|
1079
|
+
* then follow the stream from exactly where that read ended. Nothing new goes
|
|
1080
|
+
* on the wire, and a runner nobody is looking at is not followed, because the
|
|
1081
|
+
* stream lives and dies with the row.
|
|
1082
|
+
*/
|
|
1083
|
+
/** Enough lines that a trailing blank, or a final progress redraw, is not the whole answer. */
|
|
1084
|
+
const TAIL_REQUEST_LINES = 5;
|
|
1085
|
+
/**
|
|
1086
|
+
* The last line worth showing, as plain text.
|
|
1087
|
+
*
|
|
1088
|
+
* Blank lines are skipped, because a log that ends with a newline would
|
|
1089
|
+
* otherwise report nothing at all. Colour is dropped rather than rendered: the
|
|
1090
|
+
* row is nine pixels of faint text beside a name and a stop control, and a
|
|
1091
|
+
* green tick from someone else's palette in the middle of it reads as damage.
|
|
1092
|
+
*/
|
|
1093
|
+
function plainTailLine(lines) {
|
|
1094
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
1095
|
+
const text = ansiSpans(lines[index] ?? "").map((span) => span.text).join("").trim();
|
|
1096
|
+
if (text.length > 0) return text;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* The newest log line of a running runner, or undefined for a runner that is
|
|
1101
|
+
* finished, unreachable, or has not written anything yet. The caller decides
|
|
1102
|
+
* what to show instead; this hook never invents a placeholder.
|
|
1103
|
+
*/
|
|
1104
|
+
function useRunnerTail(sessionId, runId, running) {
|
|
1105
|
+
const [tail, setTail] = useState(void 0);
|
|
1106
|
+
useEffect(() => {
|
|
1107
|
+
if (!running || sessionId === null) return;
|
|
1108
|
+
let live = true;
|
|
1109
|
+
let follow;
|
|
1110
|
+
const controller = new AbortController();
|
|
1111
|
+
fetchRunnerLog(sessionId, runId, { lines: TAIL_REQUEST_LINES }, controller.signal).then((result) => {
|
|
1112
|
+
if (!live || "error" in result) return;
|
|
1113
|
+
const initial = plainTailLine(result.slice.text.split("\n"));
|
|
1114
|
+
if (initial !== void 0) setTail({
|
|
1115
|
+
runId,
|
|
1116
|
+
text: initial
|
|
1117
|
+
});
|
|
1118
|
+
if (!result.slice.running) return;
|
|
1119
|
+
follow = followRunnerLog(sessionId, runId, result.slice.completeBytes, {
|
|
1120
|
+
onEvent: (event) => {
|
|
1121
|
+
const next = plainTailLine(event.lines);
|
|
1122
|
+
if (live && next !== void 0) setTail({
|
|
1123
|
+
runId,
|
|
1124
|
+
text: next
|
|
1125
|
+
});
|
|
1126
|
+
},
|
|
1127
|
+
onLost: () => void 0
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
return () => {
|
|
1131
|
+
live = false;
|
|
1132
|
+
controller.abort();
|
|
1133
|
+
follow?.close();
|
|
1134
|
+
};
|
|
1135
|
+
}, [
|
|
1136
|
+
sessionId,
|
|
1137
|
+
runId,
|
|
1138
|
+
running
|
|
1139
|
+
]);
|
|
1140
|
+
return running && tail?.runId === runId ? tail.text : void 0;
|
|
1141
|
+
}
|
|
1142
|
+
//#endregion
|
|
1143
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/RunnersPanel.tsx
|
|
1144
|
+
const TICK_MS$1 = 1e4;
|
|
1145
|
+
const SHELL_LAUNCH_TIMEOUT_MS = 3e4;
|
|
1146
|
+
const RUNNERS_TAB_ID = "runner-runs";
|
|
1147
|
+
/** The temporary tab the dock's runners group opens from its own name. */
|
|
1148
|
+
function runnersTab() {
|
|
1149
|
+
return {
|
|
1150
|
+
id: RUNNERS_TAB_ID,
|
|
1151
|
+
label: "runners",
|
|
1152
|
+
panel: RunnersPanel
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Everything this session has up, one card each.
|
|
1157
|
+
*
|
|
1158
|
+
* The dock's rail gives a runner one line, which is enough to notice it and
|
|
1159
|
+
* not enough to work with it. This is the same set with room: the command in
|
|
1160
|
+
* full, where it runs, and the last thing it said.
|
|
1161
|
+
*
|
|
1162
|
+
* Running runners only, the same rule the rail follows. A finished run leaves
|
|
1163
|
+
* the moment it exits, because this answers "what is happening"; its log stays
|
|
1164
|
+
* readable from its own tab, and `doom-runner list` is where history lives.
|
|
1165
|
+
*/
|
|
1166
|
+
function RunnersPanel({ sessionId, sendSessionFrame, openTransientTab }) {
|
|
1167
|
+
const session = useStore(runners.store, (state) => runners.select(state, sessionId));
|
|
1168
|
+
const running = session.runs.filter((run) => run.state === "running");
|
|
1169
|
+
const [now, setNow] = useState(() => Date.now());
|
|
1170
|
+
const [launching, setLaunching] = useState(false);
|
|
1171
|
+
const shellBaseline = useRef(null);
|
|
1172
|
+
const shellLaunchTimeout = useRef(void 0);
|
|
1173
|
+
const defaultCwd = running[0]?.cwd;
|
|
1174
|
+
const startShell = () => {
|
|
1175
|
+
if (sessionId === null) return;
|
|
1176
|
+
shellBaseline.current = new Set(session.runs.map((run) => run.id));
|
|
1177
|
+
clearTimeout(shellLaunchTimeout.current);
|
|
1178
|
+
shellLaunchTimeout.current = setTimeout(() => {
|
|
1179
|
+
shellBaseline.current = null;
|
|
1180
|
+
}, SHELL_LAUNCH_TIMEOUT_MS);
|
|
1181
|
+
requestRunnerStart(sendSessionFrame, sessionId, RUNNER_SHELL_REQUEST);
|
|
1182
|
+
};
|
|
1183
|
+
useEffect(() => {
|
|
1184
|
+
const timer = setInterval(() => setNow(Date.now()), TICK_MS$1);
|
|
1185
|
+
return () => clearInterval(timer);
|
|
1186
|
+
}, []);
|
|
1187
|
+
useEffect(() => {
|
|
1188
|
+
const baseline = shellBaseline.current;
|
|
1189
|
+
if (baseline === null) return;
|
|
1190
|
+
const shell = session.runs.find((run) => run.state === "running" && run.interactive && !baseline.has(run.id));
|
|
1191
|
+
if (!shell) return;
|
|
1192
|
+
shellBaseline.current = null;
|
|
1193
|
+
clearTimeout(shellLaunchTimeout.current);
|
|
1194
|
+
openTransientTab(runnerShellTab(shell));
|
|
1195
|
+
}, [openTransientTab, session.runs]);
|
|
1196
|
+
useEffect(() => () => clearTimeout(shellLaunchTimeout.current), []);
|
|
1197
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1198
|
+
"data-testid": "runners-panel",
|
|
1199
|
+
className: "flex min-h-0 flex-1 flex-col overflow-auto",
|
|
1200
|
+
children: [
|
|
1201
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1202
|
+
className: "flex min-h-11 shrink-0 items-center gap-2.5 border-b border-doom-border-soft px-3 sm:h-11 sm:px-[26px]",
|
|
1203
|
+
children: [
|
|
1204
|
+
/* @__PURE__ */ jsx("span", {
|
|
1205
|
+
className: "text-lg font-semibold text-doom-hi",
|
|
1206
|
+
children: "runners"
|
|
1207
|
+
}),
|
|
1208
|
+
/* @__PURE__ */ jsx("span", {
|
|
1209
|
+
"data-testid": "runners-panel-count",
|
|
1210
|
+
className: "text-sm text-doom-dim",
|
|
1211
|
+
children: running.length === 0 ? "nothing running" : `${String(running.length)} running`
|
|
1212
|
+
}),
|
|
1213
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
|
|
1214
|
+
sessionId === null ? null : /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
1215
|
+
asChild: true,
|
|
1216
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
1217
|
+
variant: "ghost",
|
|
1218
|
+
size: "icon",
|
|
1219
|
+
"data-testid": "runners-panel-launch",
|
|
1220
|
+
title: "launch a runner",
|
|
1221
|
+
"aria-label": "launch a runner",
|
|
1222
|
+
className: "text-doom-faint hover:text-doom-hi",
|
|
1223
|
+
children: /* @__PURE__ */ jsx(PlusIcon, { className: "h-3 w-3" })
|
|
1224
|
+
})
|
|
1225
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
1226
|
+
align: "end",
|
|
1227
|
+
"data-testid": "runners-panel-launch-menu",
|
|
1228
|
+
children: [/* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
1229
|
+
"data-testid": "runners-panel-start-shell",
|
|
1230
|
+
onSelect: startShell,
|
|
1231
|
+
children: "start shell"
|
|
1232
|
+
}), /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
1233
|
+
"data-testid": "runners-panel-run-command",
|
|
1234
|
+
onSelect: () => setLaunching(true),
|
|
1235
|
+
children: "run command"
|
|
1236
|
+
})]
|
|
1237
|
+
})] })
|
|
1238
|
+
]
|
|
1239
|
+
}),
|
|
1240
|
+
running.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
|
|
1241
|
+
"data-testid": "runners-panel-empty",
|
|
1242
|
+
className: "px-4 py-8",
|
|
1243
|
+
title: "nothing running",
|
|
1244
|
+
description: "a command that outlives its turn moves to a runner and appears here while it works.",
|
|
1245
|
+
children: sessionId === null ? null : /* @__PURE__ */ jsx(Button, {
|
|
1246
|
+
variant: "outline",
|
|
1247
|
+
size: "xs",
|
|
1248
|
+
"data-testid": "runners-panel-launch-empty",
|
|
1249
|
+
onClick: () => setLaunching(true),
|
|
1250
|
+
className: "px-2 ",
|
|
1251
|
+
children: "launch a runner"
|
|
1252
|
+
})
|
|
1253
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
1254
|
+
"data-testid": "runners-panel-grid",
|
|
1255
|
+
className: "grid grid-cols-1 gap-2.5 px-3 py-3 sm:grid-cols-2 sm:px-[26px] xl:grid-cols-3",
|
|
1256
|
+
children: running.map((run) => /* @__PURE__ */ jsx(RunnerCard, {
|
|
1257
|
+
run,
|
|
1258
|
+
now,
|
|
1259
|
+
stopRequested: session.stopRequested.includes(run.id),
|
|
1260
|
+
sessionId,
|
|
1261
|
+
sendSessionFrame,
|
|
1262
|
+
openTransientTab
|
|
1263
|
+
}, run.id))
|
|
1264
|
+
}),
|
|
1265
|
+
launching && sessionId !== null ? /* @__PURE__ */ jsx(LaunchRunnerDialog, {
|
|
1266
|
+
sessionId,
|
|
1267
|
+
sendSessionFrame,
|
|
1268
|
+
...defaultCwd === void 0 ? {} : { defaultCwd },
|
|
1269
|
+
onClose: () => setLaunching(false)
|
|
1270
|
+
}) : null
|
|
1271
|
+
]
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* One running runner. Its own component because the tail line is a
|
|
1276
|
+
* subscription per card, and a card that leaves the grid closes it.
|
|
1277
|
+
*/
|
|
1278
|
+
function RunnerCard({ run, now, stopRequested, sessionId, sendSessionFrame, openTransientTab }) {
|
|
1279
|
+
const tail = useRunnerTail(sessionId, run.id, run.state === "running");
|
|
1280
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1281
|
+
"data-testid": `runners-card-${run.id}`,
|
|
1282
|
+
className: "flex min-w-0 flex-col gap-1.5 rounded-md border border-doom-border-soft bg-doom-panel p-2.5",
|
|
1283
|
+
children: [
|
|
1284
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1285
|
+
className: "flex min-w-0 items-center gap-1.5",
|
|
1286
|
+
children: [
|
|
1287
|
+
/* @__PURE__ */ jsx(Dot, {
|
|
1288
|
+
tone: "yellow",
|
|
1289
|
+
pulse: true
|
|
1290
|
+
}),
|
|
1291
|
+
/* @__PURE__ */ jsx("span", {
|
|
1292
|
+
className: "min-w-0 flex-1 truncate text-base font-semibold text-doom-hi",
|
|
1293
|
+
children: run.name
|
|
1294
|
+
}),
|
|
1295
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1296
|
+
className: "shrink-0 text-sm text-doom-dim",
|
|
1297
|
+
children: [formatRunnerUptime(run.startedAt, now), run.interactive ? " · tty" : ""]
|
|
1298
|
+
})
|
|
1299
|
+
]
|
|
1300
|
+
}),
|
|
1301
|
+
/* @__PURE__ */ jsx("span", {
|
|
1302
|
+
className: "min-w-0 break-all font-mono text-sm leading-normal text-doom-dim",
|
|
1303
|
+
children: run.command
|
|
1304
|
+
}),
|
|
1305
|
+
/* @__PURE__ */ jsx("span", {
|
|
1306
|
+
className: "min-w-0 truncate font-mono text-sm text-doom-dim",
|
|
1307
|
+
children: run.cwd
|
|
1308
|
+
}),
|
|
1309
|
+
/* @__PURE__ */ jsx("span", {
|
|
1310
|
+
"data-testid": `runners-card-detail-${run.id}`,
|
|
1311
|
+
"data-detail": tail === void 0 ? "command" : "tail",
|
|
1312
|
+
className: "min-w-0 truncate text-sm text-doom-dim",
|
|
1313
|
+
children: tail ?? "…"
|
|
1314
|
+
}),
|
|
1315
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1316
|
+
className: "flex items-center gap-1.5 pt-0.5",
|
|
1317
|
+
children: [
|
|
1318
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1319
|
+
variant: "outline",
|
|
1320
|
+
size: "xs",
|
|
1321
|
+
"data-testid": `runners-card-log-${run.id}`,
|
|
1322
|
+
title: "open this runner's log",
|
|
1323
|
+
onClick: () => openTransientTab(runnerLogTab(run)),
|
|
1324
|
+
className: "px-2 ",
|
|
1325
|
+
children: "log"
|
|
1326
|
+
}),
|
|
1327
|
+
run.interactive && sessionId !== null ? /* @__PURE__ */ jsx(Button, {
|
|
1328
|
+
variant: "outline",
|
|
1329
|
+
size: "xs",
|
|
1330
|
+
"data-testid": `runners-card-shell-${run.id}`,
|
|
1331
|
+
title: "attach to this runner's terminal",
|
|
1332
|
+
onClick: () => openTransientTab(runnerShellTab(run)),
|
|
1333
|
+
className: "px-2 ",
|
|
1334
|
+
children: "shell"
|
|
1335
|
+
}) : null,
|
|
1336
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
|
|
1337
|
+
sessionId !== null ? /* @__PURE__ */ jsx(Button, {
|
|
1338
|
+
variant: stopRequested ? "outline" : "danger-outline",
|
|
1339
|
+
size: "xs",
|
|
1340
|
+
"data-testid": `runners-card-stop-${run.id}`,
|
|
1341
|
+
disabled: stopRequested,
|
|
1342
|
+
title: stopRequested ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
|
|
1343
|
+
onClick: () => requestRunnerStop(sendSessionFrame, sessionId, run.id),
|
|
1344
|
+
className: "px-2 ",
|
|
1345
|
+
children: stopRequested ? "stopping…" : "stop"
|
|
1346
|
+
}) : null
|
|
1347
|
+
]
|
|
1348
|
+
})
|
|
1349
|
+
]
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
//#endregion
|
|
1353
|
+
//#region src/extensions/workspaces/sessions/(frontend)/activity-group/runners.web.ts
|
|
1354
|
+
var runners_web_default = defineActivityGroup({
|
|
1355
|
+
keys: "r l",
|
|
1356
|
+
icon: PlayCircleIcon,
|
|
1357
|
+
statusKey: "doom-runner-runners",
|
|
1358
|
+
activeSource: runnerActivitySource,
|
|
1359
|
+
transientTab: runnersTab,
|
|
1360
|
+
order: 20
|
|
1361
|
+
});
|
|
1362
|
+
//#endregion
|
|
1363
|
+
//#region src/extensions/workspaces/sessions/(frontend)/channel/runner-runs.web.ts
|
|
1364
|
+
var runner_runs_web_default = defineChannelFile(runnerRunsChannel);
|
|
1365
|
+
//#endregion
|
|
1366
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/_components/RunnersActivitySection.tsx
|
|
1367
|
+
const TICK_MS = 1e4;
|
|
1368
|
+
/**
|
|
1369
|
+
* The runners group's body in the activity dock: what this session has up
|
|
1370
|
+
* right now.
|
|
1371
|
+
*
|
|
1372
|
+
* Only running runners are listed. A finished one leaves the moment it exits,
|
|
1373
|
+
* because the dock answers "what is happening", and a rail that keeps ten
|
|
1374
|
+
* completed playwright calls around buries the one command still working. The
|
|
1375
|
+
* runtime keeps every record; `doom-runner list` and the run's own log tab are
|
|
1376
|
+
* where a finished run is read.
|
|
1377
|
+
*/
|
|
1378
|
+
function RunnersActivitySection({ sessionId, sendSessionFrame, openTransientTab }) {
|
|
1379
|
+
const session = useStore(runners.store, (state) => runners.select(state, sessionId));
|
|
1380
|
+
const running = session.runs.filter((run) => run.state === "running");
|
|
1381
|
+
const [now, setNow] = useState(() => Date.now());
|
|
1382
|
+
useEffect(() => {
|
|
1383
|
+
const timer = setInterval(() => setNow(Date.now()), TICK_MS);
|
|
1384
|
+
return () => clearInterval(timer);
|
|
1385
|
+
}, []);
|
|
1386
|
+
if (running.length === 0) return /* @__PURE__ */ jsxs("div", {
|
|
1387
|
+
className: "flex flex-wrap items-center gap-x-4 gap-y-1",
|
|
1388
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1389
|
+
"data-testid": "activity-summary-runners",
|
|
1390
|
+
className: "text-sm text-doom-dim",
|
|
1391
|
+
children: "Idle"
|
|
1392
|
+
}), sessionId === null ? null : /* @__PURE__ */ jsxs(Button, {
|
|
1393
|
+
variant: "link",
|
|
1394
|
+
size: "sm",
|
|
1395
|
+
"data-testid": "activity-runner-launch",
|
|
1396
|
+
className: "h-auto gap-1 px-0 text-doom-green",
|
|
1397
|
+
onClick: () => openTransientTab(runnersTab()),
|
|
1398
|
+
children: ["Launch a runner", /* @__PURE__ */ jsx(ArrowRightIcon, {
|
|
1399
|
+
"aria-hidden": true,
|
|
1400
|
+
className: "size-3.5"
|
|
1401
|
+
})]
|
|
1402
|
+
})]
|
|
1403
|
+
});
|
|
1404
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1405
|
+
"data-testid": "activity-runner-runs",
|
|
1406
|
+
className: "flex flex-col gap-0.5",
|
|
1407
|
+
children: running.map((run) => /* @__PURE__ */ jsx(RunnerRow, {
|
|
1408
|
+
run,
|
|
1409
|
+
now,
|
|
1410
|
+
stopRequested: session.stopRequested.includes(run.id),
|
|
1411
|
+
sessionId,
|
|
1412
|
+
sendSessionFrame,
|
|
1413
|
+
openTransientTab
|
|
1414
|
+
}, run.id))
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* One running runner: its name, how long it has been up, a stop control, and
|
|
1419
|
+
* one line of what it is doing. Its own component because the tail line is a
|
|
1420
|
+
* subscription per row, and a row that scrolls out of the dock closes it.
|
|
1421
|
+
*/
|
|
1422
|
+
function RunnerRow({ run, now, stopRequested, sessionId, sendSessionFrame, openTransientTab }) {
|
|
1423
|
+
const tail = useRunnerTail(sessionId, run.id, run.state === "running");
|
|
1424
|
+
return /* @__PURE__ */ jsxs(Button, {
|
|
1425
|
+
variant: "ghost",
|
|
1426
|
+
size: "card",
|
|
1427
|
+
"data-testid": `activity-runner-${run.id}`,
|
|
1428
|
+
"data-runner-tone": "running",
|
|
1429
|
+
title: "open this runner's log",
|
|
1430
|
+
onClick: () => {
|
|
1431
|
+
if (sessionId === null) return;
|
|
1432
|
+
openTransientTab(runnerLogTab(run));
|
|
1433
|
+
},
|
|
1434
|
+
className: "min-w-0 gap-0.5 rounded-md px-1 py-1 hover:bg-doom-panel",
|
|
1435
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
1436
|
+
className: "flex min-w-0 items-center gap-1.5",
|
|
1437
|
+
children: [
|
|
1438
|
+
/* @__PURE__ */ jsx(Dot, {
|
|
1439
|
+
tone: "yellow",
|
|
1440
|
+
pulse: true
|
|
1441
|
+
}),
|
|
1442
|
+
/* @__PURE__ */ jsx("span", {
|
|
1443
|
+
className: "min-w-0 flex-1 truncate text-left text-base font-semibold text-doom-hi",
|
|
1444
|
+
children: run.name
|
|
1445
|
+
}),
|
|
1446
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1447
|
+
className: "shrink-0 text-sm text-doom-dim",
|
|
1448
|
+
children: [formatRunnerUptime(run.startedAt, now), run.interactive ? " · tty" : ""]
|
|
1449
|
+
}),
|
|
1450
|
+
sessionId !== null ? /* @__PURE__ */ jsx(Button, {
|
|
1451
|
+
asChild: true,
|
|
1452
|
+
variant: stopRequested ? "outline" : "danger-outline",
|
|
1453
|
+
size: "xs",
|
|
1454
|
+
"data-testid": `activity-runner-stop-${run.id}`,
|
|
1455
|
+
"data-stopping": stopRequested,
|
|
1456
|
+
title: stopRequested ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
|
|
1457
|
+
className: "px-1.5",
|
|
1458
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1459
|
+
role: "button",
|
|
1460
|
+
tabIndex: stopRequested ? -1 : 0,
|
|
1461
|
+
"aria-disabled": stopRequested,
|
|
1462
|
+
onClick: (event) => {
|
|
1463
|
+
event.stopPropagation();
|
|
1464
|
+
if (!stopRequested) requestRunnerStop(sendSessionFrame, sessionId, run.id);
|
|
1465
|
+
},
|
|
1466
|
+
onKeyDown: (event) => {
|
|
1467
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1468
|
+
event.preventDefault();
|
|
1469
|
+
event.stopPropagation();
|
|
1470
|
+
if (!stopRequested) requestRunnerStop(sendSessionFrame, sessionId, run.id);
|
|
1471
|
+
},
|
|
1472
|
+
children: stopRequested ? "stopping…" : "stop"
|
|
1473
|
+
})
|
|
1474
|
+
}) : null
|
|
1475
|
+
]
|
|
1476
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1477
|
+
"data-testid": `activity-runner-detail-${run.id}`,
|
|
1478
|
+
"data-detail": tail === void 0 ? "command" : "tail",
|
|
1479
|
+
className: "truncate pl-3 text-left text-sm text-doom-dim",
|
|
1480
|
+
children: tail ?? run.command
|
|
1481
|
+
})]
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
//#endregion
|
|
1485
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/Runners.activity.runners.web.tsx
|
|
1486
|
+
var Runners_activity_runners_web_default = defineFill({ component: RunnersActivitySection });
|
|
1487
|
+
//#endregion
|
|
1488
|
+
//#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/bashToolFormat.ts
|
|
1489
|
+
/**
|
|
1490
|
+
* Pure view logic for the browser bash tool card.
|
|
1491
|
+
* Every input is the wire JSON of a tool_execution frame.
|
|
1492
|
+
*/
|
|
1493
|
+
const ELLIPSIS = "…";
|
|
1494
|
+
/** Spaced so the join never glues two words into one unreadable token. */
|
|
1495
|
+
const GAP = " … ";
|
|
1496
|
+
/** Wide enough to keep a realistic command intact, short enough to stay on a few rows. */
|
|
1497
|
+
const MAX_COMMAND_LENGTH = 160;
|
|
1498
|
+
/** Live output is unbounded while a command runs, so the running state keeps only a window. */
|
|
1499
|
+
const STREAM_TAIL_LINES = 12;
|
|
1500
|
+
const BYTES_PER_KB = 1024;
|
|
1501
|
+
const BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB;
|
|
1502
|
+
/** The user's home on the two platforms the runner ships for; the browser cannot ask the OS. */
|
|
1503
|
+
const HOME_PATH = /(?<![\w.-])(?:\/Users|\/home)\/[^/\s]+(?=\/|\s|$)/g;
|
|
1504
|
+
function isRecord(value) {
|
|
1505
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1506
|
+
}
|
|
1507
|
+
/** Narrows the result details to the fields the card reads; anything else is ignored. */
|
|
1508
|
+
function bashResultDetails(details) {
|
|
1509
|
+
if (!isRecord(details)) return {};
|
|
1510
|
+
const out = {};
|
|
1511
|
+
if (typeof details.id === "string") out.id = details.id;
|
|
1512
|
+
if (typeof details.runner === "string") out.runner = details.runner;
|
|
1513
|
+
if (typeof details.exitCode === "number" || details.exitCode === null) out.exitCode = details.exitCode;
|
|
1514
|
+
if (typeof details.logPath === "string") out.logPath = details.logPath;
|
|
1515
|
+
if (typeof details.fileSize === "number") out.fileSize = details.fileSize;
|
|
1516
|
+
if (typeof details.lines === "number") out.lines = details.lines;
|
|
1517
|
+
if (details.timedOut === true) out.timedOut = true;
|
|
1518
|
+
if (details.promoted === true) out.promoted = true;
|
|
1519
|
+
if (typeof details.reason === "string") out.reason = details.reason;
|
|
1520
|
+
if (typeof details.tail === "string") out.tail = details.tail;
|
|
1521
|
+
if (typeof details.tailLines === "number") out.tailLines = details.tailLines;
|
|
1522
|
+
return out;
|
|
1523
|
+
}
|
|
1524
|
+
/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */
|
|
1525
|
+
function abbreviateHome(text) {
|
|
1526
|
+
return text.replace(HOME_PATH, "~");
|
|
1527
|
+
}
|
|
1528
|
+
/** Collapse a heredoc or multi-line script to one scannable line plus a count. */
|
|
1529
|
+
function collapseCommand(command) {
|
|
1530
|
+
const lines = command.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1531
|
+
const first = lines[0] ?? "";
|
|
1532
|
+
return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;
|
|
1533
|
+
}
|
|
1534
|
+
/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */
|
|
1535
|
+
function truncateMiddle(text, max = MAX_COMMAND_LENGTH) {
|
|
1536
|
+
if (text.length <= max) return text;
|
|
1537
|
+
const keep = max - 3;
|
|
1538
|
+
const head = Math.ceil(keep / 2);
|
|
1539
|
+
let start = text.slice(0, head);
|
|
1540
|
+
let end = text.slice(text.length - (keep - head));
|
|
1541
|
+
const lastSpace = start.lastIndexOf(" ");
|
|
1542
|
+
if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);
|
|
1543
|
+
const firstSpace = end.indexOf(" ");
|
|
1544
|
+
if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);
|
|
1545
|
+
return `${start}${GAP}${end}`;
|
|
1546
|
+
}
|
|
1547
|
+
function formatBashCommand(command) {
|
|
1548
|
+
return truncateMiddle(abbreviateHome(collapseCommand(command)));
|
|
1549
|
+
}
|
|
1550
|
+
/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */
|
|
1551
|
+
function formatBashFlags(args) {
|
|
1552
|
+
const flags = [];
|
|
1553
|
+
if (args.background === true) flags.push("bg");
|
|
1554
|
+
if (args.interactive === true) flags.push("tty");
|
|
1555
|
+
if (typeof args.timeout === "number") flags.push(`${args.timeout}s`);
|
|
1556
|
+
if (typeof args.name === "string" && args.name.length > 0) flags.push(args.name);
|
|
1557
|
+
return flags;
|
|
1558
|
+
}
|
|
1559
|
+
/** Human-readable byte count, matching how pi reports output sizes. */
|
|
1560
|
+
function formatSize(bytes) {
|
|
1561
|
+
if (bytes < BYTES_PER_KB) return `${bytes} B`;
|
|
1562
|
+
if (bytes < BYTES_PER_MB) return `${(bytes / BYTES_PER_KB).toFixed(1)} KB`;
|
|
1563
|
+
return `${(bytes / BYTES_PER_MB).toFixed(1)} MB`;
|
|
1564
|
+
}
|
|
1565
|
+
/** The metadata footer, collapsed from five lines to one. */
|
|
1566
|
+
function formatResultSummary(details) {
|
|
1567
|
+
const parts = [];
|
|
1568
|
+
if (details.lines !== void 0) parts.push(`${details.lines.toLocaleString("en-US")} lines`);
|
|
1569
|
+
if (details.fileSize !== void 0) parts.push(formatSize(details.fileSize));
|
|
1570
|
+
if (details.runner !== void 0) parts.push(details.runner);
|
|
1571
|
+
return parts.join(" · ");
|
|
1572
|
+
}
|
|
1573
|
+
/** Last `max` lines, so a chatty command cannot push the transcript off screen. */
|
|
1574
|
+
function lastLines(text, max) {
|
|
1575
|
+
const all = text.split("\n");
|
|
1576
|
+
while (all.length > 0 && all[all.length - 1]?.trim() === "") all.pop();
|
|
1577
|
+
return all.length <= max ? all : all.slice(-max);
|
|
1578
|
+
}
|
|
1579
|
+
function bashResultView(input) {
|
|
1580
|
+
const details = bashResultDetails(input.details);
|
|
1581
|
+
if (input.isPartial) return {
|
|
1582
|
+
lines: lastLines(input.output, STREAM_TAIL_LINES),
|
|
1583
|
+
status: {
|
|
1584
|
+
glyph: "◐",
|
|
1585
|
+
tone: "running",
|
|
1586
|
+
text: "running"
|
|
1587
|
+
},
|
|
1588
|
+
hidden: 0
|
|
1589
|
+
};
|
|
1590
|
+
if (input.isError) {
|
|
1591
|
+
const all = lastLines(input.output, Number.MAX_SAFE_INTEGER);
|
|
1592
|
+
const lines = input.expanded ? all : all.slice(-12);
|
|
1593
|
+
const summary = formatResultSummary(details);
|
|
1594
|
+
return {
|
|
1595
|
+
lines,
|
|
1596
|
+
status: {
|
|
1597
|
+
glyph: "✗",
|
|
1598
|
+
tone: "error",
|
|
1599
|
+
text: summary.length > 0 ? summary : "failed"
|
|
1600
|
+
},
|
|
1601
|
+
hidden: all.length - lines.length
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
if (details.promoted === true) return {
|
|
1605
|
+
lines: [],
|
|
1606
|
+
status: {
|
|
1607
|
+
glyph: "●",
|
|
1608
|
+
tone: "background",
|
|
1609
|
+
text: `${details.runner ?? details.id ?? "runner"}${details.id === void 0 ? "" : ` · ${details.id}`} · background`
|
|
1610
|
+
},
|
|
1611
|
+
hidden: 0
|
|
1612
|
+
};
|
|
1613
|
+
const all = lastLines(details.tail ?? "", Number.MAX_SAFE_INTEGER);
|
|
1614
|
+
const lines = input.expanded ? all : all.slice(-12);
|
|
1615
|
+
const exited = details.exitCode !== void 0 && details.exitCode !== null;
|
|
1616
|
+
const failed = details.timedOut === true || exited && details.exitCode !== 0;
|
|
1617
|
+
const meta = [];
|
|
1618
|
+
if (details.timedOut === true) meta.push("timed out");
|
|
1619
|
+
else if (failed) meta.push(`exit ${details.exitCode}`);
|
|
1620
|
+
const summary = formatResultSummary(details);
|
|
1621
|
+
if (summary.length > 0) meta.push(summary);
|
|
1622
|
+
const text = meta.length > 0 ? meta.join(" · ") : failed ? "failed" : lines.length === 0 ? "done" : "";
|
|
1623
|
+
return {
|
|
1624
|
+
lines,
|
|
1625
|
+
status: text.length > 0 ? {
|
|
1626
|
+
glyph: failed ? "✗" : "✓",
|
|
1627
|
+
tone: failed ? "error" : "ok",
|
|
1628
|
+
text
|
|
1629
|
+
} : null,
|
|
1630
|
+
hidden: all.length - lines.length
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
//#endregion
|
|
1634
|
+
//#region src/extensions/workspaces/sessions/(frontend)/tool/_components/BashToolMessage.tsx
|
|
1635
|
+
const STATUS_TONE = {
|
|
1636
|
+
running: "running",
|
|
1637
|
+
ok: "ok",
|
|
1638
|
+
error: "error",
|
|
1639
|
+
background: "info"
|
|
1640
|
+
};
|
|
1641
|
+
/**
|
|
1642
|
+
* The bash tool's timeline item, the web half of renderBashCall and
|
|
1643
|
+
* renderBashResult. Collapsed it is a single header row: the tool name, the
|
|
1644
|
+
* command shortened to one line, its modifiers, and the outcome badge, so a
|
|
1645
|
+
* transcript of many calls stays scannable. Expanding it opens the body: the
|
|
1646
|
+
* command exactly as it was run, then the whole tail the frame carries, each
|
|
1647
|
+
* in its own scroll box so one chatty command cannot own the viewport. A
|
|
1648
|
+
* command promoted to a background runner offers the same stop the activity
|
|
1649
|
+
* dock does, for as long as the runners channel reports it running.
|
|
1650
|
+
*/
|
|
1651
|
+
function BashToolMessage({ sessionId, args, result, output, running, isError, sendSessionFrame, openTransientTab }) {
|
|
1652
|
+
const details = bashResultDetails(result?.details);
|
|
1653
|
+
const runnerId = details.promoted === true ? details.id : void 0;
|
|
1654
|
+
const run = useStore(runners.store, (state) => details.id === void 0 ? void 0 : runners.select(state, sessionId).runs.find((entry) => entry.id === details.id));
|
|
1655
|
+
const stopping = useStore(runners.store, (state) => runnerId !== void 0 && runners.select(state, sessionId).stopRequested.includes(runnerId));
|
|
1656
|
+
const rawCommand = typeof args.command === "string" ? args.command : "";
|
|
1657
|
+
const command = formatBashCommand(rawCommand);
|
|
1658
|
+
const flags = formatBashFlags(args);
|
|
1659
|
+
const stoppable = sessionId !== null && runnerId !== void 0 && run !== void 0 && run.exit === void 0;
|
|
1660
|
+
return /* @__PURE__ */ jsx(MessageItem, {
|
|
1661
|
+
tone: toolTone({
|
|
1662
|
+
running,
|
|
1663
|
+
isError
|
|
1664
|
+
}),
|
|
1665
|
+
expandable: true,
|
|
1666
|
+
children: ({ expanded }) => {
|
|
1667
|
+
const view = bashResultView({
|
|
1668
|
+
details: result?.details,
|
|
1669
|
+
output,
|
|
1670
|
+
expanded: true,
|
|
1671
|
+
isPartial: running,
|
|
1672
|
+
isError
|
|
1673
|
+
});
|
|
1674
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(MessageItemHeader, {
|
|
1675
|
+
title: "bash",
|
|
1676
|
+
children: [
|
|
1677
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1678
|
+
"data-testid": "tool-call-bash",
|
|
1679
|
+
className: "flex min-w-0 flex-1 items-center gap-2",
|
|
1680
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1681
|
+
className: "min-w-0 flex-1 truncate font-mono text-doom-text",
|
|
1682
|
+
children: command
|
|
1683
|
+
}), flags.length > 0 ? /* @__PURE__ */ jsxs("span", {
|
|
1684
|
+
className: "hidden shrink-0 text-doom-dim sm:inline",
|
|
1685
|
+
children: ["· ", flags.join(" · ")]
|
|
1686
|
+
}) : null]
|
|
1687
|
+
}),
|
|
1688
|
+
run !== void 0 && sessionId !== null ? /* @__PURE__ */ jsxs(Button, {
|
|
1689
|
+
variant: "outline",
|
|
1690
|
+
size: "xs",
|
|
1691
|
+
"data-testid": "tool-result-bash-open-log",
|
|
1692
|
+
title: "open this runner's full log",
|
|
1693
|
+
onClick: (event) => {
|
|
1694
|
+
event.stopPropagation();
|
|
1695
|
+
openTransientTab(runnerLogTab(run));
|
|
1696
|
+
},
|
|
1697
|
+
className: "shrink-0 gap-1 px-1.5 ",
|
|
1698
|
+
children: [/* @__PURE__ */ jsx(FileIcon, { className: "h-2.5 w-2.5" }), "log"]
|
|
1699
|
+
}) : null,
|
|
1700
|
+
stoppable ? /* @__PURE__ */ jsx(Button, {
|
|
1701
|
+
size: "xs",
|
|
1702
|
+
variant: stopping ? "outline" : "danger-outline",
|
|
1703
|
+
disabled: stopping,
|
|
1704
|
+
"data-testid": "tool-result-bash-stop",
|
|
1705
|
+
"data-stopping": stopping,
|
|
1706
|
+
title: stopping ? "stop requested; the runner reports its own exit" : "ask the runtime to stop this runner",
|
|
1707
|
+
onClick: (event) => {
|
|
1708
|
+
event.stopPropagation();
|
|
1709
|
+
if (sessionId !== null && runnerId !== void 0) requestRunnerStop(sendSessionFrame, sessionId, runnerId);
|
|
1710
|
+
},
|
|
1711
|
+
className: "shrink-0 px-1.5 ",
|
|
1712
|
+
children: stopping ? "stopping…" : "stop"
|
|
1713
|
+
}) : null
|
|
1714
|
+
]
|
|
1715
|
+
}), expanded ? /* @__PURE__ */ jsxs(MessageItemBody, {
|
|
1716
|
+
"data-testid": "tool-result-bash",
|
|
1717
|
+
className: "flex flex-col gap-2",
|
|
1718
|
+
children: [
|
|
1719
|
+
rawCommand.length > 0 ? /* @__PURE__ */ jsx(SyntaxText, {
|
|
1720
|
+
"data-testid": "tool-result-bash-command",
|
|
1721
|
+
grammar: "shell",
|
|
1722
|
+
text: rawCommand,
|
|
1723
|
+
className: "max-h-40 overflow-auto text-doom-text"
|
|
1724
|
+
}) : null,
|
|
1725
|
+
view.lines.length > 0 ? /* @__PURE__ */ jsx(AnsiText, {
|
|
1726
|
+
"data-testid": "tool-result-bash-output",
|
|
1727
|
+
text: view.lines.join("\n"),
|
|
1728
|
+
className: "max-h-96 overflow-auto whitespace-pre-wrap break-words [overflow-wrap:anywhere] border-doom-border-soft border-t pt-2 font-mono text-doom-dim"
|
|
1729
|
+
}) : null,
|
|
1730
|
+
view.status ? /* @__PURE__ */ jsx(MessageItemStatus, {
|
|
1731
|
+
"data-testid": "tool-result-bash-status",
|
|
1732
|
+
tone: STATUS_TONE[view.status.tone],
|
|
1733
|
+
glyph: view.status.glyph,
|
|
1734
|
+
className: "min-w-0",
|
|
1735
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1736
|
+
className: "break-words",
|
|
1737
|
+
children: view.status.text
|
|
1738
|
+
})
|
|
1739
|
+
}) : null,
|
|
1740
|
+
details.logPath !== void 0 ? /* @__PURE__ */ jsxs("span", {
|
|
1741
|
+
"data-testid": "tool-result-bash-log",
|
|
1742
|
+
className: "min-w-0 break-all text-doom-dim",
|
|
1743
|
+
children: ["full log · ", /* @__PURE__ */ jsx("span", {
|
|
1744
|
+
className: "text-doom-dim",
|
|
1745
|
+
children: details.logPath
|
|
1746
|
+
})]
|
|
1747
|
+
}) : null
|
|
1748
|
+
]
|
|
1749
|
+
}) : null] });
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
//#endregion
|
|
1754
|
+
//#region src/extensions/workspaces/sessions/(frontend)/tool/bash.web.tsx
|
|
1755
|
+
var bash_web_default = defineToolRenderer({ message: BashToolMessage });
|
|
1756
|
+
//#endregion
|
|
1757
|
+
//#region generated/web.ts
|
|
1758
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
1759
|
+
...identity,
|
|
1760
|
+
...value
|
|
1761
|
+
};
|
|
1762
|
+
const webPlugin = defineWebPlugin({
|
|
1763
|
+
id: "runner",
|
|
1764
|
+
session: {
|
|
1765
|
+
get activityGroups() {
|
|
1766
|
+
return [via({ name: "runners" }, runners_web_default)];
|
|
1767
|
+
},
|
|
1768
|
+
get channels() {
|
|
1769
|
+
return [via({ channel: "runner_runs" }, runner_runs_web_default)];
|
|
1770
|
+
},
|
|
1771
|
+
get fills() {
|
|
1772
|
+
return [via({
|
|
1773
|
+
slot: "activity.runners",
|
|
1774
|
+
id: "runners"
|
|
1775
|
+
}, Runners_activity_runners_web_default)];
|
|
1776
|
+
},
|
|
1777
|
+
get toolRenderers() {
|
|
1778
|
+
return [via({ tools: ["bash"] }, bash_web_default)];
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
//#endregion
|
|
1783
|
+
export { webPlugin };
|
|
1784
|
+
|
|
1785
|
+
//# sourceMappingURL=web.mjs.map
|