@floless/app 0.14.0 → 0.16.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/dist/floless-server.cjs +103 -5
- package/dist/skills/floless-app-workflows/SKILL.md +20 -0
- package/dist/web/app.css +39 -5
- package/dist/web/app.js +16 -3
- package/dist/web/aware.js +243 -9
- package/dist/web/index.html +1 -0
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -50969,6 +50969,13 @@ function readApp(id) {
|
|
|
50969
50969
|
compilerVersion: typeof lockDoc["compiler-version"] === "string" ? lockDoc["compiler-version"] : null,
|
|
50970
50970
|
agentPins
|
|
50971
50971
|
};
|
|
50972
|
+
const skillCache = /* @__PURE__ */ new Map();
|
|
50973
|
+
const nodeSkill = (agent, command) => {
|
|
50974
|
+
if (!agent || !command) return null;
|
|
50975
|
+
const key = JSON.stringify([agent, command]);
|
|
50976
|
+
if (!skillCache.has(key)) skillCache.set(key, readNodeSkill(agent, command));
|
|
50977
|
+
return skillCache.get(key) ?? null;
|
|
50978
|
+
};
|
|
50972
50979
|
const srcNodes = Array.isArray(src.nodes) ? src.nodes : [];
|
|
50973
50980
|
const nodes = srcNodes.map((n) => {
|
|
50974
50981
|
const nid = String(n.id);
|
|
@@ -50984,7 +50991,8 @@ function readApp(id) {
|
|
|
50984
50991
|
mode: lockMode === "write" || lockMode === "read" ? lockMode : "unknown",
|
|
50985
50992
|
inputs: asRecord(locked?.inputs),
|
|
50986
50993
|
config: asRecord(n.config),
|
|
50987
|
-
notes: notes2
|
|
50994
|
+
notes: notes2,
|
|
50995
|
+
skill: nodeSkill(n.agent != null ? String(n.agent) : null, command)
|
|
50988
50996
|
};
|
|
50989
50997
|
});
|
|
50990
50998
|
const layout = src.layout === "dag" ? "dag" : "linear";
|
|
@@ -51038,7 +51046,7 @@ function sourceNodeId(nodes, connections) {
|
|
|
51038
51046
|
return nodes.find((n) => !hasIncoming.has(n.id))?.id ?? nodes[0].id;
|
|
51039
51047
|
}
|
|
51040
51048
|
function readCommandSpec(agent, command, agentsDir = AGENTS_DIR) {
|
|
51041
|
-
const safe = (n) =>
|
|
51049
|
+
const safe = (n) => !/[/\\\0]/.test(n) && !n.includes("..");
|
|
51042
51050
|
if (!safe(agent) || !safe(command)) return null;
|
|
51043
51051
|
const manifestPath = (0, import_node_path2.join)(agentsDir, agent, "manifest.yaml");
|
|
51044
51052
|
if (!(0, import_node_fs3.existsSync)(manifestPath)) return null;
|
|
@@ -51065,6 +51073,55 @@ function readCommandSpec(agent, command, agentsDir = AGENTS_DIR) {
|
|
|
51065
51073
|
});
|
|
51066
51074
|
return { lifecycle, streaming, inputs };
|
|
51067
51075
|
}
|
|
51076
|
+
function firstParagraph(s) {
|
|
51077
|
+
if (typeof s !== "string") return "";
|
|
51078
|
+
return (s.trim().split(/\n\s*\n/)[0] ?? "").replace(/\s+/g, " ").trim();
|
|
51079
|
+
}
|
|
51080
|
+
function parseManifestInputs(raw) {
|
|
51081
|
+
return Object.entries(asRecord(raw)).map(([name, spec]) => {
|
|
51082
|
+
const s = typeof spec === "string" ? { type: spec } : asRecord(spec);
|
|
51083
|
+
const input = {
|
|
51084
|
+
name,
|
|
51085
|
+
type: typeof s.type === "string" ? s.type : "string",
|
|
51086
|
+
default: "default" in s ? s.default : null,
|
|
51087
|
+
description: typeof s.description === "string" ? s.description : ""
|
|
51088
|
+
};
|
|
51089
|
+
if (Array.isArray(s.values)) input.values = s.values.map(String);
|
|
51090
|
+
return input;
|
|
51091
|
+
});
|
|
51092
|
+
}
|
|
51093
|
+
function readNodeSkill(agent, command, agentsDir = AGENTS_DIR) {
|
|
51094
|
+
const safe = (n) => !/[/\\\0]/.test(n) && !n.includes("..");
|
|
51095
|
+
if (!safe(agent) || !safe(command)) return null;
|
|
51096
|
+
const manifestPath = (0, import_node_path2.join)(agentsDir, agent, "manifest.yaml");
|
|
51097
|
+
if (!(0, import_node_fs3.existsSync)(manifestPath)) return null;
|
|
51098
|
+
let doc;
|
|
51099
|
+
try {
|
|
51100
|
+
doc = asRecord((0, import_yaml.parse)((0, import_node_fs3.readFileSync)(manifestPath, "utf8")));
|
|
51101
|
+
} catch {
|
|
51102
|
+
return null;
|
|
51103
|
+
}
|
|
51104
|
+
const cmd = asRecord(asRecord(doc.commands)[command]);
|
|
51105
|
+
if (Object.keys(cmd).length === 0) return null;
|
|
51106
|
+
const outRec = asRecord(cmd.outputs);
|
|
51107
|
+
const outputs = typeof outRec.type === "string" ? {
|
|
51108
|
+
type: outRec.type,
|
|
51109
|
+
schema: Object.fromEntries(
|
|
51110
|
+
Object.entries(asRecord(outRec.schema)).map(([k, v]) => [k, String(v)])
|
|
51111
|
+
)
|
|
51112
|
+
} : null;
|
|
51113
|
+
return {
|
|
51114
|
+
agent,
|
|
51115
|
+
agentDisplayName: typeof doc["display-name"] === "string" ? doc["display-name"] : agent,
|
|
51116
|
+
agentDescription: firstParagraph(doc.description),
|
|
51117
|
+
homepage: typeof doc.homepage === "string" ? doc.homepage : null,
|
|
51118
|
+
capabilities: Object.keys(asRecord(doc.requires)),
|
|
51119
|
+
command,
|
|
51120
|
+
commandDescription: firstParagraph(cmd.description),
|
|
51121
|
+
inputs: parseManifestInputs(cmd.inputs),
|
|
51122
|
+
outputs
|
|
51123
|
+
};
|
|
51124
|
+
}
|
|
51068
51125
|
function detectTriggerSource(nodes, connections, lookup) {
|
|
51069
51126
|
const srcId = sourceNodeId(nodes, connections);
|
|
51070
51127
|
if (!srcId) return null;
|
|
@@ -51096,7 +51153,7 @@ function listIntegrations() {
|
|
|
51096
51153
|
if (!(0, import_node_fs3.existsSync)(AGENTS_DIR)) return [];
|
|
51097
51154
|
const out = [];
|
|
51098
51155
|
const seen = /* @__PURE__ */ new Set();
|
|
51099
|
-
const safe = (n) =>
|
|
51156
|
+
const safe = (n) => !/[/\\\0]/.test(n) && !n.includes("..");
|
|
51100
51157
|
for (const agentId of (0, import_node_fs3.readdirSync)(AGENTS_DIR)) {
|
|
51101
51158
|
if (!safe(agentId)) continue;
|
|
51102
51159
|
const manifestPath = (0, import_node_path2.join)(AGENTS_DIR, agentId, "manifest.yaml");
|
|
@@ -52609,7 +52666,7 @@ function appVersion() {
|
|
|
52609
52666
|
return resolveVersion({
|
|
52610
52667
|
isSea: isSea2(),
|
|
52611
52668
|
sqVersionXml: readSqVersionXml(),
|
|
52612
|
-
define: true ? "0.
|
|
52669
|
+
define: true ? "0.16.0" : void 0,
|
|
52613
52670
|
pkgVersion: readPkgVersion()
|
|
52614
52671
|
});
|
|
52615
52672
|
}
|
|
@@ -52619,7 +52676,7 @@ function resolveChannel(s) {
|
|
|
52619
52676
|
return "dev";
|
|
52620
52677
|
}
|
|
52621
52678
|
function appChannel() {
|
|
52622
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
52679
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.16.0" : void 0 });
|
|
52623
52680
|
}
|
|
52624
52681
|
|
|
52625
52682
|
// oauth-presets.ts
|
|
@@ -53211,6 +53268,9 @@ function hostRunBlocked(agent, agentsDir) {
|
|
|
53211
53268
|
if (!hostWatcherActive()) return null;
|
|
53212
53269
|
return "a host watcher is live for this host \u2014 stop it before running manually (one live model = one consumer)";
|
|
53213
53270
|
}
|
|
53271
|
+
function foregroundSessionId(appId) {
|
|
53272
|
+
return `__run__:${appId}`;
|
|
53273
|
+
}
|
|
53214
53274
|
function startSession(spec, deps = defaultDeps) {
|
|
53215
53275
|
if (spec.hostBacked && hostWatcherActive(spec.id)) {
|
|
53216
53276
|
return { ok: false, reason: "another host watcher is live \u2014 stop it first (one live model = one watcher)" };
|
|
@@ -53883,6 +53943,8 @@ function isGatedAwareRoute(url, method) {
|
|
|
53883
53943
|
if (path === "/api/routines" || path.startsWith("/api/routines/")) {
|
|
53884
53944
|
return m === "POST" || m === "PATCH";
|
|
53885
53945
|
}
|
|
53946
|
+
if (path.startsWith("/api/trigger-run/")) return false;
|
|
53947
|
+
if (path === "/api/trigger-run") return m === "POST";
|
|
53886
53948
|
return AWARE_ROUTES.some((p) => path.startsWith(p));
|
|
53887
53949
|
}
|
|
53888
53950
|
|
|
@@ -57363,6 +57425,42 @@ async function startServer() {
|
|
|
57363
57425
|
const running = cancelActiveRun();
|
|
57364
57426
|
return { ok: true, running };
|
|
57365
57427
|
});
|
|
57428
|
+
app.post(
|
|
57429
|
+
"/api/trigger-run",
|
|
57430
|
+
async (req, reply) => {
|
|
57431
|
+
const { id, inputs } = req.body ?? {};
|
|
57432
|
+
if (!id) return reply.status(400).send({ ok: false, error: "id required" });
|
|
57433
|
+
let appSpec;
|
|
57434
|
+
try {
|
|
57435
|
+
appSpec = readApp(id);
|
|
57436
|
+
} catch (err) {
|
|
57437
|
+
if (err instanceof AppNotFoundError) return reply.status(404).send({ ok: false, error: "app not found" });
|
|
57438
|
+
throw err;
|
|
57439
|
+
}
|
|
57440
|
+
if (!appSpec.triggerSource) {
|
|
57441
|
+
return reply.status(400).send({ ok: false, error: "not a streaming workflow \u2014 use \u25B6 Run for a one-shot" });
|
|
57442
|
+
}
|
|
57443
|
+
if (!appSpec.runnable) {
|
|
57444
|
+
return reply.send({ ok: false, error: "needs Compile \u2014 Run is gated on a fresh lock", blocked: true });
|
|
57445
|
+
}
|
|
57446
|
+
const sessionId = foregroundSessionId(id);
|
|
57447
|
+
const res = startSession({
|
|
57448
|
+
id: sessionId,
|
|
57449
|
+
workflow: id,
|
|
57450
|
+
sourceNodeId: appSpec.triggerSource.nodeId,
|
|
57451
|
+
hostBacked: isHostBacked(appSpec.triggerSource.agent),
|
|
57452
|
+
inputs
|
|
57453
|
+
});
|
|
57454
|
+
if (!res.ok) return reply.send({ ok: false, error: res.reason, blocked: true });
|
|
57455
|
+
return { ok: true, sessionId };
|
|
57456
|
+
}
|
|
57457
|
+
);
|
|
57458
|
+
app.post("/api/trigger-run/:id/stop", async (req, reply) => {
|
|
57459
|
+
const id = req.params.id;
|
|
57460
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id)) return reply.status(400).send({ ok: false, error: "invalid app id" });
|
|
57461
|
+
await stopSession(foregroundSessionId(id));
|
|
57462
|
+
return { ok: true };
|
|
57463
|
+
});
|
|
57366
57464
|
app.get("/api/latest-run/:id", async (req, reply) => {
|
|
57367
57465
|
const id = req.params.id;
|
|
57368
57466
|
if (!/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
@@ -258,6 +258,26 @@ manifest edit + recompile, no engine. See the design doc for the Stage-1 plan.
|
|
|
258
258
|
it renders the returned `html` in the HTML Viewer and caches it; otherwise it fills the Execution
|
|
259
259
|
trace. The secondary **`Simulate`** header button is `simulate:true` (every node stubbed from its
|
|
260
260
|
output-schema, no host) — a composition check.
|
|
261
|
+
- **▶ Run on a STREAMING/watch app = a LIVE FOREGROUND session** (not the finite run). When the
|
|
262
|
+
open app's source node is a `lifecycle:start` watch (`app.triggerSource != null`), the finite
|
|
263
|
+
`/api/run` never completes (a stream doesn't end → 60 s timeout). Instead Run starts an
|
|
264
|
+
**ephemeral** trigger session — `POST /api/trigger-run {id,inputs}` / `POST
|
|
265
|
+
/api/trigger-run/:id/stop` — that re-renders the report in the HTML Viewer on **every** fired
|
|
266
|
+
event (e.g. each Tekla selection change) until Stop, reusing the background-routine machinery
|
|
267
|
+
(`server/trigger-sessions.ts`: long-lived `aware app run` OUTSIDE the run lock, host-exclusivity
|
|
268
|
+
guard, per-event report HTML on the `trigger-report` SSE). The session id is namespaced
|
|
269
|
+
`foregroundSessionId()` → `__run__:<app>` so it can't collide with a routine id in the shared SSE
|
|
270
|
+
id space; `route-gate` gates the spawning POST and exempts the `/stop` teardown. Per-event repaint
|
|
271
|
+
reuses the existing `applyTriggerReport`→`paintReport` path (gated on `workflow === currentId`) —
|
|
272
|
+
**zero** new render plumbing. The watch node's `once:true` (AWARE `tekla.watch` input) makes the
|
|
273
|
+
same session fire once then exit ("Run complete"); `once:false` (default) streams continuously.
|
|
274
|
+
**Two UI gotchas a real Playwright pass caught (inference missed both):** (1) the always-on header
|
|
275
|
+
**■ Stop run is OCCLUDED by the report-modal backdrop**, so a live session shown in the Viewer
|
|
276
|
+
needs its OWN reachable Stop — the in-modal `#report-stop` (`web/index.html` action row,
|
|
277
|
+
warn-toned); (2) `paintGate()` recomputes Run-disabled / Stop-hidden on **every** gate repaint
|
|
278
|
+
(an fs-change → `loadApp` mid-session repaints), so it MUST include the live-session flag
|
|
279
|
+
(`foregroundTrigger`) exactly like `syncRunControls`, or a repaint re-arms Run and hides Stop
|
|
280
|
+
while the watcher is still live.
|
|
261
281
|
- **Per-node run status on the canvas + a Stop button.** During a run each node card paints
|
|
262
282
|
running → ✓ done / ✗ failed from the trace (`pushTrace`), and the report-run overlay shows a
|
|
263
283
|
**Stop** button. See `references/dev-server-and-run-trace.md` for the trace event kinds, the live
|
package/dist/web/app.css
CHANGED
|
@@ -46,7 +46,10 @@
|
|
|
46
46
|
grid-template-rows: 60px 1fr 44px;
|
|
47
47
|
--left-width: 340px;
|
|
48
48
|
--right-width: 420px;
|
|
49
|
-
|
|
49
|
+
/* minmax(0, 1fr) (not the default minmax(auto, 1fr)) lets the center column
|
|
50
|
+
shrink below its content's min-size, so a wide center never blows the grid
|
|
51
|
+
past the viewport. See min-width:0 on the three areas below. */
|
|
52
|
+
grid-template-columns: var(--left-width) minmax(0, 1fr) var(--right-width);
|
|
50
53
|
grid-template-areas:
|
|
51
54
|
"header header header"
|
|
52
55
|
"chat canvas inspect"
|
|
@@ -104,8 +107,15 @@
|
|
|
104
107
|
justify-content: space-between;
|
|
105
108
|
padding: 0 18px;
|
|
106
109
|
border-bottom: 1px solid var(--border);
|
|
110
|
+
/* Let the header's flex children shrink rather than force the whole grid wider
|
|
111
|
+
than the viewport (the old cause of a page-wide horizontal scrollbar): the
|
|
112
|
+
workflow picker truncates first (down to its 220px floor), the run spine
|
|
113
|
+
compresses only slightly after that, and a scrollbar — never off-screen
|
|
114
|
+
clipping of the run buttons — is the final fallback at tiny widths (#53). */
|
|
115
|
+
min-width: 0;
|
|
107
116
|
}
|
|
108
|
-
|
|
117
|
+
/* Brand + view-toggle stay full size; .controls is the shrink sink instead. */
|
|
118
|
+
.header-left { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
|
109
119
|
.brand { display: flex; align-items: center; gap: 12px; }
|
|
110
120
|
.brand .mark {
|
|
111
121
|
display: inline-flex;
|
|
@@ -119,7 +129,7 @@
|
|
|
119
129
|
.brand .mark .mark-node { fill: var(--bg); }
|
|
120
130
|
.brand .name { font-size: 15px; font-weight: 700; letter-spacing: 0.08em; }
|
|
121
131
|
.brand .tag { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.12em; }
|
|
122
|
-
.controls { display: flex; align-items: center; gap: 10px; }
|
|
132
|
+
.controls { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
123
133
|
/* Vertical divider separating "which workflow + reference" from the run spine
|
|
124
134
|
(state → Compile → Simulate → Run). Uses the existing border token. */
|
|
125
135
|
.ctl-sep { width: 1px; height: 20px; background: var(--border-strong); flex: none; }
|
|
@@ -236,6 +246,7 @@
|
|
|
236
246
|
display: flex;
|
|
237
247
|
flex-direction: column;
|
|
238
248
|
overflow: hidden;
|
|
249
|
+
min-width: 0;
|
|
239
250
|
position: relative;
|
|
240
251
|
}
|
|
241
252
|
.host-banner {
|
|
@@ -326,6 +337,7 @@
|
|
|
326
337
|
display: flex;
|
|
327
338
|
flex-direction: column;
|
|
328
339
|
overflow: hidden;
|
|
340
|
+
min-width: 0;
|
|
329
341
|
position: relative;
|
|
330
342
|
}
|
|
331
343
|
/* While middle-mouse panning, force the grab cursor over the whole canvas. */
|
|
@@ -738,6 +750,7 @@
|
|
|
738
750
|
display: flex;
|
|
739
751
|
flex-direction: column;
|
|
740
752
|
overflow: hidden;
|
|
753
|
+
min-width: 0;
|
|
741
754
|
position: relative;
|
|
742
755
|
}
|
|
743
756
|
.tabs {
|
|
@@ -1416,10 +1429,13 @@
|
|
|
1416
1429
|
#rtn-delete-confirm:hover { color: var(--err); border-color: var(--err); background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
1417
1430
|
|
|
1418
1431
|
/* ========== WORKFLOW COMBOBOX (searchable, provider-grouped) ========== */
|
|
1419
|
-
|
|
1432
|
+
/* Shrinkable in the header flex row: prefers ~300px, but flex-shrink lets it
|
|
1433
|
+
give space back down to a 220px floor (min-width) when the header is tight,
|
|
1434
|
+
so the run spine never overflows the viewport. The name ellipsizes within. */
|
|
1435
|
+
.wf-combo { position: relative; display: inline-flex; flex: 0 1 300px; min-width: 220px; }
|
|
1420
1436
|
.wf-trigger {
|
|
1421
1437
|
display: inline-flex; align-items: center; gap: 8px;
|
|
1422
|
-
min-width:
|
|
1438
|
+
min-width: 0; width: 100%;
|
|
1423
1439
|
background: var(--surface-2); color: var(--text);
|
|
1424
1440
|
border: 1px solid var(--border-strong); border-radius: 4px;
|
|
1425
1441
|
padding: 7px 12px; font-family: var(--ui); font-size: 12px; cursor: pointer;
|
|
@@ -2041,6 +2057,20 @@ body {
|
|
|
2041
2057
|
.kv td { font-family: var(--mono); color: var(--text); word-break: break-word; }
|
|
2042
2058
|
.kv tr + tr th, .kv tr + tr td { border-top: 1px solid var(--border); }
|
|
2043
2059
|
|
|
2060
|
+
/* Skill tab (Inspect): capability/mode pills mirror the notes-strip .note-kind
|
|
2061
|
+
pill, and the agent docs link uses the accent — existing tokens only, no new
|
|
2062
|
+
palette (issue #54). */
|
|
2063
|
+
.inspect-content .cap-badge {
|
|
2064
|
+
display: inline-block; font-size: 9px; text-transform: uppercase; letter-spacing: 0.06em;
|
|
2065
|
+
color: var(--info); border: 1px solid color-mix(in srgb, var(--info) 35%, transparent);
|
|
2066
|
+
border-radius: 3px; padding: 0 5px; margin: 0 2px; vertical-align: 1px;
|
|
2067
|
+
}
|
|
2068
|
+
.inspect-content .cap-badge.write { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
|
|
2069
|
+
/* Breathing room above the docs link — it follows a schema table (no margin-bottom). */
|
|
2070
|
+
.inspect-content .skill-docs-row { margin-top: 8px; }
|
|
2071
|
+
.inspect-content .skill-docs { color: var(--accent); text-decoration: none; }
|
|
2072
|
+
.inspect-content .skill-docs:hover { text-decoration: underline; }
|
|
2073
|
+
|
|
2044
2074
|
/* ── HTML Viewer (report) modal ─────────────────────────────────────────────── */
|
|
2045
2075
|
.modal.report-viewer {
|
|
2046
2076
|
width: 1180px; max-width: 94vw; height: 86vh;
|
|
@@ -2056,6 +2086,10 @@ body {
|
|
|
2056
2086
|
.report-actions button { padding: 6px 12px; }
|
|
2057
2087
|
.report-actions button.ghost { background: var(--surface-3); color: var(--text-muted); }
|
|
2058
2088
|
.report-actions button.ghost:hover { color: var(--text); border-color: var(--accent-dim); }
|
|
2089
|
+
/* The in-modal live-session Stop reads as a stop (warn-toned), matching the header
|
|
2090
|
+
■ Stop run — not a neutral ghost link like Share/Open. Existing --warn token only. */
|
|
2091
|
+
.report-actions button.report-stop { background: transparent; color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent); font-weight: 600; letter-spacing: 0.04em; }
|
|
2092
|
+
.report-actions button.report-stop:hover { background: color-mix(in srgb, var(--warn) 16%, transparent); border-color: var(--warn); color: var(--text); }
|
|
2059
2093
|
#report-close { font-size: 18px; line-height: 1; padding: 4px 10px; }
|
|
2060
2094
|
.report-stage { position: relative; flex: 1; background: #0f172a; }
|
|
2061
2095
|
#report-frame { width: 100%; height: 100%; border: 0; display: block; background: #0f172a; }
|
package/dist/web/app.js
CHANGED
|
@@ -354,7 +354,7 @@ function cardEl(id, ports) {
|
|
|
354
354
|
`;
|
|
355
355
|
div.onclick = (e) => {
|
|
356
356
|
if (e.target.closest('.fav-btn')) return;
|
|
357
|
-
selectAgent(id);
|
|
357
|
+
selectAgent(id, { reveal: true }); // clicking the card (incl. "inspect ▸") opens the panel if collapsed
|
|
358
358
|
};
|
|
359
359
|
div.querySelector('.fav-btn').onclick = (e) => {
|
|
360
360
|
e.stopPropagation();
|
|
@@ -379,13 +379,26 @@ function wireEl(idx, label) {
|
|
|
379
379
|
return wrap;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
|
|
382
|
+
// `reveal` is set by USER-initiated selections (clicking a node card / the
|
|
383
|
+
// "inspect ▸" hint / a favorite chip). When the Inspect panel is collapsed those
|
|
384
|
+
// would otherwise give zero visible feedback — the panel quietly re-renders behind
|
|
385
|
+
// the rail (issue #52) — so a revealing selection also expands it. Programmatic /
|
|
386
|
+
// initial selection (on render) passes no reveal, so a saved collapsed preference
|
|
387
|
+
// is respected on load.
|
|
388
|
+
function selectAgent(id, { reveal = false } = {}) {
|
|
383
389
|
state.selectedAgentId = id;
|
|
384
390
|
document.querySelectorAll('.agent-card').forEach(c => {
|
|
385
391
|
c.classList.toggle('selected', c.dataset.agentId === id);
|
|
386
392
|
});
|
|
387
393
|
const a = AGENTS[id];
|
|
388
394
|
if (a) $inspectRole.textContent = a.title.toLowerCase();
|
|
395
|
+
if (reveal && state.collapse.right) {
|
|
396
|
+
// Momentary reveal — expand the panel for THIS interaction but do NOT persist:
|
|
397
|
+
// the user collapsed it deliberately, so a single inspect must not overwrite
|
|
398
|
+
// that saved preference (only an explicit toggle/drag does). UX review on #52.
|
|
399
|
+
state.collapse.right = false;
|
|
400
|
+
applyCollapse();
|
|
401
|
+
}
|
|
389
402
|
renderInspect();
|
|
390
403
|
}
|
|
391
404
|
|
|
@@ -624,7 +637,7 @@ function renderFavBar() {
|
|
|
624
637
|
const id = chip.dataset.favAgent;
|
|
625
638
|
const p = PROMPTS[state.promptKey];
|
|
626
639
|
if (nodeIds(p).includes(id)) {
|
|
627
|
-
selectAgent(id);
|
|
640
|
+
selectAgent(id, { reveal: true });
|
|
628
641
|
} else {
|
|
629
642
|
chip.style.borderColor = 'var(--warn)';
|
|
630
643
|
setTimeout(() => chip.style.borderColor = '', 600);
|
package/dist/web/aware.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
const $reportOpen = document.getElementById('report-open');
|
|
25
25
|
const $reportShare = document.getElementById('report-share');
|
|
26
26
|
const $reportClose = document.getElementById('report-close');
|
|
27
|
+
const $reportStop = document.getElementById('report-stop');
|
|
27
28
|
const $stopRunBtn = document.getElementById('stop-run-btn');
|
|
28
29
|
|
|
29
30
|
// One persistent array the inspect Execution tab reads; we mutate in place so
|
|
@@ -143,6 +144,47 @@
|
|
|
143
144
|
.join('')}</table>`;
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
// Plain-text summary of one declared command input (for the Skill tab's inputs
|
|
148
|
+
// table — kvTable escapes it, so this stays text, no markup): "type (enum vals)
|
|
149
|
+
// · default X — description".
|
|
150
|
+
function inputSummary(i) {
|
|
151
|
+
let s = i.type || 'string';
|
|
152
|
+
if (i.values && i.values.length) s += ` (${i.values.join(' · ')})`;
|
|
153
|
+
if (i.default !== null && i.default !== undefined && i.default !== '') {
|
|
154
|
+
s += ` · default ${typeof i.default === 'string' ? i.default : JSON.stringify(i.default)}`;
|
|
155
|
+
}
|
|
156
|
+
if (i.description) s += ` — ${i.description}`;
|
|
157
|
+
return s;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Inspect → Skill tab body. Rich when the agent manifest is installed (n.skill):
|
|
161
|
+
// what the agent + command do, capabilities, the command's DECLARED inputs and
|
|
162
|
+
// output schema, and a docs link. Falls back to the lock-pointer summary for
|
|
163
|
+
// exec/agent-less nodes or an uninstalled agent. DISPLAY of manifest data only —
|
|
164
|
+
// every file-derived string is escaped (kvTable/escapeHtml), never injected raw.
|
|
165
|
+
function skillBody(n, fallback, notesHtml) {
|
|
166
|
+
const sk = n.skill;
|
|
167
|
+
if (!sk) return fallback; // fallback already carries notesHtml
|
|
168
|
+
const modeBadge = `<span class="cap-badge${n.mode === 'write' ? ' write' : ''}">${escapeHtml(n.mode)}</span>`;
|
|
169
|
+
const capBadges = sk.capabilities.map((c) => `<span class="cap-badge">${escapeHtml(c)}</span>`).join(' ');
|
|
170
|
+
const inputsObj = Object.fromEntries(sk.inputs.map((i) => [i.name, inputSummary(i)]));
|
|
171
|
+
const docs = sk.homepage && /^https?:\/\//i.test(sk.homepage)
|
|
172
|
+
? `<p class="skill-docs-row"><a class="skill-docs" href="${escapeHtml(sk.homepage)}" target="_blank" rel="noopener">Agent documentation ↗</a></p>`
|
|
173
|
+
: '';
|
|
174
|
+
const outputs = sk.outputs
|
|
175
|
+
? `<p><strong>Returns</strong> · ${escapeHtml(sk.outputs.type)}</p>${Object.keys(sk.outputs.schema).length ? kvTable(sk.outputs.schema) : ''}`
|
|
176
|
+
: '';
|
|
177
|
+
return `
|
|
178
|
+
<h3>${escapeHtml(sk.agentDisplayName)}</h3>
|
|
179
|
+
${sk.agentDescription ? `<p>${escapeHtml(sk.agentDescription)}</p>` : ''}
|
|
180
|
+
<h3>Command · <code>${escapeHtml(sk.command)}</code></h3>
|
|
181
|
+
${sk.commandDescription ? `<p>${escapeHtml(sk.commandDescription)}</p>` : ''}
|
|
182
|
+
<p><strong>Mode</strong> ${modeBadge}${capBadges ? ' · <strong>Requires</strong> ' + capBadges : ''}</p>
|
|
183
|
+
<p><strong>Accepts</strong></p>${Object.keys(inputsObj).length ? kvTable(inputsObj) : '<p class="dim-note">No declared inputs.</p>'}
|
|
184
|
+
${outputs}
|
|
185
|
+
${docs}${notesHtml || ''}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
146
188
|
// Dependency-free C# highlighter for the Code tab. Single-pass scanner that
|
|
147
189
|
// handles comments/strings/chars BEFORE keywords (so keywords inside strings
|
|
148
190
|
// aren't recolored), escapes every token's text, and reuses the demo's token
|
|
@@ -249,8 +291,14 @@
|
|
|
249
291
|
subtitle: `${agentLabel}${cmd ? '/' + cmd : ''} · ${mode}`,
|
|
250
292
|
blurb: n.notes[0] ? escapeHtml(humanizeNote(n.notes[0].text)) : `${mode}-mode ${cmd ? '<code>' + cmd + '</code>' : escapeHtml(n.kind)} node`,
|
|
251
293
|
description: `${plainDesc ? `<p>${escapeHtml(plainDesc)}</p>` : ''}<p>Resolves to ${modeBadge} via <code>${escapeHtml(n.agent || n.kind)}${n.command ? '.' + escapeHtml(n.command) : ''}</code>.</p>${Object.keys(inputsNoCode).length ? `<p><strong>Inputs</strong></p>${kvTable(inputsNoCode)}` : ''}${execSource ? `<p class="dim-note">Full source in the <strong>Code</strong> tab.</p>` : ''}${notesHtml}`,
|
|
252
|
-
|
|
294
|
+
// Rich agent/command detail when the manifest is installed (n.skill); else
|
|
295
|
+
// the lock-pointer fallback below (exec/agent-less nodes, uninstalled agents).
|
|
296
|
+
skill: skillBody(
|
|
297
|
+
n,
|
|
298
|
+
`<h3>Provided by</h3><p><code>${escapeHtml(n.agent || n.kind)}</code>${pin ? ' · pinned <code>v' + escapeHtml(pin) + '</code>' : ''}${n.command ? ' · command <code>' + escapeHtml(n.command) + '</code>' : ''}</p>
|
|
253
299
|
<p>Resolved write/read mode is part of the approved <code>.lock</code> — see the <strong>Code</strong> tab.</p>${notesHtml}`,
|
|
300
|
+
notesHtml,
|
|
301
|
+
),
|
|
254
302
|
// Code tab: the REAL node source. For exec nodes (Roslyn C# run against a
|
|
255
303
|
// live host) that's `config.code` straight from the .flo — the same text
|
|
256
304
|
// the host compiles, so "Debug in VS" attaches to exactly this. For
|
|
@@ -330,8 +378,13 @@
|
|
|
330
378
|
|
|
331
379
|
// Keep Run disabled (and the header Stop shown) if a gate re-paint lands mid-run (#39) —
|
|
332
380
|
// never let a re-render re-arm Run while a run is still in flight.
|
|
333
|
-
|
|
334
|
-
|
|
381
|
+
// …including a live foreground trigger session (▶ Run on a streaming app): a
|
|
382
|
+
// gate repaint mid-session (e.g. an fs-change → loadApp) must not re-arm Run or
|
|
383
|
+
// hide the Stop while the watcher is still live. The in-modal Stop tracks the
|
|
384
|
+
// same state (it's the reachable Stop while the Viewer covers the header one).
|
|
385
|
+
$runBtn.disabled = !app.runnable || reportRunning || state.running || !!foregroundTrigger;
|
|
386
|
+
if ($stopRunBtn) $stopRunBtn.hidden = !(reportRunning || state.running || foregroundTrigger);
|
|
387
|
+
if ($reportStop) $reportStop.hidden = !foregroundTrigger;
|
|
335
388
|
$runBtn.dataset.tip = app.runnable
|
|
336
389
|
? 'Run the approved workflow'
|
|
337
390
|
: app.runState === 'drift'
|
|
@@ -911,6 +964,16 @@
|
|
|
911
964
|
// double-click LOAD the last result instantly instead of re-running.
|
|
912
965
|
const lastReportByApp = new Map();
|
|
913
966
|
|
|
967
|
+
// A live FOREGROUND trigger session — ▶ Run on a streaming/watch app (one whose
|
|
968
|
+
// source is a lifecycle:start watch). The finite run can't complete for a stream,
|
|
969
|
+
// so Run hosts a live session that re-renders the report in the Viewer on every
|
|
970
|
+
// fired event until Stop. `{ appId, sessionId, firedCount }` while live, else null.
|
|
971
|
+
// Distinct from a background routine (⏱ Routines) and the finite report run
|
|
972
|
+
// (reportRunning). Its session id is server-namespaced with this prefix so its SSE
|
|
973
|
+
// snapshots route here, never to the Routines list.
|
|
974
|
+
const FG_SESSION_PREFIX = '__run__:';
|
|
975
|
+
let foregroundTrigger = null;
|
|
976
|
+
|
|
914
977
|
// Reflect run-in-flight state in the ALWAYS-VISIBLE header so a run is stoppable even
|
|
915
978
|
// when the HTML Viewer modal is closed (#39). The header ■ Stop run appears for the whole
|
|
916
979
|
// duration of either run path (the modal run `reportRunning` and the inline run
|
|
@@ -919,7 +982,10 @@
|
|
|
919
982
|
// the user, on the modal's × tooltip, that closing leaves the run going (Stop is in the
|
|
920
983
|
// header) — the canvas keeps showing progress (markCanvasRunning), by design.
|
|
921
984
|
function syncRunControls() {
|
|
922
|
-
const running = reportRunning || state.running;
|
|
985
|
+
const running = reportRunning || state.running || !!foregroundTrigger;
|
|
986
|
+
// The in-modal Stop is reachable only for a live foreground session (the header
|
|
987
|
+
// Stop is occluded by the report-modal backdrop). Show it while one is live.
|
|
988
|
+
if ($reportStop) $reportStop.hidden = !foregroundTrigger;
|
|
923
989
|
if ($stopRunBtn) {
|
|
924
990
|
$stopRunBtn.hidden = !running;
|
|
925
991
|
// Reset to a fresh, clickable Stop each run; stopRun() flips it to "Cancelling…".
|
|
@@ -945,7 +1011,22 @@
|
|
|
945
1011
|
// pulse, and asks the server to kill the `aware app run` child. The in-flight
|
|
946
1012
|
// /api/run then rejects via api() and the catch renders the cancelled state.
|
|
947
1013
|
async function stopRun() {
|
|
948
|
-
if (!reportRunning && !state.running) return;
|
|
1014
|
+
if (!reportRunning && !state.running && !foregroundTrigger) return;
|
|
1015
|
+
// A live foreground session ends via its own stop route (it runs outside the
|
|
1016
|
+
// run lock, so /api/run/stop would no-op). Clear the flag first so the late
|
|
1017
|
+
// `stopped` SSE snapshot is a harmless no-op rather than a double-handle.
|
|
1018
|
+
if (foregroundTrigger) {
|
|
1019
|
+
const ft = foregroundTrigger;
|
|
1020
|
+
foregroundTrigger = null;
|
|
1021
|
+
if ($stopRunBtn) { $stopRunBtn.disabled = true; $stopRunBtn.textContent = 'Stopping…'; }
|
|
1022
|
+
const ovStop = document.querySelector('.overlay-stop');
|
|
1023
|
+
if (ovStop) { ovStop.disabled = true; ovStop.textContent = 'Stopping…'; }
|
|
1024
|
+
try { await api(`/api/trigger-run/${encodeURIComponent(ft.appId)}/stop`, { method: 'POST' }); } catch { /* best effort — the session may already be gone */ }
|
|
1025
|
+
// A deliberate stop needs no toast — the subtitle change is the confirmation.
|
|
1026
|
+
paintForegroundStopped('stopped');
|
|
1027
|
+
syncRunControls();
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
949
1030
|
cancelRequested = true;
|
|
950
1031
|
clearNodeStatus();
|
|
951
1032
|
// Reflect "cancelling" on BOTH Stop surfaces — the in-modal overlay and the header.
|
|
@@ -1308,6 +1389,10 @@
|
|
|
1308
1389
|
});
|
|
1309
1390
|
|
|
1310
1391
|
$reportClose.onclick = () => hideModal($reportModal);
|
|
1392
|
+
// The in-modal Stop for a live foreground session: the always-on header Stop sits
|
|
1393
|
+
// BEHIND the report modal's backdrop, so a session shown in the Viewer needs its
|
|
1394
|
+
// own reachable Stop. Visibility is driven by syncRunControls (foregroundTrigger).
|
|
1395
|
+
if ($reportStop) $reportStop.onclick = () => stopRun();
|
|
1311
1396
|
$reportModal.addEventListener('click', (e) => { if (e.target === $reportModal) hideModal($reportModal); });
|
|
1312
1397
|
// The Stop button is rebuilt into the overlay each run — delegate so one
|
|
1313
1398
|
// listener survives every innerHTML swap.
|
|
@@ -2127,7 +2212,12 @@
|
|
|
2127
2212
|
const app = currentId && apps.get(currentId);
|
|
2128
2213
|
if (!app) return;
|
|
2129
2214
|
if (!app.runnable) { showToast($runBtn.dataset.tip, 'warn'); return; }
|
|
2130
|
-
if (state.running || reportRunning) return;
|
|
2215
|
+
if (state.running || reportRunning || foregroundTrigger) return;
|
|
2216
|
+
|
|
2217
|
+
// Streaming/watch app → a LIVE FOREGROUND session, not a finite run (which would
|
|
2218
|
+
// never complete and time out at 60s). It re-renders the report in the Viewer on
|
|
2219
|
+
// every fired event until Stop. (Simulate still takes the normal stubbed path.)
|
|
2220
|
+
if (!simulate && app.triggerSource) { startForegroundTrigger(app); return; }
|
|
2131
2221
|
|
|
2132
2222
|
markCanvasRunning(); // immediate during-run feedback; the trace refines per node
|
|
2133
2223
|
|
|
@@ -2782,7 +2872,11 @@
|
|
|
2782
2872
|
loadRoutinesData();
|
|
2783
2873
|
if (window.flolessPanels) window.flolessPanels.refreshData(); // routine runs also refresh bindings
|
|
2784
2874
|
} else if (m.type === 'trigger-session-changed') {
|
|
2785
|
-
|
|
2875
|
+
// A foreground live-run session (▶ Run on a streaming app) shares this SSE
|
|
2876
|
+
// event with background routines; its server-namespaced id routes it to the
|
|
2877
|
+
// Viewer handler instead of the Routines list.
|
|
2878
|
+
if (typeof m.id === 'string' && m.id.startsWith(FG_SESSION_PREFIX)) applyForegroundSnapshot(m.id, m.snapshot);
|
|
2879
|
+
else applyTriggerSnapshot(m.id, m.snapshot);
|
|
2786
2880
|
} else if (m.type === 'trigger-report') {
|
|
2787
2881
|
applyTriggerReport(m);
|
|
2788
2882
|
} else if (m.type === 'connect-result') {
|
|
@@ -3248,8 +3342,10 @@
|
|
|
3248
3342
|
// button only for exec nodes (those carry C# the host can run under a debugger).
|
|
3249
3343
|
// Debug lives HERE in the main window, not in the report modal.
|
|
3250
3344
|
const _selectAgent = selectAgent;
|
|
3251
|
-
|
|
3252
|
-
|
|
3345
|
+
// Forward ALL args (id + the { reveal } opts from app.js) — dropping them here
|
|
3346
|
+
// silently swallowed the collapsed-panel reveal on inspect (issue #52).
|
|
3347
|
+
selectAgent = function selectAgentTweakAware(id, ...rest) {
|
|
3348
|
+
_selectAgent(id, ...rest);
|
|
3253
3349
|
const tb = document.getElementById('tweak-btn');
|
|
3254
3350
|
if (tb) tb.hidden = !id;
|
|
3255
3351
|
const db = document.getElementById('debug-btn');
|
|
@@ -3489,6 +3585,144 @@
|
|
|
3489
3585
|
$reportOverlay.append(mkEl('div', null, 'Listening for the next selection…'));
|
|
3490
3586
|
}
|
|
3491
3587
|
|
|
3588
|
+
// ── Foreground live run (▶ Run on a streaming/watch app) ──────────────────────
|
|
3589
|
+
// Start a live session: the Viewer re-renders the report on every fired event
|
|
3590
|
+
// (e.g. each Tekla selection change) until Stop. Reuses the exact trigger-report /
|
|
3591
|
+
// trigger-session-changed SSE path the background routine already drives — the
|
|
3592
|
+
// server hosts the long-lived `aware app run`; here we open the Viewer, surface
|
|
3593
|
+
// Stop, and reflect the live state. We never compose HTML — the Viewer relays
|
|
3594
|
+
// exactly what the node returns (applyTriggerReport paints each fresh report).
|
|
3595
|
+
async function startForegroundTrigger(app) {
|
|
3596
|
+
if (foregroundTrigger) return;
|
|
3597
|
+
const id = currentId;
|
|
3598
|
+
$reportTitle.textContent = `HTML Viewer · ${app.displayName}`;
|
|
3599
|
+
$reportModal.dataset.nodeId = reportNodeId() || '';
|
|
3600
|
+
const cached = lastReportByApp.get(id);
|
|
3601
|
+
if (cached && cached.html) paintReport(cached.html);
|
|
3602
|
+
else { $reportFrame.srcdoc = ''; $reportShare.hidden = true; }
|
|
3603
|
+
$reportOverlay.hidden = false;
|
|
3604
|
+
$reportOverlay.replaceChildren();
|
|
3605
|
+
$reportOverlay.append(mkEl('div', 'spinner'), mkEl('div', null, 'Starting live session…'));
|
|
3606
|
+
$reportSub.innerHTML = `Live run of <code>${escapeHtml(id)}</code> — the report re-renders on every event.`;
|
|
3607
|
+
showModal($reportModal);
|
|
3608
|
+
try {
|
|
3609
|
+
const res = await api('/api/trigger-run', { method: 'POST', body: JSON.stringify({ id, inputs: currentInputs() }) });
|
|
3610
|
+
foregroundTrigger = { appId: id, sessionId: res.sessionId, firedCount: 0 };
|
|
3611
|
+
syncRunControls(); // header ■ Stop run becomes reachable; Run disabled while live
|
|
3612
|
+
paintForegroundListening();
|
|
3613
|
+
appendNarration(`Live session started for <strong>${escapeHtml(id)}</strong> — the report updates on every event. Click <strong>■ Stop run</strong> (top right) to end it.`);
|
|
3614
|
+
} catch (e) {
|
|
3615
|
+
const msg = (e && e.message) ? e.message : String(e);
|
|
3616
|
+
// A refusal (a watcher already holds this host) names the concrete path back —
|
|
3617
|
+
// never a dead end. Other failures are recoverable; the server returns them
|
|
3618
|
+
// in-band, so a failed start is a viewer message + toast, not a console fault.
|
|
3619
|
+
const blocked = !!(e.body && e.body.blocked);
|
|
3620
|
+
$reportShare.hidden = true; // the overlay is the primary content — nothing to share
|
|
3621
|
+
$reportOverlay.hidden = false;
|
|
3622
|
+
$reportOverlay.replaceChildren();
|
|
3623
|
+
if (blocked) {
|
|
3624
|
+
$reportOverlay.append(
|
|
3625
|
+
mkEl('div', null, 'One live model, one watcher — a session is already watching this host.'),
|
|
3626
|
+
mkEl('div', null, 'To run here, stop it first: open ⏱ Routines and disable the watcher, then click ▶ Run workflow.'),
|
|
3627
|
+
);
|
|
3628
|
+
$reportSub.textContent = 'Live run blocked — a watcher already holds this host.';
|
|
3629
|
+
showToast('Blocked — stop the active watcher in ⏱ Routines first', 'warn');
|
|
3630
|
+
} else {
|
|
3631
|
+
$reportOverlay.append(mkEl('div', null, `Couldn't start the live session: ${msg}. Check the host is attached and the lock is fresh, then click ▶ Run workflow.`));
|
|
3632
|
+
$reportSub.textContent = 'Live session failed to start.';
|
|
3633
|
+
showToast('Live run failed: ' + msg, 'warn');
|
|
3634
|
+
}
|
|
3635
|
+
syncRunControls();
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
// SSE: a foreground live session's snapshot changed. Drives the Viewer's live
|
|
3640
|
+
// state (collecting on each fire; ended on stop/error). Ignores a stale snapshot
|
|
3641
|
+
// for a superseded/stopped session (foregroundTrigger already cleared).
|
|
3642
|
+
function applyForegroundSnapshot(id, snapshot) {
|
|
3643
|
+
if (!foregroundTrigger || id !== foregroundTrigger.sessionId || !snapshot) return;
|
|
3644
|
+
const prevFired = foregroundTrigger.firedCount || 0;
|
|
3645
|
+
foregroundTrigger.firedCount = snapshot.firedCount || 0;
|
|
3646
|
+
const fired = foregroundTrigger.firedCount > prevFired;
|
|
3647
|
+
const viewerOpen = currentId === foregroundTrigger.appId && $reportModal.classList.contains('show');
|
|
3648
|
+
if (snapshot.state === 'error') {
|
|
3649
|
+
foregroundTrigger = null;
|
|
3650
|
+
if (viewerOpen) {
|
|
3651
|
+
$reportShare.hidden = true; // the error overlay is the primary content now
|
|
3652
|
+
$reportOverlay.hidden = false;
|
|
3653
|
+
$reportOverlay.replaceChildren();
|
|
3654
|
+
$reportOverlay.append(mkEl('div', null, `Live session ended with an error: ${snapshot.error || 'the host stopped responding'}. Check Tekla is open and the workflow is compiled, then click ▶ Run workflow to try again.`));
|
|
3655
|
+
$reportSub.textContent = 'Live session error.';
|
|
3656
|
+
}
|
|
3657
|
+
showToast('Live session ended: ' + (snapshot.error || 'host error'), 'warn');
|
|
3658
|
+
syncRunControls();
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
if (snapshot.state === 'stopped') {
|
|
3662
|
+
// The stream ended on its own — a one-shot (`once`) snapshot fired then exited,
|
|
3663
|
+
// or the host closed cleanly. A clean self-exit reads as "Run complete".
|
|
3664
|
+
foregroundTrigger = null;
|
|
3665
|
+
if (viewerOpen) paintForegroundStopped('complete');
|
|
3666
|
+
syncRunControls();
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
// listening: a new fire → "collecting" spinner until applyTriggerReport paints it.
|
|
3670
|
+
if (fired && viewerOpen && !reportRunning) showTriggerCollecting();
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
// The live-but-idle state: the session is listening and no event has fired yet.
|
|
3674
|
+
// A watch app shows nothing until the user next acts in the host, so the copy
|
|
3675
|
+
// DIRECTS that action. If a previous report is cached, keep it visible behind a
|
|
3676
|
+
// live pip (NOT a spinner — nothing is computing); else an instructional prompt
|
|
3677
|
+
// with a Stop. Never a dead end, never a spinner that won't resolve on its own.
|
|
3678
|
+
function paintForegroundListening() {
|
|
3679
|
+
if (!$reportModal.classList.contains('show')) return;
|
|
3680
|
+
const cached = currentId && lastReportByApp.get(currentId);
|
|
3681
|
+
if (cached && cached.html) {
|
|
3682
|
+
$reportOverlay.hidden = true;
|
|
3683
|
+
$reportOverlay.replaceChildren();
|
|
3684
|
+
$reportShare.hidden = false; // the cached report is the primary content
|
|
3685
|
+
$reportSub.innerHTML = `<span class="live-pip" aria-hidden="true"></span>Live · waiting for the next change — change your selection in Tekla to refresh the report.`;
|
|
3686
|
+
} else {
|
|
3687
|
+
$reportFrame.srcdoc = '';
|
|
3688
|
+
$reportShare.hidden = true;
|
|
3689
|
+
$reportOverlay.hidden = false;
|
|
3690
|
+
$reportOverlay.replaceChildren();
|
|
3691
|
+
$reportOverlay.append(
|
|
3692
|
+
mkEl('div', null, 'Listening for events — change your selection in Tekla to generate the first report.'),
|
|
3693
|
+
Object.assign(mkEl('button', 'overlay-stop', '■ Stop run'), { type: 'button' }),
|
|
3694
|
+
);
|
|
3695
|
+
$reportSub.innerHTML = `<span class="live-pip" aria-hidden="true"></span>Live · listening…`;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
// The session ended. `reason` distinguishes a clean self-exit ('complete' — e.g. a
|
|
3700
|
+
// one-shot `once` snapshot) from a deliberate Stop ('stopped'). Either way keep the
|
|
3701
|
+
// last report visible; the path back is ▶ Run workflow.
|
|
3702
|
+
function paintForegroundStopped(reason) {
|
|
3703
|
+
if (!$reportModal.classList.contains('show')) return;
|
|
3704
|
+
const complete = reason === 'complete';
|
|
3705
|
+
const cached = currentId && lastReportByApp.get(currentId);
|
|
3706
|
+
if (complete) showToast('Run complete', 'ok');
|
|
3707
|
+
if (cached && cached.html) {
|
|
3708
|
+
$reportOverlay.hidden = true;
|
|
3709
|
+
$reportOverlay.replaceChildren();
|
|
3710
|
+
$reportShare.hidden = false;
|
|
3711
|
+
$reportSub.innerHTML = complete
|
|
3712
|
+
? `Run complete · last report above — click <strong>▶ Run workflow</strong> to run again.`
|
|
3713
|
+
: `Live session stopped · last report above — click <strong>▶ Run workflow</strong> to start again.`;
|
|
3714
|
+
} else {
|
|
3715
|
+
$reportFrame.srcdoc = '';
|
|
3716
|
+
$reportShare.hidden = true;
|
|
3717
|
+
$reportOverlay.hidden = false;
|
|
3718
|
+
$reportOverlay.replaceChildren();
|
|
3719
|
+
$reportOverlay.append(mkEl('div', null, complete
|
|
3720
|
+
? 'Run complete. Click ▶ Run workflow to run again.'
|
|
3721
|
+
: 'Live session stopped. Click ▶ Run workflow to start again.'));
|
|
3722
|
+
$reportSub.textContent = complete ? 'Run complete.' : 'Live session stopped.';
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3492
3726
|
function renderRoutinesList() {
|
|
3493
3727
|
const $list = document.getElementById('routines-list');
|
|
3494
3728
|
const $quota = document.getElementById('rtn-quota');
|
package/dist/web/index.html
CHANGED
|
@@ -289,6 +289,7 @@
|
|
|
289
289
|
<div class="modal-sub" id="report-sub">Rendered from the live run — never composed by the UI.</div>
|
|
290
290
|
</div>
|
|
291
291
|
<div class="report-actions">
|
|
292
|
+
<button id="report-stop" class="ghost report-stop" hidden data-tip="Stop the live session — the report stops updating on each change">■ Stop run</button>
|
|
292
293
|
<button id="report-share" class="ghost" hidden data-tip="Share this report as a link anyone can view — hosted on floless.io">↗ Share</button>
|
|
293
294
|
<button id="report-open" class="ghost" data-tip="Open the report in a new browser tab">↗ Open</button>
|
|
294
295
|
<button id="report-close" data-tip="Close">×</button>
|