@agimon-ai/po-workflow 0.0.0-alpha.0 → 0.0.0-alpha.2
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/dist/extensions/native.mjs +1620 -0
- package/dist/extensions/native.mjs.map +1 -0
- package/dist/extensions/server.cjs +107 -27
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.mjs +107 -27
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/extensions/web.mjs +18 -13
- package/dist/extensions/web.mjs.map +1 -1
- package/package.json +29 -11
- package/src/types/webWorkflowTerminal.ts +2 -0
|
@@ -0,0 +1,1620 @@
|
|
|
1
|
+
import { createApiClient, defineActivityGroup, defineChannelFile, defineFill, defineNativePlugin, defineSessionStore, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
|
|
2
|
+
import { Badge, Button, CodeBlock, Dot, Input, ListRow, Markdown, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, Textarea, WorkflowIcon } from "@agimon-ai/po-native-components";
|
|
3
|
+
import { createElement, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { View } from "react-native-unistyles/components/native/View";
|
|
5
|
+
import { Text } from "react-native-unistyles/components/native/Text";
|
|
6
|
+
import { ScrollView } from "react-native-unistyles/components/native/ScrollView";
|
|
7
|
+
import { StyleSheet } from "react-native-unistyles";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/types/webWorkflows.ts
|
|
10
|
+
const WORKFLOW_RUNS_TYPE = "workflow_runs";
|
|
11
|
+
const WORKFLOW_CATALOG_TYPE = "workflow_catalog";
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/runDuration.ts
|
|
14
|
+
/**
|
|
15
|
+
* Local copy of the host's formatRunDuration (src/web/lib/sessionSummary.ts):
|
|
16
|
+
* this plugin keeps zero host-lib imports so its later move into the owning
|
|
17
|
+
* package is a file relocation.
|
|
18
|
+
*/
|
|
19
|
+
const MINUTE_MS = 6e4;
|
|
20
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
21
|
+
function formatRunDuration(elapsedMs) {
|
|
22
|
+
if (elapsedMs < MINUTE_MS) return "<1m";
|
|
23
|
+
if (elapsedMs < HOUR_MS) return `${Math.floor(elapsedMs / MINUTE_MS)}m`;
|
|
24
|
+
const hours = Math.floor(elapsedMs / HOUR_MS);
|
|
25
|
+
const minutes = Math.floor(elapsedMs % HOUR_MS / MINUTE_MS);
|
|
26
|
+
return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/workflowActivity.ts
|
|
30
|
+
const PAUSED_STATES = /* @__PURE__ */ new Set(["paused", "pause_requested"]);
|
|
31
|
+
function workflowRunIdentity(run) {
|
|
32
|
+
return `${run.workspace}/${run.runKey}`;
|
|
33
|
+
}
|
|
34
|
+
function toneOf(run) {
|
|
35
|
+
if (run.stage === "running") return run.executionState !== void 0 && PAUSED_STATES.has(run.executionState) ? "paused" : "running";
|
|
36
|
+
if (run.stage === "error") return "failed";
|
|
37
|
+
if (run.outcome !== void 0 && run.outcome !== "success") return run.outcome === "failed" ? "failed" : "skipped";
|
|
38
|
+
return "done";
|
|
39
|
+
}
|
|
40
|
+
function detailOf(run, tone) {
|
|
41
|
+
if (tone === "paused") return "paused; resume it from the owning session";
|
|
42
|
+
if (tone === "failed") return run.errorMessage ?? (run.failedJob === void 0 ? "failed" : `job '${run.failedJob}' failed`);
|
|
43
|
+
if (tone === "running") {
|
|
44
|
+
if (run.position === void 0) return "starting";
|
|
45
|
+
return run.position.step === void 0 ? run.position.job : `${run.position.job} · ${run.position.step}`;
|
|
46
|
+
}
|
|
47
|
+
return run.outcome ?? "done";
|
|
48
|
+
}
|
|
49
|
+
function elapsedOf(run, now) {
|
|
50
|
+
const start = Date.parse(run.startedAt);
|
|
51
|
+
if (!Number.isFinite(start)) return "";
|
|
52
|
+
const end = run.finishedAt === void 0 ? now : Date.parse(run.finishedAt);
|
|
53
|
+
return formatRunDuration(Math.max(0, (Number.isFinite(end) ? end : now) - start));
|
|
54
|
+
}
|
|
55
|
+
/** The hub's presented order is kept: it already puts live runs first. */
|
|
56
|
+
function workflowActivityRows(runs, now, sessionId = null) {
|
|
57
|
+
return runs.map((run) => {
|
|
58
|
+
const tone = toneOf(run);
|
|
59
|
+
return {
|
|
60
|
+
identity: workflowRunIdentity(run),
|
|
61
|
+
runKey: run.runKey,
|
|
62
|
+
name: run.displayName,
|
|
63
|
+
tone,
|
|
64
|
+
elapsed: elapsedOf(run, now),
|
|
65
|
+
detail: detailOf(run, tone),
|
|
66
|
+
...run.ownerSessionId === void 0 || run.ownerSessionId === sessionId ? {} : { ownerSessionId: run.ownerSessionId }
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const GROUP_OF = {
|
|
71
|
+
running: "running",
|
|
72
|
+
paused: "running",
|
|
73
|
+
failed: "failed",
|
|
74
|
+
done: "successful",
|
|
75
|
+
skipped: "successful"
|
|
76
|
+
};
|
|
77
|
+
const GROUP_ORDER = [
|
|
78
|
+
"running",
|
|
79
|
+
"failed",
|
|
80
|
+
"successful"
|
|
81
|
+
];
|
|
82
|
+
/** Rows split by outcome, in the order the dock shows them; empty groups are dropped. */
|
|
83
|
+
function workflowActivityGroups(rows) {
|
|
84
|
+
return GROUP_ORDER.map((name) => ({
|
|
85
|
+
name,
|
|
86
|
+
rows: rows.filter((row) => GROUP_OF[row.tone] === name),
|
|
87
|
+
openByDefault: name !== "successful"
|
|
88
|
+
})).filter((group) => group.rows.length > 0);
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/workflowsStore.native.ts
|
|
92
|
+
const workflows = defineSessionStore({
|
|
93
|
+
runs: [],
|
|
94
|
+
focusedRun: void 0,
|
|
95
|
+
pendingLaunch: void 0
|
|
96
|
+
});
|
|
97
|
+
function isRecord$2(value) {
|
|
98
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
99
|
+
}
|
|
100
|
+
/** Points the workflows tab at one run; the activity dock uses this before opening the tab. */
|
|
101
|
+
function focusRun(sessionId, identity) {
|
|
102
|
+
workflows.update(sessionId, (current) => ({
|
|
103
|
+
...current,
|
|
104
|
+
focusedRun: identity
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
/** Removes a deleted run immediately while the registry channel catches up. */
|
|
108
|
+
function removeRun(sessionId, identity) {
|
|
109
|
+
workflows.update(sessionId, (current) => {
|
|
110
|
+
const runs = current.runs.filter((run) => workflowRunIdentity(run) !== identity);
|
|
111
|
+
return {
|
|
112
|
+
...current,
|
|
113
|
+
runs,
|
|
114
|
+
focusedRun: current.focusedRun === identity ? runs[0] === void 0 ? void 0 : workflowRunIdentity(runs[0]) : current.focusedRun
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Sends a launch line to the session and remembers what was already running.
|
|
120
|
+
*
|
|
121
|
+
* The cockpit's only channel to a session is a prompt frame, so a launch is
|
|
122
|
+
* the verb the extension registers for exactly this; see web/launchLine.ts.
|
|
123
|
+
*/
|
|
124
|
+
function requestLaunch(send, sessionId, line, workflowPath) {
|
|
125
|
+
send(sessionId, {
|
|
126
|
+
type: "prompt",
|
|
127
|
+
message: line
|
|
128
|
+
});
|
|
129
|
+
workflows.update(sessionId, (current) => ({
|
|
130
|
+
...current,
|
|
131
|
+
pendingLaunch: {
|
|
132
|
+
workflowPath,
|
|
133
|
+
knownRuns: current.runs.map(workflowRunIdentity)
|
|
134
|
+
}
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
/** The plugin's session data channel: 'workflow_runs' payloads into the store. */
|
|
138
|
+
const workflowRunsChannel = workflows.channel({
|
|
139
|
+
channel: WORKFLOW_RUNS_TYPE,
|
|
140
|
+
parse(input) {
|
|
141
|
+
if (!isRecord$2(input) || !Array.isArray(input.runs)) return null;
|
|
142
|
+
return { runs: input.runs.filter(isRecord$2) };
|
|
143
|
+
},
|
|
144
|
+
reduce: (current, { runs }) => {
|
|
145
|
+
const pending = current.pendingLaunch;
|
|
146
|
+
if (pending === void 0) return {
|
|
147
|
+
...current,
|
|
148
|
+
runs
|
|
149
|
+
};
|
|
150
|
+
const known = new Set(pending.knownRuns);
|
|
151
|
+
const arrived = runs.find((run) => !known.has(workflowRunIdentity(run)));
|
|
152
|
+
if (arrived === void 0) return {
|
|
153
|
+
...current,
|
|
154
|
+
runs
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
...current,
|
|
158
|
+
runs,
|
|
159
|
+
focusedRun: workflowRunIdentity(arrived),
|
|
160
|
+
pendingLaunch: void 0
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/extensions/workspaces/sessions/(frontend)/activity-group/workflows.native.ts
|
|
166
|
+
var workflows_native_default = defineActivityGroup({
|
|
167
|
+
keys: "w r",
|
|
168
|
+
icon: () => createElement(WorkflowIcon),
|
|
169
|
+
widgetKeys: ["workflow-mcp-progress", "workflow-mcp-follow"],
|
|
170
|
+
order: 30,
|
|
171
|
+
activeSource: {
|
|
172
|
+
subscribe(listener) {
|
|
173
|
+
const subscription = workflows.store.subscribe(listener);
|
|
174
|
+
return () => subscription.unsubscribe();
|
|
175
|
+
},
|
|
176
|
+
isActive(sessionId) {
|
|
177
|
+
return workflows.select(workflows.store.state, sessionId).runs.some((run) => run.stage === "running");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/catalogStore.native.ts
|
|
183
|
+
const catalog = defineSessionStore({
|
|
184
|
+
cwd: "",
|
|
185
|
+
workflows: [],
|
|
186
|
+
warning: void 0,
|
|
187
|
+
open: false,
|
|
188
|
+
filter: "",
|
|
189
|
+
prompt: "",
|
|
190
|
+
selected: void 0,
|
|
191
|
+
inspected: void 0,
|
|
192
|
+
launch: void 0
|
|
193
|
+
});
|
|
194
|
+
function isRecord$1(value) {
|
|
195
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
196
|
+
}
|
|
197
|
+
function openCatalog(sessionId, prompt = "") {
|
|
198
|
+
catalog.update(sessionId, (current) => ({
|
|
199
|
+
...current,
|
|
200
|
+
open: true,
|
|
201
|
+
prompt
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
function closeCatalog(sessionId) {
|
|
205
|
+
catalog.update(sessionId, (current) => ({
|
|
206
|
+
...current,
|
|
207
|
+
open: false,
|
|
208
|
+
prompt: "",
|
|
209
|
+
launch: void 0
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
/** Rows whose name, path, tag or job matches the filter, case-insensitively. */
|
|
213
|
+
function filterCatalog(workflows, filter) {
|
|
214
|
+
const needle = filter.trim().toLowerCase();
|
|
215
|
+
if (needle === "") return [...workflows];
|
|
216
|
+
return workflows.filter((workflow) => [
|
|
217
|
+
workflow.name,
|
|
218
|
+
workflow.relativePath,
|
|
219
|
+
workflow.description,
|
|
220
|
+
...workflow.tags,
|
|
221
|
+
...workflow.jobs.map((job) => job.name)
|
|
222
|
+
].join("\n").toLowerCase().includes(needle));
|
|
223
|
+
}
|
|
224
|
+
/** The plugin's catalog channel: 'workflow_catalog' payloads into the store. */
|
|
225
|
+
const workflowCatalogChannel = catalog.channel({
|
|
226
|
+
channel: WORKFLOW_CATALOG_TYPE,
|
|
227
|
+
parse(input) {
|
|
228
|
+
if (!isRecord$1(input) || typeof input.cwd !== "string" || !Array.isArray(input.workflows)) return null;
|
|
229
|
+
return {
|
|
230
|
+
cwd: input.cwd,
|
|
231
|
+
workflows: input.workflows.filter(isRecord$1),
|
|
232
|
+
...typeof input.warning === "string" ? { warning: input.warning } : {}
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
reduce(current, payload) {
|
|
236
|
+
const paths = new Set(payload.workflows.map((workflow) => workflow.path));
|
|
237
|
+
return {
|
|
238
|
+
...current,
|
|
239
|
+
cwd: payload.cwd,
|
|
240
|
+
workflows: payload.workflows,
|
|
241
|
+
warning: payload.warning,
|
|
242
|
+
selected: current.selected !== void 0 && paths.has(current.selected) ? current.selected : void 0,
|
|
243
|
+
inspected: current.inspected !== void 0 && paths.has(current.inspected) ? current.inspected : void 0,
|
|
244
|
+
launch: current.launch !== void 0 && paths.has(current.launch) ? current.launch : void 0
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/extensions/workspaces/sessions/(frontend)/channel/workflow-catalog.native.ts
|
|
250
|
+
var workflow_catalog_native_default = defineChannelFile(workflowCatalogChannel);
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/extensions/workspaces/sessions/(frontend)/channel/workflow-runs.native.ts
|
|
253
|
+
var workflow_runs_native_default = defineChannelFile(workflowRunsChannel);
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/launchLine.ts
|
|
256
|
+
/**
|
|
257
|
+
* The `/workflow-launch` line the dialog sends.
|
|
258
|
+
*
|
|
259
|
+
* WHY THE BUILDER LIVES HERE AND THE PARSER DOES NOT:
|
|
260
|
+
* Cockpit code is compiled into the host bundle and may import only this
|
|
261
|
+
* package's own web and type modules, so it cannot share the session's parser.
|
|
262
|
+
* The two are pinned together by a test that parses what this writes, which is
|
|
263
|
+
* the only guarantee that matters: the line has to survive the trip.
|
|
264
|
+
*/
|
|
265
|
+
const WORKFLOW_LAUNCH_VERB = "/workflow-launch";
|
|
266
|
+
/** Reserved keys; every other pair is one of the workflow's own inputs. */
|
|
267
|
+
const RUNNER_KEY = "runner";
|
|
268
|
+
const COMMAND_KEY = "command";
|
|
269
|
+
const CHOICE_KEY = "choice";
|
|
270
|
+
/** Quotes a value that carries whitespace, which is how a value survives tokenizing. */
|
|
271
|
+
function quote(value) {
|
|
272
|
+
return /\s/.test(value) ? `"${value}"` : value;
|
|
273
|
+
}
|
|
274
|
+
function reservedPair(key, value) {
|
|
275
|
+
return value === void 0 || value === "" ? [] : [`${key}=${quote(value)}`];
|
|
276
|
+
}
|
|
277
|
+
function workflowLaunchLine(request) {
|
|
278
|
+
const pairs = [
|
|
279
|
+
...reservedPair(RUNNER_KEY, request.runner),
|
|
280
|
+
...reservedPair(COMMAND_KEY, request.command),
|
|
281
|
+
...reservedPair(CHOICE_KEY, request.choice),
|
|
282
|
+
...Object.entries(request.inputs).filter(([, value]) => value !== "").map(([key, value]) => `${key}=${quote(value)}`)
|
|
283
|
+
];
|
|
284
|
+
const prompt = request.prompt?.trim();
|
|
285
|
+
return [
|
|
286
|
+
WORKFLOW_LAUNCH_VERB,
|
|
287
|
+
quote(request.workflow),
|
|
288
|
+
...pairs,
|
|
289
|
+
...prompt ? [prompt] : []
|
|
290
|
+
].join(" ");
|
|
291
|
+
}
|
|
292
|
+
/** The trigger that makes a prompt mandatory. */
|
|
293
|
+
const USER_PROMPT_TRIGGER = "user_prompt";
|
|
294
|
+
/** What the dialog will not let a reader send, worded the way the session would answer. */
|
|
295
|
+
function launchProblems(workflow, request) {
|
|
296
|
+
const problems = [];
|
|
297
|
+
if (workflow.triggers.includes(USER_PROMPT_TRIGGER) && (request.prompt ?? "").trim() === "") problems.push("This workflow is triggered by a prompt, so it needs one.");
|
|
298
|
+
const missing = workflow.inputs.filter((input) => input.required === true && (request.inputs[input.name] ?? "") === "").map((input) => input.name);
|
|
299
|
+
if (missing.length > 0) problems.push(`Missing required input${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}.`);
|
|
300
|
+
return problems;
|
|
301
|
+
}
|
|
302
|
+
/** The default a field starts with: what the workflow declared, or empty. */
|
|
303
|
+
function initialInputs(workflow) {
|
|
304
|
+
return Object.fromEntries(workflow.inputs.map((input) => [input.name, input.default ?? ""]));
|
|
305
|
+
}
|
|
306
|
+
/** The runner a launch starts on: the workflow's first declared one, when it declares any. */
|
|
307
|
+
function initialRunner(workflow) {
|
|
308
|
+
return workflow.runners?.[0];
|
|
309
|
+
}
|
|
310
|
+
/** The command templated steps start on: the one their steps list first. */
|
|
311
|
+
function initialCommand(workflow) {
|
|
312
|
+
return workflow.commands?.[0];
|
|
313
|
+
}
|
|
314
|
+
/** The choice a launch starts on: `default` when declared, else the first. */
|
|
315
|
+
function initialChoice(workflow) {
|
|
316
|
+
const choices = workflow.choices ?? [];
|
|
317
|
+
return choices.includes("default") ? "default" : choices[0];
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/types/apiRoutes.ts
|
|
321
|
+
/**
|
|
322
|
+
* This package's routes, as data.
|
|
323
|
+
*
|
|
324
|
+
* No scope and no base path. The build reads both off
|
|
325
|
+
* `src/extensions/(backend)/api/workflow/`, so the mount is stated once, by the
|
|
326
|
+
* folder that creates it. The same API answers at all three scopes, because a
|
|
327
|
+
* global contribution cascades into workspace and session; the folder under
|
|
328
|
+
* `workspaces/sessions/` only re-mounts it for a session's own server.
|
|
329
|
+
*
|
|
330
|
+
* This is the one file the routes, the Hono app and the browser all read, so a
|
|
331
|
+
* path cannot move on one side alone. The page used to build the hub's own
|
|
332
|
+
* absolute URL and then rewrite its plugin prefix with String.replace to reach
|
|
333
|
+
* a session, which is exactly the kind of copy that can move by one character
|
|
334
|
+
* without anything noticing.
|
|
335
|
+
*
|
|
336
|
+
* `:workspace` and `:runKey` are Hono's own parameter syntax, because the app
|
|
337
|
+
* registers `path` verbatim.
|
|
338
|
+
*/
|
|
339
|
+
/** One run, which every route below addresses. */
|
|
340
|
+
const RUN = "/runs/:workspace/:runKey";
|
|
341
|
+
/**
|
|
342
|
+
* The artifact route's parameter, as Hono spells a multi-segment match.
|
|
343
|
+
*
|
|
344
|
+
* An artifact is named by its path inside the run directory, so `reports/a.md`
|
|
345
|
+
* is one parameter across two segments. The client's `params` substitution
|
|
346
|
+
* fills a single segment and percent-encodes the slash with it, so the page
|
|
347
|
+
* substitutes this one itself; see the page's `_lib/terminalApi.ts`.
|
|
348
|
+
*/
|
|
349
|
+
const ARTIFACT_NAME_PARAM = ":name{.+}";
|
|
350
|
+
/**
|
|
351
|
+
* The step whose pane a terminal route reads: a step ref's id. Absent, the
|
|
352
|
+
* route follows the run's current step, which is one of a parallel group's.
|
|
353
|
+
*/
|
|
354
|
+
const STEP_TARGET_PARAM = "step";
|
|
355
|
+
const ARTIFACT_DOWNLOAD_PARAM = "download";
|
|
356
|
+
const api = createApiClient({
|
|
357
|
+
/**
|
|
358
|
+
* The terminal, as server-sent events.
|
|
359
|
+
*
|
|
360
|
+
* `stream: true` gets a URL and no call method: the consumer is the browser's
|
|
361
|
+
* own `EventSource`, and a relayed call would buffer a stream that never ends.
|
|
362
|
+
*/
|
|
363
|
+
snapshot: {
|
|
364
|
+
method: "GET",
|
|
365
|
+
path: `${RUN}/screen`,
|
|
366
|
+
query: [STEP_TARGET_PARAM],
|
|
367
|
+
response: void 0
|
|
368
|
+
},
|
|
369
|
+
screen: {
|
|
370
|
+
method: "GET",
|
|
371
|
+
path: `${RUN}/screen/stream`,
|
|
372
|
+
query: [STEP_TARGET_PARAM],
|
|
373
|
+
stream: true
|
|
374
|
+
},
|
|
375
|
+
control: {
|
|
376
|
+
method: "POST",
|
|
377
|
+
path: `${RUN}/control`,
|
|
378
|
+
response: void 0
|
|
379
|
+
},
|
|
380
|
+
keys: {
|
|
381
|
+
method: "POST",
|
|
382
|
+
path: `${RUN}/keys`
|
|
383
|
+
},
|
|
384
|
+
resize: {
|
|
385
|
+
method: "POST",
|
|
386
|
+
path: `${RUN}/resize`,
|
|
387
|
+
response: void 0
|
|
388
|
+
},
|
|
389
|
+
remove: {
|
|
390
|
+
method: "DELETE",
|
|
391
|
+
path: RUN,
|
|
392
|
+
response: void 0
|
|
393
|
+
},
|
|
394
|
+
stop: {
|
|
395
|
+
method: "POST",
|
|
396
|
+
path: `${RUN}/stop`,
|
|
397
|
+
response: void 0
|
|
398
|
+
},
|
|
399
|
+
/** Guidance for the agent session the running step is in, like steering a Team sub-agent. */
|
|
400
|
+
steer: {
|
|
401
|
+
method: "POST",
|
|
402
|
+
path: `${RUN}/steer`,
|
|
403
|
+
response: void 0
|
|
404
|
+
},
|
|
405
|
+
/**
|
|
406
|
+
* Launch a workflow into the mounting session's own runtime, which then owns
|
|
407
|
+
* the run. A dispatcher session reaches its root's session mount through this.
|
|
408
|
+
*/
|
|
409
|
+
launch: {
|
|
410
|
+
method: "POST",
|
|
411
|
+
path: "/launch",
|
|
412
|
+
response: void 0
|
|
413
|
+
},
|
|
414
|
+
openRunDirectory: {
|
|
415
|
+
method: "POST",
|
|
416
|
+
path: `${RUN}/open-directory`,
|
|
417
|
+
response: void 0
|
|
418
|
+
},
|
|
419
|
+
artifacts: {
|
|
420
|
+
method: "GET",
|
|
421
|
+
path: `${RUN}/artifacts`,
|
|
422
|
+
query: ["directory"],
|
|
423
|
+
response: void 0
|
|
424
|
+
},
|
|
425
|
+
/** One artifact: its metadata and text, or its raw bytes under `raw=1`. */
|
|
426
|
+
artifact: {
|
|
427
|
+
method: "GET",
|
|
428
|
+
path: `${RUN}/artifacts/${ARTIFACT_NAME_PARAM}`,
|
|
429
|
+
query: ["raw", ARTIFACT_DOWNLOAD_PARAM],
|
|
430
|
+
response: void 0
|
|
431
|
+
}
|
|
432
|
+
}, {
|
|
433
|
+
scopes: [
|
|
434
|
+
"global",
|
|
435
|
+
"workspace",
|
|
436
|
+
"session"
|
|
437
|
+
],
|
|
438
|
+
basePath: "workflow",
|
|
439
|
+
transport: nativeApiTransport,
|
|
440
|
+
sessionAddress: sessionApiAddress
|
|
441
|
+
});
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_lib/terminalApi.native.ts
|
|
444
|
+
/**
|
|
445
|
+
* The page's half of this package's hub API: one run's terminal and the files
|
|
446
|
+
* its run directory holds.
|
|
447
|
+
*
|
|
448
|
+
* These are adapters now, not a transport. The generated client owns the URL
|
|
449
|
+
* and the sealed transport with it, so nothing here spells a mount. What stays
|
|
450
|
+
* is this package's own vocabulary: which refusals the reader is shown, and the
|
|
451
|
+
* shapes the terminal expects back.
|
|
452
|
+
*
|
|
453
|
+
* A run belongs to its multiplexer rather than to a session, so the same API
|
|
454
|
+
* answers at the hub and inside a session's own server. Passing a session id
|
|
455
|
+
* addresses that session's copy and passing none addresses the hub. The deleted
|
|
456
|
+
* builder made that branch the other way round: it always built the hub's
|
|
457
|
+
* absolute URL, then rewrote the plugin prefix into the session's with
|
|
458
|
+
* String.replace, so a session's URL was a hub URL with its head swapped.
|
|
459
|
+
*/
|
|
460
|
+
const UNREACHABLE = "The cockpit hub is unreachable.";
|
|
461
|
+
/** The hub, or one session's own copy of it; a null session id is the hub. */
|
|
462
|
+
function scoped(sessionId) {
|
|
463
|
+
return sessionId == null ? api.global : api.session(sessionId);
|
|
464
|
+
}
|
|
465
|
+
/** The run every route addresses, as the route table's two path parameters. */
|
|
466
|
+
function runParams(workspace, runKey) {
|
|
467
|
+
return {
|
|
468
|
+
workspace,
|
|
469
|
+
runKey
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function isRecord(value) {
|
|
473
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* The message a reader is shown: the route's own words, or why there were none.
|
|
477
|
+
*
|
|
478
|
+
* `error` is optional because an answered call that simply carried the wrong
|
|
479
|
+
* shape reaches here too, and that result has no error field at all.
|
|
480
|
+
*/
|
|
481
|
+
function messageOf(result, fallback) {
|
|
482
|
+
if (result.status === 0) return UNREACHABLE;
|
|
483
|
+
return result.error === void 0 || result.error === "" ? fallback : result.error;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* One artifact's URL, with its path filled in.
|
|
487
|
+
*
|
|
488
|
+
* An artifact is named by its path inside the run directory, which Hono matches
|
|
489
|
+
* as the one multi-segment parameter `:name{.+}`. The client's own `params`
|
|
490
|
+
* substitution fills a single segment and percent-encodes the separating slash
|
|
491
|
+
* with it, which would move every nested artifact's URL, so that one segment is
|
|
492
|
+
* substituted here instead: each segment encoded, the slashes between them
|
|
493
|
+
* kept. Everything left of it still comes from the client.
|
|
494
|
+
*/
|
|
495
|
+
function artifactUrl(scope, workspace, runKey, artifactPath, query) {
|
|
496
|
+
return scope.artifact.url({
|
|
497
|
+
params: runParams(workspace, runKey),
|
|
498
|
+
...query === void 0 ? {} : { query }
|
|
499
|
+
}).replace(ARTIFACT_NAME_PARAM, artifactPath.split("/").map(encodeURIComponent).join("/"));
|
|
500
|
+
}
|
|
501
|
+
/** The pane a terminal call names, when it reads one step's rather than the run's current one. */
|
|
502
|
+
function stepBody(step) {
|
|
503
|
+
return step === void 0 ? {} : { step };
|
|
504
|
+
}
|
|
505
|
+
/** One finite screen read over the session-bound sealed transport. */
|
|
506
|
+
async function readScreen(workspace, runKey, sessionId, step, signal) {
|
|
507
|
+
const response = await nativeApiTransport(scoped(sessionId).snapshot.url({
|
|
508
|
+
params: runParams(workspace, runKey),
|
|
509
|
+
query: { [STEP_TARGET_PARAM]: step }
|
|
510
|
+
}), { signal });
|
|
511
|
+
const body = await response.json();
|
|
512
|
+
if (!response.ok) throw new Error(isRecord(body) && typeof body.error === "string" ? body.error : "The screen could not be read.");
|
|
513
|
+
if (!isRecord(body) || !Array.isArray(body.lines) || body.lines.length > 48 || !body.lines.every((line) => typeof line === "string" && line.length <= 4096) || !isRecord(body.capabilities) || typeof body.capabilities.readable !== "boolean" || typeof body.capabilities.writable !== "boolean" || typeof body.capabilities.resizable !== "boolean") throw new Error("The workflow hub returned an invalid screen.");
|
|
514
|
+
return body;
|
|
515
|
+
}
|
|
516
|
+
/** Takes the keyboard for one run, renewing when this page already holds it. */
|
|
517
|
+
async function takeControl(workspace, runKey, token, sessionId, step) {
|
|
518
|
+
const result = await scoped(sessionId).control({
|
|
519
|
+
params: runParams(workspace, runKey),
|
|
520
|
+
body: {
|
|
521
|
+
token,
|
|
522
|
+
...stepBody(step)
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
if (result.status === 0) return {
|
|
526
|
+
held: false,
|
|
527
|
+
reason: UNREACHABLE
|
|
528
|
+
};
|
|
529
|
+
if (isRecord(result.data) && typeof result.data.held === "boolean") return result.data;
|
|
530
|
+
return {
|
|
531
|
+
held: false,
|
|
532
|
+
reason: messageOf(result, "The run refused the keyboard.")
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async function releaseControl(workspace, runKey, token, sessionId, step) {
|
|
536
|
+
await scoped(sessionId).control({
|
|
537
|
+
params: runParams(workspace, runKey),
|
|
538
|
+
body: {
|
|
539
|
+
token,
|
|
540
|
+
release: true,
|
|
541
|
+
...stepBody(step)
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
/** Sends literal keystrokes; the reason comes back when the lease has moved on. */
|
|
546
|
+
async function sendKeys(workspace, runKey, token, data, sessionId, step) {
|
|
547
|
+
const result = await scoped(sessionId).keys({
|
|
548
|
+
params: runParams(workspace, runKey),
|
|
549
|
+
body: {
|
|
550
|
+
token,
|
|
551
|
+
data,
|
|
552
|
+
...stepBody(step)
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
if (result.ok) return {};
|
|
556
|
+
return { error: messageOf(result, "The run would not take those keys.") };
|
|
557
|
+
}
|
|
558
|
+
/** Matches the run's terminal to the viewport of whoever holds the keyboard. */
|
|
559
|
+
async function resizeRun(workspace, runKey, token, columns, rows, sessionId, step) {
|
|
560
|
+
await scoped(sessionId).resize({
|
|
561
|
+
params: runParams(workspace, runKey),
|
|
562
|
+
body: {
|
|
563
|
+
token,
|
|
564
|
+
columns,
|
|
565
|
+
rows,
|
|
566
|
+
...stepBody(step)
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/** Permanently removes one settled run and its run directory. */
|
|
571
|
+
async function deleteWorkflowRun(workspace, runKey, sessionId) {
|
|
572
|
+
const result = await scoped(sessionId).remove({ params: runParams(workspace, runKey) });
|
|
573
|
+
if (!result.ok) return { error: messageOf(result, "The workflow could not be deleted.") };
|
|
574
|
+
if (result.data.deleted === true) return { result: { deleted: true } };
|
|
575
|
+
return { error: "The workflow hub returned an invalid deletion response." };
|
|
576
|
+
}
|
|
577
|
+
/** Asks the engine running a workflow to stop it; the run settles on its own shortly after. */
|
|
578
|
+
async function stopWorkflowRun(workspace, runKey, sessionId) {
|
|
579
|
+
const result = await scoped(sessionId).stop({ params: runParams(workspace, runKey) });
|
|
580
|
+
if (!result.ok) return { error: messageOf(result, "The workflow could not be stopped.") };
|
|
581
|
+
if (result.data.requested === true) return { result: { requested: true } };
|
|
582
|
+
return { error: "The workflow hub returned an invalid stop response." };
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Sends guidance to the agent session a running step is in: the run's current
|
|
586
|
+
* step, or `step`, the session of one step of a parallel group.
|
|
587
|
+
*/
|
|
588
|
+
async function steerStep(workspace, runKey, message, sessionId, step) {
|
|
589
|
+
const result = await scoped(sessionId).steer({
|
|
590
|
+
params: runParams(workspace, runKey),
|
|
591
|
+
body: step === void 0 ? { message } : {
|
|
592
|
+
message,
|
|
593
|
+
step
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
if (!result.ok) return { error: messageOf(result, "The step would not take that guidance.") };
|
|
597
|
+
if (result.data.delivered === true) return { result: { delivered: true } };
|
|
598
|
+
return { error: "The workflow hub returned an invalid steer response." };
|
|
599
|
+
}
|
|
600
|
+
async function fetchArtifacts(workspace, runKey, sessionId, directory) {
|
|
601
|
+
const result = await scoped(sessionId).artifacts({
|
|
602
|
+
params: runParams(workspace, runKey),
|
|
603
|
+
query: { directory }
|
|
604
|
+
});
|
|
605
|
+
if (!result.ok) return { error: messageOf(result, "This run has no directory to read.") };
|
|
606
|
+
return { artifacts: result.data };
|
|
607
|
+
}
|
|
608
|
+
/** Trusted same-origin URL for browser-native media previews and downloads. */
|
|
609
|
+
function artifactContentUrl(workspace, runKey, path, download = false, sessionId) {
|
|
610
|
+
return artifactUrl(scoped(sessionId), workspace, runKey, path, {
|
|
611
|
+
["raw"]: 1,
|
|
612
|
+
...download ? { [ARTIFACT_DOWNLOAD_PARAM]: 1 } : {}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* One artifact's metadata and text.
|
|
617
|
+
*
|
|
618
|
+
* The only route here that addresses itself rather than being called: its path
|
|
619
|
+
* parameter spans segments, so the URL comes from `artifactUrl` and the request
|
|
620
|
+
* goes over the same sealed transport the client is built on.
|
|
621
|
+
*/
|
|
622
|
+
async function fetchArtifact(workspace, runKey, path, sessionId) {
|
|
623
|
+
try {
|
|
624
|
+
const response = await nativeApiTransport(artifactUrl(scoped(sessionId), workspace, runKey, path));
|
|
625
|
+
const body = await response.json();
|
|
626
|
+
if (!response.ok) {
|
|
627
|
+
const error = isRecord(body) && typeof body.error === "string" && body.error !== "" ? body.error : "";
|
|
628
|
+
return { error: error === "" ? "That file has not been written." : error };
|
|
629
|
+
}
|
|
630
|
+
return { artifact: body };
|
|
631
|
+
} catch {
|
|
632
|
+
return { error: UNREACHABLE };
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const MAX_MEDIA_BYTES = 5242880;
|
|
636
|
+
const MAX_FRAME_BYTES = 65536;
|
|
637
|
+
/** Drain each UTF-8 snapshot before asking the server for another screen. */
|
|
638
|
+
function finiteScreenReader(load) {
|
|
639
|
+
let bytes = /* @__PURE__ */ new Uint8Array(0);
|
|
640
|
+
let cursor = 0;
|
|
641
|
+
let generation = 0;
|
|
642
|
+
let frame;
|
|
643
|
+
return async (from, previous, signal) => {
|
|
644
|
+
if (signal.aborted) throw new Error("Preview cancelled.");
|
|
645
|
+
let reset = previous !== String(generation) || from !== cursor;
|
|
646
|
+
if (!frame || cursor >= bytes.length || reset) {
|
|
647
|
+
const next = await load(signal);
|
|
648
|
+
if (signal.aborted) throw new Error("Preview cancelled.");
|
|
649
|
+
frame = next;
|
|
650
|
+
bytes = utf8(next.lines.join("\r\n"));
|
|
651
|
+
cursor = 0;
|
|
652
|
+
generation += 1;
|
|
653
|
+
reset = true;
|
|
654
|
+
}
|
|
655
|
+
const chunk = bytes.subarray(cursor, cursor + MAX_FRAME_BYTES);
|
|
656
|
+
cursor += chunk.length;
|
|
657
|
+
return {
|
|
658
|
+
chunk: base64(chunk),
|
|
659
|
+
offset: cursor,
|
|
660
|
+
reset,
|
|
661
|
+
generation: String(generation),
|
|
662
|
+
exists: frame.capabilities.readable,
|
|
663
|
+
hasMore: cursor < bytes.length,
|
|
664
|
+
ended: frame.ended === true
|
|
665
|
+
};
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/** Raw media uses the same scoped sealed route as artifact metadata. */
|
|
669
|
+
async function fetchArtifactMedia(workspace, runKey, path, sessionId, signal) {
|
|
670
|
+
if (signal.aborted) throw new Error("Preview cancelled.");
|
|
671
|
+
const response = await nativeApiTransport(artifactContentUrl(workspace, runKey, path, false, sessionId), { signal });
|
|
672
|
+
if (signal.aborted) throw new Error("Preview cancelled.");
|
|
673
|
+
if (!response.ok) throw new Error("That artifact could not be read.");
|
|
674
|
+
const mimeType = (response.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
|
|
675
|
+
if (!/^(application\/pdf|image\/(png|jpeg|gif|webp|bmp)|audio\/(mpeg|mp4|wav|x-wav|ogg|webm|aac|flac)|video\/(mp4|webm|quicktime|ogg))$/.test(mimeType)) throw new Error("This artifact type cannot be previewed.");
|
|
676
|
+
if (Number(response.headers.get("content-length")) > MAX_MEDIA_BYTES) throw new Error("This artifact exceeds the 5 MiB preview limit.");
|
|
677
|
+
const bytes = await response.arrayBuffer();
|
|
678
|
+
if (signal.aborted) throw new Error("Preview cancelled.");
|
|
679
|
+
if (bytes.byteLength > MAX_MEDIA_BYTES) throw new Error("This artifact exceeds the 5 MiB preview limit.");
|
|
680
|
+
return {
|
|
681
|
+
data: base64(new Uint8Array(bytes)),
|
|
682
|
+
mimeType
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function base64(bytes) {
|
|
686
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
687
|
+
const parts = [];
|
|
688
|
+
let chunk = "";
|
|
689
|
+
for (let offset = 0; offset < bytes.length; offset += 3) {
|
|
690
|
+
const first = bytes[offset];
|
|
691
|
+
const second = bytes[offset + 1];
|
|
692
|
+
const third = bytes[offset + 2];
|
|
693
|
+
chunk += alphabet[first >> 2] + alphabet[(first & 3) << 4 | (second ?? 0) >> 4] + (second === void 0 ? "=" : alphabet[(second & 15) << 2 | (third ?? 0) >> 6]) + (third === void 0 ? "=" : alphabet[third & 63]);
|
|
694
|
+
if (chunk.length >= 8192) {
|
|
695
|
+
parts.push(chunk);
|
|
696
|
+
chunk = "";
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
parts.push(chunk);
|
|
700
|
+
return parts.join("");
|
|
701
|
+
}
|
|
702
|
+
function utf8(text) {
|
|
703
|
+
const bytes = [];
|
|
704
|
+
for (const character of text) {
|
|
705
|
+
let code = character.codePointAt(0);
|
|
706
|
+
if (code >= 55296 && code <= 57343) code = 65533;
|
|
707
|
+
if (code < 128) bytes.push(code);
|
|
708
|
+
else if (code < 2048) bytes.push(192 | code >> 6, 128 | code & 63);
|
|
709
|
+
else if (code < 65536) bytes.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
|
|
710
|
+
else bytes.push(240 | code >> 18, 128 | code >> 12 & 63, 128 | code >> 6 & 63, 128 | code & 63);
|
|
711
|
+
}
|
|
712
|
+
return Uint8Array.from(bytes);
|
|
713
|
+
}
|
|
714
|
+
/** Binds a save to this package's guarded sealed route and one file version. */
|
|
715
|
+
async function prepareArtifactSave(workspace, runKey, path, sessionId, signal) {
|
|
716
|
+
const load = async (headers, signal) => {
|
|
717
|
+
if (signal.aborted) throw new Error("Download cancelled.");
|
|
718
|
+
return nativeApiTransport(artifactContentUrl(workspace, runKey, path, true, sessionId), {
|
|
719
|
+
headers,
|
|
720
|
+
signal
|
|
721
|
+
});
|
|
722
|
+
};
|
|
723
|
+
const probe = await load({ Range: "bytes=0-0" }, signal);
|
|
724
|
+
if (signal.aborted) throw new Error("Download cancelled.");
|
|
725
|
+
const range = /^bytes 0-0\/(\d+)$/u.exec(probe.headers.get("content-range") ?? "");
|
|
726
|
+
const empty = probe.status === 416 && probe.headers.get("content-range") === "bytes */0" || probe.status === 200 && probe.headers.get("content-length") === "0";
|
|
727
|
+
if (!empty && (probe.status !== 206 || !range)) throw new Error("The file does not support bounded downloads.");
|
|
728
|
+
const size = empty ? 0 : Number(range[1]);
|
|
729
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new Error("Invalid file size.");
|
|
730
|
+
if ((await probe.arrayBuffer()).byteLength !== (empty ? 0 : 1)) throw new Error("Invalid file response.");
|
|
731
|
+
const version = probe.headers.get("etag");
|
|
732
|
+
if (!version) throw new Error("The file has no version validator.");
|
|
733
|
+
return {
|
|
734
|
+
name: path.split(/[\\/]/u).at(-1) ?? "",
|
|
735
|
+
mimeType: probe.headers.get("content-type")?.split(";")[0]?.trim() || "application/octet-stream",
|
|
736
|
+
size,
|
|
737
|
+
signal,
|
|
738
|
+
read: async (offset, count, signal) => {
|
|
739
|
+
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(count) || offset < 0 || count < 1 || count > 1048576 || offset + count > size) throw new Error("Invalid download range.");
|
|
740
|
+
const end = offset + count - 1;
|
|
741
|
+
const response = await load({
|
|
742
|
+
Range: `bytes=${offset}-${end}`,
|
|
743
|
+
"If-Match": version
|
|
744
|
+
}, signal);
|
|
745
|
+
if (signal.aborted) throw new Error("Download cancelled.");
|
|
746
|
+
if (response.status !== 206 || response.headers.get("etag") !== version || response.headers.get("content-range") !== `bytes ${offset}-${end}/${size}`) throw new Error("The file changed or its range response is invalid.");
|
|
747
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
748
|
+
if (signal.aborted) throw new Error("Download cancelled.");
|
|
749
|
+
if (bytes.byteLength !== count) throw new Error("The file response is incomplete.");
|
|
750
|
+
return bytes;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
//#endregion
|
|
755
|
+
//#region src/extensions/workspaces/sessions/(frontend)/_components/WorkflowPanel.native.tsx
|
|
756
|
+
function useWorkflows(sessionId) {
|
|
757
|
+
const subscribe = useCallback((listener) => {
|
|
758
|
+
const sub = workflows.store.subscribe(listener);
|
|
759
|
+
return () => sub.unsubscribe();
|
|
760
|
+
}, []);
|
|
761
|
+
return useSyncExternalStore(subscribe, () => workflows.select(workflows.store.state, sessionId));
|
|
762
|
+
}
|
|
763
|
+
function workflowTab(run) {
|
|
764
|
+
const identity = workflowRunIdentity(run);
|
|
765
|
+
return {
|
|
766
|
+
id: `workflows-run-${run.workspace}-${run.runKey}`.replace(/[^\w-]+/g, "-"),
|
|
767
|
+
label: run.displayName,
|
|
768
|
+
retainComposer: true,
|
|
769
|
+
panel: (props) => /* @__PURE__ */ jsx(WorkflowPanel, {
|
|
770
|
+
...props,
|
|
771
|
+
identity
|
|
772
|
+
})
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function WorkflowLauncher(props) {
|
|
776
|
+
const subscribe = useCallback((listener) => {
|
|
777
|
+
const sub = catalog.store.subscribe(listener);
|
|
778
|
+
return () => sub.unsubscribe();
|
|
779
|
+
}, []);
|
|
780
|
+
const state = useSyncExternalStore(subscribe, () => catalog.select(catalog.store.state, props.sessionId));
|
|
781
|
+
const [filter, setFilter] = useState("");
|
|
782
|
+
const [selected, setSelected] = useState();
|
|
783
|
+
if (props.sessionId === null || !state.open) return null;
|
|
784
|
+
const workflow = state.workflows.find((item) => item.path === selected);
|
|
785
|
+
return /* @__PURE__ */ jsx(Sheet, {
|
|
786
|
+
open: true,
|
|
787
|
+
onOpenChange: (open) => {
|
|
788
|
+
if (!open) {
|
|
789
|
+
closeCatalog(props.sessionId);
|
|
790
|
+
setSelected(void 0);
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: workflow ? `Launch ${workflow.name}` : "Launch a workflow" }) }), /* @__PURE__ */ jsx(SheetBody, { children: workflow ? /* @__PURE__ */ jsx(WorkflowLaunch, {
|
|
794
|
+
...props,
|
|
795
|
+
sessionId: props.sessionId,
|
|
796
|
+
workflow,
|
|
797
|
+
initialPrompt: state.prompt,
|
|
798
|
+
onClose: () => {
|
|
799
|
+
closeCatalog(props.sessionId);
|
|
800
|
+
setSelected(void 0);
|
|
801
|
+
}
|
|
802
|
+
}, workflow.path) : /* @__PURE__ */ jsxs(View, {
|
|
803
|
+
style: styles.body,
|
|
804
|
+
children: [
|
|
805
|
+
/* @__PURE__ */ jsx(Input, {
|
|
806
|
+
accessibilityLabel: "Find a workflow",
|
|
807
|
+
value: filter,
|
|
808
|
+
onChangeText: setFilter,
|
|
809
|
+
placeholder: "Find a workflow"
|
|
810
|
+
}),
|
|
811
|
+
state.warning ? /* @__PURE__ */ jsx(Text, {
|
|
812
|
+
style: styles.error,
|
|
813
|
+
children: state.warning
|
|
814
|
+
}) : null,
|
|
815
|
+
filterCatalog(state.workflows, filter).map((item) => /* @__PURE__ */ jsx(ListRow, {
|
|
816
|
+
title: /* @__PURE__ */ jsx(Button, {
|
|
817
|
+
variant: "ghost",
|
|
818
|
+
onPress: () => setSelected(item.path),
|
|
819
|
+
children: item.name || item.relativePath
|
|
820
|
+
}),
|
|
821
|
+
description: `${item.description}\n${item.tags.join(", ")} · ${item.jobs.map((job) => job.name).join(", ")}`
|
|
822
|
+
}, item.path)),
|
|
823
|
+
state.workflows.length === 0 ? /* @__PURE__ */ jsx(Text, {
|
|
824
|
+
style: styles.meta,
|
|
825
|
+
children: "No workflows reported yet."
|
|
826
|
+
}) : null
|
|
827
|
+
]
|
|
828
|
+
}) })] })
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
function Choice({ label, options, value, onChange }) {
|
|
832
|
+
return /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsx(Text, {
|
|
833
|
+
style: styles.meta,
|
|
834
|
+
children: label
|
|
835
|
+
}), options.map((option) => /* @__PURE__ */ jsx(Button, {
|
|
836
|
+
variant: "outline",
|
|
837
|
+
accessibilityState: { selected: value === option },
|
|
838
|
+
onPress: () => onChange(option),
|
|
839
|
+
children: option
|
|
840
|
+
}, option))] });
|
|
841
|
+
}
|
|
842
|
+
function WorkflowLaunch({ sessionId, workflow, initialPrompt, sendSessionFrame, onClose }) {
|
|
843
|
+
const [prompt, setPrompt] = useState(initialPrompt);
|
|
844
|
+
const [inputs, setInputs] = useState(() => initialInputs(workflow));
|
|
845
|
+
const [runner, setRunner] = useState(() => initialRunner(workflow));
|
|
846
|
+
const [command, setCommand] = useState(() => initialCommand(workflow));
|
|
847
|
+
const [choice, setChoice] = useState(() => initialChoice(workflow));
|
|
848
|
+
const [error, setError] = useState();
|
|
849
|
+
const request = {
|
|
850
|
+
workflow: workflow.name || workflow.relativePath,
|
|
851
|
+
inputs,
|
|
852
|
+
runner,
|
|
853
|
+
command,
|
|
854
|
+
choice,
|
|
855
|
+
...prompt.trim() === "" ? {} : { prompt: prompt.trim() }
|
|
856
|
+
};
|
|
857
|
+
const problems = launchProblems(workflow, request);
|
|
858
|
+
const line = workflowLaunchLine(request);
|
|
859
|
+
return /* @__PURE__ */ jsxs(View, {
|
|
860
|
+
style: styles.body,
|
|
861
|
+
children: [
|
|
862
|
+
/* @__PURE__ */ jsx(Text, {
|
|
863
|
+
style: styles.meta,
|
|
864
|
+
children: workflow.description
|
|
865
|
+
}),
|
|
866
|
+
/* @__PURE__ */ jsx(Textarea, {
|
|
867
|
+
accessibilityLabel: "Workflow prompt",
|
|
868
|
+
placeholder: "What should this workflow do?",
|
|
869
|
+
value: prompt,
|
|
870
|
+
onChangeText: setPrompt
|
|
871
|
+
}),
|
|
872
|
+
workflow.inputs.map((input) => {
|
|
873
|
+
const options = input.options ?? (input.type === "boolean" ? ["true", "false"] : void 0);
|
|
874
|
+
return /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Text, {
|
|
875
|
+
style: styles.meta,
|
|
876
|
+
children: [
|
|
877
|
+
input.name,
|
|
878
|
+
input.required ? " (required)" : "",
|
|
879
|
+
" · ",
|
|
880
|
+
input.description
|
|
881
|
+
]
|
|
882
|
+
}), options ? /* @__PURE__ */ jsx(Choice, {
|
|
883
|
+
label: input.name,
|
|
884
|
+
options,
|
|
885
|
+
value: inputs[input.name],
|
|
886
|
+
onChange: (value) => setInputs((current) => ({
|
|
887
|
+
...current,
|
|
888
|
+
[input.name]: value
|
|
889
|
+
}))
|
|
890
|
+
}) : /* @__PURE__ */ jsx(Input, {
|
|
891
|
+
accessibilityLabel: input.name,
|
|
892
|
+
value: inputs[input.name] ?? "",
|
|
893
|
+
onChangeText: (value) => setInputs((current) => ({
|
|
894
|
+
...current,
|
|
895
|
+
[input.name]: value
|
|
896
|
+
}))
|
|
897
|
+
})] }, input.name);
|
|
898
|
+
}),
|
|
899
|
+
workflow.runners?.length ? /* @__PURE__ */ jsx(Choice, {
|
|
900
|
+
label: "Runner",
|
|
901
|
+
options: workflow.runners,
|
|
902
|
+
value: runner,
|
|
903
|
+
onChange: setRunner
|
|
904
|
+
}) : null,
|
|
905
|
+
workflow.commands?.length ? /* @__PURE__ */ jsx(Choice, {
|
|
906
|
+
label: "Command",
|
|
907
|
+
options: workflow.commands,
|
|
908
|
+
value: command,
|
|
909
|
+
onChange: setCommand
|
|
910
|
+
}) : null,
|
|
911
|
+
workflow.choices?.length ? /* @__PURE__ */ jsx(Choice, {
|
|
912
|
+
label: "Choice",
|
|
913
|
+
options: workflow.choices,
|
|
914
|
+
value: choice,
|
|
915
|
+
onChange: setChoice
|
|
916
|
+
}) : null,
|
|
917
|
+
/* @__PURE__ */ jsx(CodeBlock, { text: line }),
|
|
918
|
+
problems.map((problem) => /* @__PURE__ */ jsx(Text, {
|
|
919
|
+
style: styles.meta,
|
|
920
|
+
children: problem
|
|
921
|
+
}, problem)),
|
|
922
|
+
error ? /* @__PURE__ */ jsx(Text, {
|
|
923
|
+
style: styles.error,
|
|
924
|
+
children: error
|
|
925
|
+
}) : null,
|
|
926
|
+
/* @__PURE__ */ jsx(Button, {
|
|
927
|
+
testID: "launch-workflow-submit",
|
|
928
|
+
disabled: problems.length > 0,
|
|
929
|
+
onPress: () => {
|
|
930
|
+
try {
|
|
931
|
+
requestLaunch(sendSessionFrame, sessionId, line, workflow.path);
|
|
932
|
+
onClose();
|
|
933
|
+
} catch (cause) {
|
|
934
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
935
|
+
}
|
|
936
|
+
},
|
|
937
|
+
children: "Launch"
|
|
938
|
+
})
|
|
939
|
+
]
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
function WorkflowPanel(props) {
|
|
943
|
+
const state = useWorkflows(props.sessionId);
|
|
944
|
+
const run = state.runs.find((item) => workflowRunIdentity(item) === props.identity);
|
|
945
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
946
|
+
const [busy, setBusy] = useState(false);
|
|
947
|
+
const [error, setError] = useState();
|
|
948
|
+
const act = async (action) => {
|
|
949
|
+
if (busy) return;
|
|
950
|
+
setBusy(true);
|
|
951
|
+
setError(void 0);
|
|
952
|
+
try {
|
|
953
|
+
await action();
|
|
954
|
+
} catch (cause) {
|
|
955
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
956
|
+
} finally {
|
|
957
|
+
setBusy(false);
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
if (!run) return /* @__PURE__ */ jsx(Text, {
|
|
961
|
+
style: styles.meta,
|
|
962
|
+
children: "This run is no longer listed."
|
|
963
|
+
});
|
|
964
|
+
return /* @__PURE__ */ jsxs(ScrollView, {
|
|
965
|
+
contentContainerStyle: styles.body,
|
|
966
|
+
children: [
|
|
967
|
+
/* @__PURE__ */ jsx(Text, {
|
|
968
|
+
style: styles.title,
|
|
969
|
+
children: run.displayName
|
|
970
|
+
}),
|
|
971
|
+
/* @__PURE__ */ jsx(Badge, { children: run.executionState ?? run.outcome ?? run.stage }),
|
|
972
|
+
/* @__PURE__ */ jsx(Text, {
|
|
973
|
+
style: styles.meta,
|
|
974
|
+
children: run.errorMessage ?? `${run.position?.job ?? ""} ${run.position?.step ?? ""}`
|
|
975
|
+
}),
|
|
976
|
+
state.runs.length > 1 ? state.runs.map((item) => /* @__PURE__ */ jsx(Button, {
|
|
977
|
+
variant: "ghost",
|
|
978
|
+
onPress: () => {
|
|
979
|
+
if (props.sessionId !== null) focusRun(props.sessionId, workflowRunIdentity(item));
|
|
980
|
+
props.openTransientTab(workflowTab(item));
|
|
981
|
+
},
|
|
982
|
+
children: item.displayName
|
|
983
|
+
}, workflowRunIdentity(item))) : null,
|
|
984
|
+
run.executionState === "paused" || run.executionState === "pause_requested" ? /* @__PURE__ */ jsx(Text, {
|
|
985
|
+
style: styles.meta,
|
|
986
|
+
children: "Waiting to be resumed. Ask the agent in this conversation to resume it."
|
|
987
|
+
}) : null,
|
|
988
|
+
run.jobs.map((job) => /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
989
|
+
variant: "ghost",
|
|
990
|
+
onPress: () => props.openTransientTab(stepTab(run, job.name)),
|
|
991
|
+
children: [
|
|
992
|
+
job.name,
|
|
993
|
+
" · ",
|
|
994
|
+
job.status
|
|
995
|
+
]
|
|
996
|
+
}), job.steps.map((step) => /* @__PURE__ */ jsx(ListRow, {
|
|
997
|
+
title: /* @__PURE__ */ jsx(Button, {
|
|
998
|
+
variant: "ghost",
|
|
999
|
+
onPress: () => props.openTransientTab(stepTab(run, job.name, step.name)),
|
|
1000
|
+
children: step.name
|
|
1001
|
+
}),
|
|
1002
|
+
description: step.status
|
|
1003
|
+
}, step.name))] }, job.name)),
|
|
1004
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1005
|
+
style: styles.title,
|
|
1006
|
+
children: "Artifacts"
|
|
1007
|
+
}),
|
|
1008
|
+
/* @__PURE__ */ jsx(WorkflowArtifacts, {
|
|
1009
|
+
...props,
|
|
1010
|
+
run
|
|
1011
|
+
}),
|
|
1012
|
+
error ? /* @__PURE__ */ jsx(Text, {
|
|
1013
|
+
style: styles.error,
|
|
1014
|
+
children: error
|
|
1015
|
+
}) : null,
|
|
1016
|
+
run.stage === "running" ? /* @__PURE__ */ jsx(Button, {
|
|
1017
|
+
variant: "danger-outline",
|
|
1018
|
+
disabled: busy || props.sessionId === null,
|
|
1019
|
+
onPress: () => void act(async () => {
|
|
1020
|
+
const result = await stopWorkflowRun(run.workspace, run.runKey, props.sessionId);
|
|
1021
|
+
if ("error" in result) throw new Error(result.error);
|
|
1022
|
+
}),
|
|
1023
|
+
children: "Stop workflow"
|
|
1024
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
1025
|
+
variant: "danger-outline",
|
|
1026
|
+
disabled: props.sessionId === null,
|
|
1027
|
+
onPress: () => setConfirmDelete(true),
|
|
1028
|
+
children: "Delete run…"
|
|
1029
|
+
}),
|
|
1030
|
+
/* @__PURE__ */ jsx(Sheet, {
|
|
1031
|
+
open: confirmDelete,
|
|
1032
|
+
onOpenChange: setConfirmDelete,
|
|
1033
|
+
children: /* @__PURE__ */ jsxs(SheetContent, { children: [/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsx(SheetTitle, { children: "Delete this run?" }) }), /* @__PURE__ */ jsxs(SheetBody, { children: [/* @__PURE__ */ jsx(Text, {
|
|
1034
|
+
style: styles.meta,
|
|
1035
|
+
children: "This permanently removes the settled run and its directory."
|
|
1036
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1037
|
+
variant: "danger-outline",
|
|
1038
|
+
disabled: busy,
|
|
1039
|
+
onPress: () => void act(async () => {
|
|
1040
|
+
const result = await deleteWorkflowRun(run.workspace, run.runKey, props.sessionId);
|
|
1041
|
+
if ("error" in result) throw new Error(result.error);
|
|
1042
|
+
if (props.sessionId !== null) removeRun(props.sessionId, props.identity);
|
|
1043
|
+
setConfirmDelete(false);
|
|
1044
|
+
props.closeTransientTab(`workflows-run-${run.workspace}-${run.runKey}`.replace(/[^\w-]+/g, "-"));
|
|
1045
|
+
}),
|
|
1046
|
+
children: "Delete"
|
|
1047
|
+
})] })] })
|
|
1048
|
+
})
|
|
1049
|
+
]
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function stepTab(run, job, step) {
|
|
1053
|
+
const workspace = run.workspace;
|
|
1054
|
+
const runKey = run.runKey;
|
|
1055
|
+
return {
|
|
1056
|
+
id: `workflows-step-${workspace}-${runKey}-${job}${step === void 0 ? "" : `-${step}`}`.replace(/[^\w-]+/g, "-"),
|
|
1057
|
+
label: `${run.displayName} › ${step ?? job}`,
|
|
1058
|
+
panel: (props) => /* @__PURE__ */ jsx(WorkflowStep, {
|
|
1059
|
+
...props,
|
|
1060
|
+
workspace,
|
|
1061
|
+
runKey,
|
|
1062
|
+
job,
|
|
1063
|
+
step
|
|
1064
|
+
})
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function WorkflowStep({ sessionId, workspace, runKey, job, step, renderSessionTranscript, renderRunnerTerminal }) {
|
|
1068
|
+
const run = useWorkflows(sessionId).runs.find((item) => item.workspace === workspace && item.runKey === runKey);
|
|
1069
|
+
const steps = run?.jobs.find((item) => item.name === job)?.steps;
|
|
1070
|
+
const target = step === void 0 ? steps?.at(-1) : steps?.find((item) => item.name === step);
|
|
1071
|
+
const [message, setMessage] = useState("");
|
|
1072
|
+
const [error, setError] = useState();
|
|
1073
|
+
const [busy, setBusy] = useState(false);
|
|
1074
|
+
const ref = target?.ref;
|
|
1075
|
+
const session = ref?.kind === "session" ? ref.id : void 0;
|
|
1076
|
+
return /* @__PURE__ */ jsxs(View, {
|
|
1077
|
+
style: styles.screen,
|
|
1078
|
+
children: [
|
|
1079
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
1080
|
+
style: styles.title,
|
|
1081
|
+
children: [
|
|
1082
|
+
job,
|
|
1083
|
+
step === void 0 ? "" : ` › ${step}`,
|
|
1084
|
+
" · ",
|
|
1085
|
+
target?.status
|
|
1086
|
+
]
|
|
1087
|
+
}),
|
|
1088
|
+
session ? renderSessionTranscript?.(session) : /* @__PURE__ */ jsx(WorkflowTerminal, {
|
|
1089
|
+
sessionId,
|
|
1090
|
+
workspace,
|
|
1091
|
+
runKey,
|
|
1092
|
+
pane: ref?.kind === "pane" ? ref.id : void 0,
|
|
1093
|
+
renderRunnerTerminal
|
|
1094
|
+
}, `${sessionId}:${workspace}:${runKey}:${ref?.id ?? ""}`),
|
|
1095
|
+
session && run?.stage === "running" && target?.status === "running" ? /* @__PURE__ */ jsxs(View, {
|
|
1096
|
+
style: styles.body,
|
|
1097
|
+
children: [/* @__PURE__ */ jsx(Textarea, {
|
|
1098
|
+
accessibilityLabel: "Step guidance",
|
|
1099
|
+
value: message,
|
|
1100
|
+
onChangeText: setMessage
|
|
1101
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1102
|
+
disabled: busy || message.trim() === "",
|
|
1103
|
+
onPress: () => {
|
|
1104
|
+
setBusy(true);
|
|
1105
|
+
steerStep(workspace, runKey, message.trim(), sessionId, session).then((result) => {
|
|
1106
|
+
if ("error" in result) setError(result.error);
|
|
1107
|
+
else {
|
|
1108
|
+
setMessage("");
|
|
1109
|
+
setError(void 0);
|
|
1110
|
+
}
|
|
1111
|
+
}).catch((cause) => setError(cause instanceof Error ? cause.message : String(cause))).finally(() => setBusy(false));
|
|
1112
|
+
},
|
|
1113
|
+
children: "Send guidance"
|
|
1114
|
+
})]
|
|
1115
|
+
}) : null,
|
|
1116
|
+
error ? /* @__PURE__ */ jsx(Text, {
|
|
1117
|
+
style: styles.error,
|
|
1118
|
+
children: error
|
|
1119
|
+
}) : null
|
|
1120
|
+
]
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
function WorkflowTerminal({ sessionId, workspace, runKey, pane, renderRunnerTerminal }) {
|
|
1124
|
+
const [capabilities, setCapabilities] = useState();
|
|
1125
|
+
const [token, setToken] = useState();
|
|
1126
|
+
const [notice, setNotice] = useState();
|
|
1127
|
+
const [columns, setColumns] = useState("80");
|
|
1128
|
+
const [rows, setRows] = useState("24");
|
|
1129
|
+
const [busy, setBusy] = useState(false);
|
|
1130
|
+
const [lifetime] = useState(() => new AbortController());
|
|
1131
|
+
useEffect(() => () => lifetime.abort(), [lifetime]);
|
|
1132
|
+
useEffect(() => {
|
|
1133
|
+
if (token === void 0) return;
|
|
1134
|
+
return () => {
|
|
1135
|
+
releaseControl(workspace, runKey, token, sessionId, pane).catch(() => {});
|
|
1136
|
+
};
|
|
1137
|
+
}, [
|
|
1138
|
+
workspace,
|
|
1139
|
+
runKey,
|
|
1140
|
+
token,
|
|
1141
|
+
sessionId,
|
|
1142
|
+
pane
|
|
1143
|
+
]);
|
|
1144
|
+
const request = useMemo(() => ({
|
|
1145
|
+
key: `${sessionId}:${workspace}:${runKey}:${pane ?? ""}`,
|
|
1146
|
+
read: finiteScreenReader(async (signal) => {
|
|
1147
|
+
const frame = await readScreen(workspace, runKey, sessionId, pane, signal);
|
|
1148
|
+
if (!signal.aborted && !lifetime.signal.aborted) setCapabilities(frame.capabilities);
|
|
1149
|
+
return frame;
|
|
1150
|
+
}),
|
|
1151
|
+
write: async (text) => {
|
|
1152
|
+
if (token === void 0) throw new Error("Take control before typing into this run.");
|
|
1153
|
+
const result = await sendKeys(workspace, runKey, token, text, sessionId, pane);
|
|
1154
|
+
if (result.error) {
|
|
1155
|
+
if (!lifetime.signal.aborted) {
|
|
1156
|
+
setNotice(result.error);
|
|
1157
|
+
setToken(void 0);
|
|
1158
|
+
}
|
|
1159
|
+
throw new Error(result.error);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}), [
|
|
1163
|
+
sessionId,
|
|
1164
|
+
workspace,
|
|
1165
|
+
runKey,
|
|
1166
|
+
pane,
|
|
1167
|
+
token,
|
|
1168
|
+
lifetime
|
|
1169
|
+
]);
|
|
1170
|
+
const arm = async () => {
|
|
1171
|
+
setBusy(true);
|
|
1172
|
+
try {
|
|
1173
|
+
const grant = await takeControl(workspace, runKey, void 0, sessionId, pane);
|
|
1174
|
+
if (lifetime.signal.aborted) {
|
|
1175
|
+
if (grant.held && grant.token) await releaseControl(workspace, runKey, grant.token, sessionId, pane);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (grant.held && grant.token) {
|
|
1179
|
+
setToken(grant.token);
|
|
1180
|
+
setNotice(void 0);
|
|
1181
|
+
} else setNotice(grant.reason ?? "The keyboard is not available for this run.");
|
|
1182
|
+
} catch (cause) {
|
|
1183
|
+
if (!lifetime.signal.aborted) setNotice(cause instanceof Error ? cause.message : String(cause));
|
|
1184
|
+
} finally {
|
|
1185
|
+
if (!lifetime.signal.aborted) setBusy(false);
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
return /* @__PURE__ */ jsxs(View, {
|
|
1189
|
+
style: styles.transcript,
|
|
1190
|
+
children: [
|
|
1191
|
+
/* @__PURE__ */ jsx(Badge, { children: token ? "keyboard yours" : "watching" }),
|
|
1192
|
+
capabilities?.writable ? /* @__PURE__ */ jsx(Button, {
|
|
1193
|
+
variant: "outline",
|
|
1194
|
+
disabled: busy,
|
|
1195
|
+
onPress: () => {
|
|
1196
|
+
if (token) setToken(void 0);
|
|
1197
|
+
else arm();
|
|
1198
|
+
},
|
|
1199
|
+
children: token ? "Release keyboard" : "Take control"
|
|
1200
|
+
}) : null,
|
|
1201
|
+
/* @__PURE__ */ jsx(View, {
|
|
1202
|
+
style: styles.transcript,
|
|
1203
|
+
children: renderRunnerTerminal?.(request)
|
|
1204
|
+
}),
|
|
1205
|
+
capabilities?.reason ? /* @__PURE__ */ jsx(Text, {
|
|
1206
|
+
style: styles.meta,
|
|
1207
|
+
children: capabilities.reason
|
|
1208
|
+
}) : null,
|
|
1209
|
+
token && capabilities?.resizable ? /* @__PURE__ */ jsxs(View, { children: [
|
|
1210
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1211
|
+
accessibilityLabel: "Terminal columns",
|
|
1212
|
+
keyboardType: "number-pad",
|
|
1213
|
+
value: columns,
|
|
1214
|
+
onChangeText: setColumns
|
|
1215
|
+
}),
|
|
1216
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1217
|
+
accessibilityLabel: "Terminal rows",
|
|
1218
|
+
keyboardType: "number-pad",
|
|
1219
|
+
value: rows,
|
|
1220
|
+
onChangeText: setRows
|
|
1221
|
+
}),
|
|
1222
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1223
|
+
variant: "outline",
|
|
1224
|
+
disabled: !Number.isInteger(Number(columns)) || !Number.isInteger(Number(rows)) || Number(columns) < 1 || Number(rows) < 1,
|
|
1225
|
+
onPress: () => {
|
|
1226
|
+
resizeRun(workspace, runKey, token, Number(columns), Number(rows), sessionId, pane).catch((cause) => {
|
|
1227
|
+
if (!lifetime.signal.aborted) setNotice(cause instanceof Error ? cause.message : String(cause));
|
|
1228
|
+
});
|
|
1229
|
+
},
|
|
1230
|
+
children: "Resize terminal"
|
|
1231
|
+
})
|
|
1232
|
+
] }) : null,
|
|
1233
|
+
notice ? /* @__PURE__ */ jsx(Text, {
|
|
1234
|
+
style: styles.meta,
|
|
1235
|
+
children: notice
|
|
1236
|
+
}) : null
|
|
1237
|
+
]
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
function WorkflowArtifacts(props) {
|
|
1241
|
+
const { run, sessionId, openTransientTab } = props;
|
|
1242
|
+
const [directory, setDirectory] = useState();
|
|
1243
|
+
const [listing, setListing] = useState();
|
|
1244
|
+
const [error, setError] = useState();
|
|
1245
|
+
useEffect(() => {
|
|
1246
|
+
let active = true;
|
|
1247
|
+
fetchArtifacts(run.workspace, run.runKey, sessionId, directory).then((result) => {
|
|
1248
|
+
if (active) {
|
|
1249
|
+
if ("error" in result) setError(result.error);
|
|
1250
|
+
else {
|
|
1251
|
+
setListing(result.artifacts);
|
|
1252
|
+
setError(void 0);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}).catch((cause) => {
|
|
1256
|
+
if (active) setError(cause instanceof Error ? cause.message : String(cause));
|
|
1257
|
+
});
|
|
1258
|
+
return () => {
|
|
1259
|
+
active = false;
|
|
1260
|
+
};
|
|
1261
|
+
}, [
|
|
1262
|
+
run.workspace,
|
|
1263
|
+
run.runKey,
|
|
1264
|
+
sessionId,
|
|
1265
|
+
directory,
|
|
1266
|
+
run.stage
|
|
1267
|
+
]);
|
|
1268
|
+
return /* @__PURE__ */ jsxs(View, { children: [
|
|
1269
|
+
error ? /* @__PURE__ */ jsx(Text, {
|
|
1270
|
+
style: styles.error,
|
|
1271
|
+
children: error
|
|
1272
|
+
}) : null,
|
|
1273
|
+
directory ? /* @__PURE__ */ jsx(Button, {
|
|
1274
|
+
variant: "ghost",
|
|
1275
|
+
onPress: () => setDirectory(directory.includes("/") ? directory.slice(0, directory.lastIndexOf("/")) : void 0),
|
|
1276
|
+
children: "Parent directory"
|
|
1277
|
+
}) : null,
|
|
1278
|
+
listing?.artifacts.map((item) => /* @__PURE__ */ jsx(ListRow, {
|
|
1279
|
+
title: /* @__PURE__ */ jsx(Button, {
|
|
1280
|
+
variant: "ghost",
|
|
1281
|
+
disabled: item.kind !== "directory" && item.state !== "written" && item.state !== "empty",
|
|
1282
|
+
onPress: () => {
|
|
1283
|
+
if (item.kind === "directory") setDirectory(item.path);
|
|
1284
|
+
else {
|
|
1285
|
+
const workspace = run.workspace;
|
|
1286
|
+
const runKey = run.runKey;
|
|
1287
|
+
const path = item.path;
|
|
1288
|
+
openTransientTab({
|
|
1289
|
+
id: `workflows-artifact-${workspace}-${runKey}-${path}`.replace(/[^\w-]+/g, "-"),
|
|
1290
|
+
label: path,
|
|
1291
|
+
panel: (panelProps) => /* @__PURE__ */ jsx(ArtifactPreview, {
|
|
1292
|
+
...panelProps,
|
|
1293
|
+
workspace,
|
|
1294
|
+
runKey,
|
|
1295
|
+
path
|
|
1296
|
+
})
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
children: item.path
|
|
1301
|
+
}),
|
|
1302
|
+
description: `${item.state} · ${item.description}`
|
|
1303
|
+
}, item.path))
|
|
1304
|
+
] });
|
|
1305
|
+
}
|
|
1306
|
+
function ArtifactPreview({ sessionId, workspace, runKey, path, renderFilePreview, renderMediaPreview, nativeSaveFile }) {
|
|
1307
|
+
const [content, setContent] = useState();
|
|
1308
|
+
const [error, setError] = useState();
|
|
1309
|
+
const [raw, setRaw] = useState(false);
|
|
1310
|
+
useEffect(() => {
|
|
1311
|
+
let active = true;
|
|
1312
|
+
fetchArtifact(workspace, runKey, path, sessionId).then((result) => {
|
|
1313
|
+
if (active) {
|
|
1314
|
+
if ("error" in result) setError(result.error);
|
|
1315
|
+
else setContent(result.artifact);
|
|
1316
|
+
}
|
|
1317
|
+
}).catch((cause) => {
|
|
1318
|
+
if (active) setError(cause instanceof Error ? cause.message : String(cause));
|
|
1319
|
+
});
|
|
1320
|
+
return () => {
|
|
1321
|
+
active = false;
|
|
1322
|
+
};
|
|
1323
|
+
}, [
|
|
1324
|
+
workspace,
|
|
1325
|
+
runKey,
|
|
1326
|
+
path,
|
|
1327
|
+
sessionId
|
|
1328
|
+
]);
|
|
1329
|
+
const downloadController = useRef(void 0);
|
|
1330
|
+
const [saving, setSaving] = useState(false);
|
|
1331
|
+
const [saveNote, setSaveNote] = useState();
|
|
1332
|
+
useEffect(() => () => downloadController.current?.abort(), [
|
|
1333
|
+
workspace,
|
|
1334
|
+
runKey,
|
|
1335
|
+
path,
|
|
1336
|
+
sessionId
|
|
1337
|
+
]);
|
|
1338
|
+
const download = async () => {
|
|
1339
|
+
if (!nativeSaveFile || saving) return;
|
|
1340
|
+
const controller = new AbortController();
|
|
1341
|
+
downloadController.current = controller;
|
|
1342
|
+
setSaving(true);
|
|
1343
|
+
try {
|
|
1344
|
+
await nativeSaveFile(await prepareArtifactSave(workspace, runKey, path, sessionId, controller.signal));
|
|
1345
|
+
if (!controller.signal.aborted) setSaveNote("Saved.");
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
if (!controller.signal.aborted) setSaveNote(error instanceof Error ? error.message : String(error));
|
|
1348
|
+
} finally {
|
|
1349
|
+
setSaving(false);
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
return /* @__PURE__ */ jsxs(ScrollView, {
|
|
1353
|
+
contentContainerStyle: styles.body,
|
|
1354
|
+
children: [
|
|
1355
|
+
/* @__PURE__ */ jsx(Text, {
|
|
1356
|
+
style: styles.title,
|
|
1357
|
+
children: path
|
|
1358
|
+
}),
|
|
1359
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1360
|
+
variant: "outline",
|
|
1361
|
+
disabled: !nativeSaveFile || saving,
|
|
1362
|
+
onPress: () => void download(),
|
|
1363
|
+
children: "Download"
|
|
1364
|
+
}),
|
|
1365
|
+
saving ? /* @__PURE__ */ jsx(Button, {
|
|
1366
|
+
variant: "ghost",
|
|
1367
|
+
onPress: () => downloadController.current?.abort(),
|
|
1368
|
+
children: "Cancel download"
|
|
1369
|
+
}) : null,
|
|
1370
|
+
saveNote ? /* @__PURE__ */ jsx(Text, {
|
|
1371
|
+
style: styles.meta,
|
|
1372
|
+
children: saveNote
|
|
1373
|
+
}) : null,
|
|
1374
|
+
error ? /* @__PURE__ */ jsx(Text, {
|
|
1375
|
+
style: styles.error,
|
|
1376
|
+
children: error
|
|
1377
|
+
}) : null,
|
|
1378
|
+
content?.text !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1379
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1380
|
+
variant: "outline",
|
|
1381
|
+
onPress: () => setRaw((value) => !value),
|
|
1382
|
+
children: raw ? "Preview" : "Raw source"
|
|
1383
|
+
}),
|
|
1384
|
+
!raw && /\.(md|markdown)$/i.test(path) ? /* @__PURE__ */ jsx(Markdown, { text: content.text }) : !raw && /\.(html|htm)$/i.test(path) ? renderFilePreview?.({
|
|
1385
|
+
key: `${sessionId}:${workspace}:${runKey}:${path}`,
|
|
1386
|
+
html: content.text,
|
|
1387
|
+
title: path
|
|
1388
|
+
}) : !raw && /\.(csv|tsv)$/i.test(path) ? /* @__PURE__ */ jsx(DelimitedArtifact, {
|
|
1389
|
+
text: content.text,
|
|
1390
|
+
delimiter: /\.csv$/i.test(path) ? "," : " "
|
|
1391
|
+
}) : /* @__PURE__ */ jsx(CodeBlock, { text: !raw && /\.json$/i.test(path) ? prettyJson(content.text) : content.text }),
|
|
1392
|
+
content.truncated ? /* @__PURE__ */ jsx(Text, {
|
|
1393
|
+
style: styles.meta,
|
|
1394
|
+
children: "This preview was truncated."
|
|
1395
|
+
}) : null
|
|
1396
|
+
] }) : content ? /* @__PURE__ */ jsx(BinaryArtifactPreview, {
|
|
1397
|
+
sessionId,
|
|
1398
|
+
workspace,
|
|
1399
|
+
runKey,
|
|
1400
|
+
path,
|
|
1401
|
+
renderMediaPreview
|
|
1402
|
+
}) : /* @__PURE__ */ jsx(Text, {
|
|
1403
|
+
style: styles.meta,
|
|
1404
|
+
children: "Loading preview…"
|
|
1405
|
+
})
|
|
1406
|
+
]
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
function BinaryArtifactPreview({ sessionId, workspace, runKey, path, renderMediaPreview }) {
|
|
1410
|
+
const requestKey = JSON.stringify([
|
|
1411
|
+
sessionId,
|
|
1412
|
+
workspace,
|
|
1413
|
+
runKey,
|
|
1414
|
+
path
|
|
1415
|
+
]);
|
|
1416
|
+
const [loaded, setLoaded] = useState();
|
|
1417
|
+
const media = loaded?.key === requestKey ? loaded.media : void 0;
|
|
1418
|
+
const error = loaded?.key === requestKey ? loaded.error : void 0;
|
|
1419
|
+
useEffect(() => {
|
|
1420
|
+
const controller = new AbortController();
|
|
1421
|
+
fetchArtifactMedia(workspace, runKey, path, sessionId, controller.signal).then((value) => {
|
|
1422
|
+
if (!controller.signal.aborted) setLoaded({
|
|
1423
|
+
key: requestKey,
|
|
1424
|
+
media: value
|
|
1425
|
+
});
|
|
1426
|
+
}).catch((cause) => {
|
|
1427
|
+
if (!controller.signal.aborted) setLoaded({
|
|
1428
|
+
key: requestKey,
|
|
1429
|
+
error: cause instanceof Error ? cause.message : String(cause)
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
return () => controller.abort();
|
|
1433
|
+
}, [
|
|
1434
|
+
workspace,
|
|
1435
|
+
runKey,
|
|
1436
|
+
path,
|
|
1437
|
+
sessionId,
|
|
1438
|
+
requestKey
|
|
1439
|
+
]);
|
|
1440
|
+
return media ? renderMediaPreview?.({
|
|
1441
|
+
key: `${sessionId}:${workspace}:${runKey}:${path}`,
|
|
1442
|
+
...media,
|
|
1443
|
+
title: path,
|
|
1444
|
+
path
|
|
1445
|
+
}) : /* @__PURE__ */ jsx(Text, {
|
|
1446
|
+
style: styles.meta,
|
|
1447
|
+
children: error ?? "Loading preview…"
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
function prettyJson(text) {
|
|
1451
|
+
try {
|
|
1452
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
1453
|
+
} catch {
|
|
1454
|
+
return text;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
function DelimitedArtifact({ text, delimiter }) {
|
|
1458
|
+
const rows = [];
|
|
1459
|
+
let row = [];
|
|
1460
|
+
let cell = "";
|
|
1461
|
+
let quoted = false;
|
|
1462
|
+
for (let index = 0; index <= text.length && rows.length < 500; index += 1) {
|
|
1463
|
+
const character = text[index] ?? "\n";
|
|
1464
|
+
if (quoted) {
|
|
1465
|
+
if (character === "\"" && text[index + 1] === "\"") {
|
|
1466
|
+
cell += "\"";
|
|
1467
|
+
index += 1;
|
|
1468
|
+
} else if (character === "\"") quoted = false;
|
|
1469
|
+
else cell += character;
|
|
1470
|
+
} else if (character === "\"" && cell === "") quoted = true;
|
|
1471
|
+
else if (character === delimiter) {
|
|
1472
|
+
row.push(cell);
|
|
1473
|
+
cell = "";
|
|
1474
|
+
} else if (character === "\n") {
|
|
1475
|
+
row.push(cell.replace(/\r$/, ""));
|
|
1476
|
+
rows.push(row);
|
|
1477
|
+
row = [];
|
|
1478
|
+
cell = "";
|
|
1479
|
+
} else cell += character;
|
|
1480
|
+
}
|
|
1481
|
+
return /* @__PURE__ */ jsx(ScrollView, {
|
|
1482
|
+
horizontal: true,
|
|
1483
|
+
children: /* @__PURE__ */ jsx(View, { children: rows.map((cells, index) => /* @__PURE__ */ jsx(View, {
|
|
1484
|
+
style: styles.tableRow,
|
|
1485
|
+
children: cells.map((value, column) => /* @__PURE__ */ jsx(Text, {
|
|
1486
|
+
style: styles.tableCell,
|
|
1487
|
+
children: value
|
|
1488
|
+
}, column))
|
|
1489
|
+
}, index)) })
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
const styles = StyleSheet.create((theme) => ({
|
|
1493
|
+
screen: {
|
|
1494
|
+
flex: 1,
|
|
1495
|
+
backgroundColor: theme.colors.bg,
|
|
1496
|
+
padding: theme.spacing(3),
|
|
1497
|
+
uni__dependencies: [0]
|
|
1498
|
+
},
|
|
1499
|
+
transcript: { flex: 1 },
|
|
1500
|
+
tableRow: { flexDirection: "row" },
|
|
1501
|
+
tableCell: {
|
|
1502
|
+
color: theme.colors.text,
|
|
1503
|
+
fontSize: theme.fontSize.sm,
|
|
1504
|
+
padding: theme.spacing(2),
|
|
1505
|
+
width: theme.spacing(30),
|
|
1506
|
+
borderWidth: 1,
|
|
1507
|
+
borderColor: theme.colors.border,
|
|
1508
|
+
uni__dependencies: [0]
|
|
1509
|
+
},
|
|
1510
|
+
body: {
|
|
1511
|
+
padding: theme.spacing(3),
|
|
1512
|
+
gap: theme.spacing(2),
|
|
1513
|
+
uni__dependencies: [0]
|
|
1514
|
+
},
|
|
1515
|
+
title: {
|
|
1516
|
+
color: theme.colors.hi,
|
|
1517
|
+
fontSize: theme.fontSize.base,
|
|
1518
|
+
fontWeight: "600",
|
|
1519
|
+
uni__dependencies: [0]
|
|
1520
|
+
},
|
|
1521
|
+
meta: {
|
|
1522
|
+
color: theme.colors.dim,
|
|
1523
|
+
fontSize: theme.fontSize.sm,
|
|
1524
|
+
uni__dependencies: [0]
|
|
1525
|
+
},
|
|
1526
|
+
error: {
|
|
1527
|
+
color: theme.colors.red,
|
|
1528
|
+
fontSize: theme.fontSize.sm,
|
|
1529
|
+
uni__dependencies: [0]
|
|
1530
|
+
}
|
|
1531
|
+
}));
|
|
1532
|
+
//#endregion
|
|
1533
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/WorkflowLauncher.overlay.native.tsx
|
|
1534
|
+
var WorkflowLauncher_overlay_native_default = defineFill({ component: WorkflowLauncher });
|
|
1535
|
+
//#endregion
|
|
1536
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/_components/WorkflowsActivitySection.native.tsx
|
|
1537
|
+
function WorkflowsActivitySection({ sessionId, openSession, openTransientTab }) {
|
|
1538
|
+
const state = useWorkflows(sessionId);
|
|
1539
|
+
const [now, setNow] = useState(() => Date.now());
|
|
1540
|
+
const [folded, setFolded] = useState({});
|
|
1541
|
+
useEffect(() => {
|
|
1542
|
+
const timer = setInterval(() => setNow(Date.now()), 1e4);
|
|
1543
|
+
return () => clearInterval(timer);
|
|
1544
|
+
}, []);
|
|
1545
|
+
const groups = workflowActivityGroups(workflowActivityRows(state.runs, now, sessionId));
|
|
1546
|
+
return /* @__PURE__ */ jsxs(View, { children: [groups.map((group) => {
|
|
1547
|
+
const open = folded[group.name] ?? group.openByDefault;
|
|
1548
|
+
return /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
1549
|
+
variant: "ghost",
|
|
1550
|
+
accessibilityState: { expanded: open },
|
|
1551
|
+
onPress: () => setFolded((current) => ({
|
|
1552
|
+
...current,
|
|
1553
|
+
[group.name]: !open
|
|
1554
|
+
})),
|
|
1555
|
+
children: [
|
|
1556
|
+
group.name,
|
|
1557
|
+
" · ",
|
|
1558
|
+
group.rows.length
|
|
1559
|
+
]
|
|
1560
|
+
}), open ? group.rows.map((row) => /* @__PURE__ */ jsx(ListRow, {
|
|
1561
|
+
leading: /* @__PURE__ */ jsx(Dot, { tone: row.tone === "failed" ? "red" : row.tone === "running" || row.tone === "paused" ? "yellow" : row.tone === "done" ? "green" : "muted" }),
|
|
1562
|
+
title: /* @__PURE__ */ jsx(Button, {
|
|
1563
|
+
variant: "ghost",
|
|
1564
|
+
testID: `activity-workflow-${row.runKey}`,
|
|
1565
|
+
onPress: () => {
|
|
1566
|
+
if (row.ownerSessionId && openSession) {
|
|
1567
|
+
openSession(row.ownerSessionId);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const run = state.runs.find((item) => `${item.workspace}/${item.runKey}` === row.identity);
|
|
1571
|
+
if (sessionId !== null) focusRun(sessionId, row.identity);
|
|
1572
|
+
if (run) openTransientTab(workflowTab(run));
|
|
1573
|
+
},
|
|
1574
|
+
children: row.name
|
|
1575
|
+
}),
|
|
1576
|
+
description: `${row.detail} · ${row.elapsed}`
|
|
1577
|
+
}, row.identity)) : null] }, group.name);
|
|
1578
|
+
}), groups.length === 0 ? /* @__PURE__ */ jsx(Button, {
|
|
1579
|
+
variant: "ghost",
|
|
1580
|
+
testID: "activity-workflow-launch",
|
|
1581
|
+
disabled: sessionId === null,
|
|
1582
|
+
onPress: () => {
|
|
1583
|
+
if (sessionId !== null) openCatalog(sessionId);
|
|
1584
|
+
},
|
|
1585
|
+
children: "Launch a workflow"
|
|
1586
|
+
}) : null] });
|
|
1587
|
+
}
|
|
1588
|
+
//#endregion
|
|
1589
|
+
//#region src/extensions/workspaces/sessions/(frontend)/fill/Workflows.activity.workflows.native.tsx
|
|
1590
|
+
var Workflows_activity_workflows_native_default = defineFill({ component: WorkflowsActivitySection });
|
|
1591
|
+
//#endregion
|
|
1592
|
+
//#region generated/native.ts
|
|
1593
|
+
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
1594
|
+
...identity,
|
|
1595
|
+
...value
|
|
1596
|
+
};
|
|
1597
|
+
const nativePlugin = defineNativePlugin({
|
|
1598
|
+
id: "workflows",
|
|
1599
|
+
session: {
|
|
1600
|
+
get activityGroups() {
|
|
1601
|
+
return [via({ name: "workflows" }, workflows_native_default)];
|
|
1602
|
+
},
|
|
1603
|
+
get channels() {
|
|
1604
|
+
return [via({ channel: "workflow_catalog" }, workflow_catalog_native_default), via({ channel: "workflow_runs" }, workflow_runs_native_default)];
|
|
1605
|
+
},
|
|
1606
|
+
get fills() {
|
|
1607
|
+
return [via({
|
|
1608
|
+
slot: "overlay",
|
|
1609
|
+
id: "workflow-launcher"
|
|
1610
|
+
}, WorkflowLauncher_overlay_native_default), via({
|
|
1611
|
+
slot: "activity.workflows",
|
|
1612
|
+
id: "workflows"
|
|
1613
|
+
}, Workflows_activity_workflows_native_default)];
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
//#endregion
|
|
1618
|
+
export { nativePlugin };
|
|
1619
|
+
|
|
1620
|
+
//# sourceMappingURL=native.mjs.map
|